From 001f99db7c8ff1dbbc1c66caa47e0f3385166e72 Mon Sep 17 00:00:00 2001 From: Travis Shears Date: Thu, 10 Aug 2023 08:11:39 +0200 Subject: [PATCH] split code up into modules and add pubsub and timers --- CIRCUITPY/.vscode/settings.json | 15 +++ CIRCUITPY/code.py | 167 +++++--------------------- CIRCUITPY/weather_station/__init__.py | 0 CIRCUITPY/weather_station/airlift.py | 84 +++++++++++++ CIRCUITPY/weather_station/bme280.py | 73 +++++++++++ CIRCUITPY/weather_station/pubsub.py | 16 +++ CIRCUITPY/weather_station/timer.py | 10 ++ 7 files changed, 228 insertions(+), 137 deletions(-) create mode 100755 CIRCUITPY/.vscode/settings.json create mode 100755 CIRCUITPY/weather_station/__init__.py create mode 100755 CIRCUITPY/weather_station/airlift.py create mode 100755 CIRCUITPY/weather_station/bme280.py create mode 100755 CIRCUITPY/weather_station/pubsub.py create mode 100755 CIRCUITPY/weather_station/timer.py diff --git a/CIRCUITPY/.vscode/settings.json b/CIRCUITPY/.vscode/settings.json new file mode 100755 index 0000000..d1e24ed --- /dev/null +++ b/CIRCUITPY/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "python.languageServer": "None", + "python.linting.pylintEnabled": false, + "python.analysis.diagnosticSeverityOverrides": { + "reportMissingModuleSource": "none" + }, + "python.analysis.extraPaths": [ + "/Users/travis.shears/.vscode/extensions/joedevivo.vscode-circuitpython-0.1.20-darwin-arm64/boards/0x239A/0x80F4", + "/Users/travis.shears/.vscode/extensions/joedevivo.vscode-circuitpython-0.1.20-darwin-arm64/stubs", + "/Users/travis.shears/Library/Application Support/Code/User/globalStorage/joedevivo.vscode-circuitpython/bundle/20230808/adafruit-circuitpython-bundle-py-20230808/lib" + ], + "circuitpython.board.version": "8.2.0", + "circuitpython.board.vid": "0x239A", + "circuitpython.board.pid": "0x80F4" +} \ No newline at end of file diff --git a/CIRCUITPY/code.py b/CIRCUITPY/code.py index 3a20218..a6aa7b8 100755 --- a/CIRCUITPY/code.py +++ b/CIRCUITPY/code.py @@ -1,144 +1,37 @@ -import os -import board import time -import busio -from adafruit_bme280 import basic as adafruit_bme280 +import supervisor +from weather_station.pubsub import PubSub +from weather_station.timer import Timer +from weather_station.bme280 import BME280 +from weather_station.airlift import AirLift +pubsub = PubSub() +airlift = AirLift(pubsub) +bme280 = BME280(pubsub) +timers = [ + Timer(pubsub, 120), + Timer(pubsub, 86400), +] -i2c = busio.I2C(scl=board.GP15, sda=board.GP14) -bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c, 0x76) -# location's pressure (hPa) QNH at sea level -bme280.sea_level_pressure = 1016 # value from https://metar-taf.com/EDDM +board_reload_calls_count = 0 +def board_reload(_): + global board_reload_calls_count + if board_reload_calls_count > 0: + print("resetting board in 10 sec") + time.sleep(10) + supervisor.reload() + board_reload_calls_count += 1 +pubsub.subscribe('tick 86400', board_reload) -from digitalio import DigitalInOut -import adafruit_esp32spi.adafruit_esp32spi_socket as socket -from adafruit_esp32spi import adafruit_esp32spi -import adafruit_minimqtt.adafruit_minimqtt as MQTT - -esp32_cs = DigitalInOut(board.GP21) -esp32_ready = DigitalInOut(board.GP22) -esp32_reset = DigitalInOut(board.GP17) - -spi = busio.SPI(clock=board.GP18, MOSI=board.GP19, MISO=board.GP20) -esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) - -if esp.status == adafruit_esp32spi.WL_IDLE_STATUS: - print("ESP32 found and in idle mode") -print("Firmware vers.", esp.firmware_version) -print("MAC addr:", [hex(i) for i in esp.MAC_address]) - -for ap in esp.scan_networks(): - print("\t%s\t\tRSSI: %d" % (str(ap['ssid'], 'utf-8'), ap['rssi'])) - -print("Connecting to AP...") -while not esp.is_connected: - try: - esp.connect_AP( - os.getenv("WIFI_SSID"), os.getenv("WIFI_PASSWORD") - ) - except RuntimeError as e: - print("could not connect to AP, retrying: ", e) - continue -print("Connected to", str(esp.ssid, "utf-8"), "\tRSSI:", esp.rssi) - -# MQTT inspiration from https://docs.circuitpython.org/projects/minimqtt/en/latest/examples.html -mqtt_state_topic = "homeassistant/sensor/balcony_weather_station/bme_280_001/state" -mqtt_config_topic_temperature = "homeassistant/sensor/balcony_weather_station/bme_280_001_temp/config" -mqtt_config_topic_humidity = "homeassistant/sensor/balcony_weather_station/bme_280_001_humi/config" -mqtt_config_topic_pressure = "homeassistant/sensor/balcony_weather_station/bme_280_001_pres/config" - -def connect(mqtt_client, userdata, flags, rc): - print("Connected to MQTT Broker!") - print("Flags: {0}\n RC: {1}".format(flags, rc)) - -def disconnect(mqtt_client, userdata, rc): - print("Disconnected from MQTT Broker!") - -def subscribe(mqtt_client, userdata, topic, granted_qos): - print("Subscribed to {0} with QOS level {1}".format(topic, granted_qos)) - -def unsubscribe(mqtt_client, userdata, topic, pid): - print("Unsubscribed from {0} with PID {1}".format(topic, pid)) - -def publish(mqtt_client, userdata, topic, pid): - print("Published to {0} with PID {1}".format(topic, pid)) - -def message(client, topic, message): - print("New message on topic {0}: {1}".format(topic, message)) - -socket.set_interface(esp) -MQTT.set_socket(socket, esp) -mqtt_client = MQTT.MQTT( - broker=os.getenv("MQTT_HOST"), - username=os.getenv("MQTT_USER"), - password=os.getenv("MQTT_PASSWORD"), -) - -mqtt_client.on_connect = connect -mqtt_client.on_disconnect = disconnect -mqtt_client.on_subscribe = subscribe -mqtt_client.on_unsubscribe = unsubscribe -mqtt_client.on_publish = publish -mqtt_client.on_message = message - -print("Attempting to connect to %s" % mqtt_client.broker) -mqtt_client.connect() - -print("Publishing to %s" % mqtt_config_topic_temperature) -config_msg_temperature = """{ - "name": "BEM 280 Temperature", - "device_class": "temperature", - "unit_of_measurement": "°C", - "value_template": "{{ value_json.temperature}}", - "state_topic": "homeassistant/sensor/balcony_weather_station/bme_280_001/state", - "unique_id": "balcony_weather_station_bme_280_001_temperature", - "device": { - "identifiers": "balcony_weather_station_001", - "name": "Balcony Weather Station" - } -}""" -mqtt_client.publish(mqtt_config_topic_temperature, config_msg_temperature) - -print("Publishing to %s" % mqtt_config_topic_humidity) -config_msg_humidity = """{ - "name": "BEM 280 Humidity", - "device_class": "humidity", - "unit_of_measurement": "%", - "value_template": "{{ value_json.humidity}}", - "state_topic": "homeassistant/sensor/balcony_weather_station/bme_280_001/state", - "unique_id": "balcony_weather_station_bme_280_001_humidity", - "device": { - "identifiers": "balcony_weather_station_001", - "name": "Balcony Weather Station" - } -}""" -mqtt_client.publish(mqtt_config_topic_humidity, config_msg_humidity) - -print("Publishing to %s" % mqtt_config_topic_pressure) -config_msg_pressure = """{ - "name": "BEM 280 Pressure", - "device_class": "pressure", - "unit_of_measurement": "hPa", - "value_template": "{{ value_json.pressure}}", - "state_topic": "homeassistant/sensor/balcony_weather_station/bme_280_001/state", - "unique_id": "balcony_weather_station_bme_280_001_pressure", - "device": { - "identifiers": "balcony_weather_station_001", - "name": "Balcony Weather Station" - } -}""" -mqtt_client.publish(mqtt_config_topic_pressure, config_msg_pressure) - while True: - msg = f"""{{ - "temperature": {bme280.temperature:.1f}, - "humidity": {bme280.relative_humidity:.1f}, - "pressure": {bme280.pressure:.1f} - }}""" - mqtt_client.publish(mqtt_state_topic, msg) - time.sleep(30) + try: + for timer in timers: + t = time.time() + timer.tick(t) + except Exception as e: + # raise # for debugging + print("encountered problem", e) + time.sleep(60) + supervisor.reload() -print("Disconnecting from %s" % mqtt_client.broker) -mqtt_client.disconnect() -print("Done!") diff --git a/CIRCUITPY/weather_station/__init__.py b/CIRCUITPY/weather_station/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/CIRCUITPY/weather_station/airlift.py b/CIRCUITPY/weather_station/airlift.py new file mode 100755 index 0000000..f0f6e9b --- /dev/null +++ b/CIRCUITPY/weather_station/airlift.py @@ -0,0 +1,84 @@ +import os +import board +import busio +import time +from digitalio import DigitalInOut +import adafruit_esp32spi.adafruit_esp32spi_socket as socket +from adafruit_esp32spi import adafruit_esp32spi +import adafruit_minimqtt.adafruit_minimqtt as MQTT + +class AirLift(): + def __init__(self, pubsub): + self.ssid = os.getenv("WIFI_SSID") + self.wifi_pass = os.getenv("WIFI_PASSWORD") + self._setup() + pubsub.subscribe("mqtt_pub", self._publish) + def _publish(self, body): + self.mqtt_client.publish(body['topic'], body['msg']) + # print(body['msg']) + def reset(self): + print("Resetting AIRLIFT") + self.esp.reset() + time.sleep(5) + self._connect() + + def _connect(self): + while not self.esp.is_connected: + try: + print(f"Trying to connect to {self.ssid}") + self.esp.connect_AP(self.ssid, self.wifi_pass) + except ConnectionError as e: + print("Could not connect to AP, sleeping for 10 sec then trying again\n", e) + time.sleep(10) + continue + print("Connected to", str(self.esp.ssid, "utf-8"), "\tRSSI:", self.esp.rssi) + + def _setup(self): + esp32_cs = DigitalInOut(board.GP21) + esp32_ready = DigitalInOut(board.GP22) + esp32_reset = DigitalInOut(board.GP17) + spi = busio.SPI(clock=board.GP18, MOSI=board.GP19, MISO=board.GP20) + self.esp = adafruit_esp32spi.ESP_SPIcontrol(spi, esp32_cs, esp32_ready, esp32_reset) + if self.esp.status == adafruit_esp32spi.WL_IDLE_STATUS: + print("ESP32 found and in idle mode") + print("Firmware vers.", self.esp.firmware_version) + print("MAC addr:", [hex(i) for i in self.esp.MAC_address]) + self._connect() + + # MQTT inspiration from https://docs.circuitpython.org/projects/minimqtt/en/latest/examples.html + def connect(mqtt_client, userdata, flags, rc): + print("Connected to MQTT Broker!") + print("Flags: {0}\n RC: {1}".format(flags, rc)) + + def disconnect(mqtt_client, userdata, rc): + print("Disconnected from MQTT Broker!") + + def subscribe(mqtt_client, userdata, topic, granted_qos): + print("Subscribed to {0} with QOS level {1}".format(topic, granted_qos)) + + def unsubscribe(mqtt_client, userdata, topic, pid): + print("Unsubscribed from {0} with PID {1}".format(topic, pid)) + + def publish(mqtt_client, userdata, topic, pid): + print("Published to {0} with PID {1}".format(topic, pid)) + + def message(client, topic, message): + print("New message on topic {0}: {1}".format(topic, message)) + + socket.set_interface(self.esp) + MQTT.set_socket(socket, self.esp) + self.mqtt_client = MQTT.MQTT( + broker=os.getenv("MQTT_HOST"), + username=os.getenv("MQTT_USER"), + password=os.getenv("MQTT_PASSWORD"), + ) + + self.mqtt_client.on_connect = connect + self.mqtt_client.on_disconnect = disconnect + self.mqtt_client.on_subscribe = subscribe + self.mqtt_client.on_unsubscribe = unsubscribe + self.mqtt_client.on_publish = publish + self.mqtt_client.on_message = message + + print("Attempting to connect to %s" % self.mqtt_client.broker) + self.mqtt_client.connect() diff --git a/CIRCUITPY/weather_station/bme280.py b/CIRCUITPY/weather_station/bme280.py new file mode 100755 index 0000000..b83a9a0 --- /dev/null +++ b/CIRCUITPY/weather_station/bme280.py @@ -0,0 +1,73 @@ +import board +import busio +from adafruit_bme280 import basic as adafruit_bme280 + +class BME280(): + mqtt_state_topic = "homeassistant/sensor/balcony_weather_station/bme_280_001/state" + mqtt_config_topic_temperature = "homeassistant/sensor/balcony_weather_station/bme_280_001_temp/config" + mqtt_config_topic_humidity = "homeassistant/sensor/balcony_weather_station/bme_280_001_humi/config" + mqtt_config_topic_pressure = "homeassistant/sensor/balcony_weather_station/bme_280_001_pres/config" + config_msg_temperature = """{ + "name": "BEM 280 Temperature", + "device_class": "temperature", + "unit_of_measurement": "°C", + "value_template": "{{ value_json.temperature}}", + "state_topic": "homeassistant/sensor/balcony_weather_station/bme_280_001/state", + "unique_id": "balcony_weather_station_bme_280_001_temperature", + "device": { + "identifiers": "balcony_weather_station_001", + "name": "Balcony Weather Station" + } + }""" + config_msg_pressure = """{ + "name": "BEM 280 Pressure", + "device_class": "pressure", + "unit_of_measurement": "hPa", + "value_template": "{{ value_json.pressure}}", + "state_topic": "homeassistant/sensor/balcony_weather_station/bme_280_001/state", + "unique_id": "balcony_weather_station_bme_280_001_pressure", + "device": { + "identifiers": "balcony_weather_station_001", + "name": "Balcony Weather Station" + } + }""" + config_msg_humidity = """{ + "name": "BEM 280 Humidity", + "device_class": "humidity", + "unit_of_measurement": "%", + "value_template": "{{ value_json.humidity}}", + "state_topic": "homeassistant/sensor/balcony_weather_station/bme_280_001/state", + "unique_id": "balcony_weather_station_bme_280_001_humidity", + "device": { + "identifiers": "balcony_weather_station_001", + "name": "Balcony Weather Station" + } + }""" + + def __init__(self, pubsub): + self.pubsub = pubsub + i2c = busio.I2C(scl=board.GP15, sda=board.GP14) + self.bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c, 0x76) + self.bme280.sea_level_pressure = 1016 # value from https://metar-taf.com/EDDM + self.pubsub.publish('mqtt_pub', { + 'topic': self.mqtt_config_topic_temperature, + 'msg': self.config_msg_temperature + }) + self.pubsub.publish('mqtt_pub', { + 'topic': self.mqtt_config_topic_humidity, + 'msg': self.config_msg_humidity + }) + self.pubsub.publish('mqtt_pub', { + 'topic': self.mqtt_config_topic_pressure, + 'msg': self.config_msg_pressure + }) + self.pubsub.subscribe('tick 120', self._send) + + def _send(self, _): + msg = f"""{{ + "temperature": {self.bme280.temperature:.1f}, + "humidity": {self.bme280.relative_humidity:.1f}, + "pressure": {self.bme280.pressure:.1f} + }}""" + self.pubsub.publish('mqtt_pub', {'topic': self.mqtt_state_topic, 'msg': msg}) + diff --git a/CIRCUITPY/weather_station/pubsub.py b/CIRCUITPY/weather_station/pubsub.py new file mode 100755 index 0000000..b551562 --- /dev/null +++ b/CIRCUITPY/weather_station/pubsub.py @@ -0,0 +1,16 @@ +class PubSub: + def __init__(self): + self.cbs = {} + def _check_msg_type(self, msg_type): + if msg_type not in self.cbs: + self.cbs[msg_type] = [] + + def subscribe(self, msg_type, cb): + self._check_msg_type(msg_type) + self.cbs[msg_type].append(cb) + + def publish(self, msg_type, body): + self._check_msg_type(msg_type) + print(f"publishing msg {msg_type}") + for cb in self.cbs[msg_type]: + cb(body) \ No newline at end of file diff --git a/CIRCUITPY/weather_station/timer.py b/CIRCUITPY/weather_station/timer.py new file mode 100755 index 0000000..302384b --- /dev/null +++ b/CIRCUITPY/weather_station/timer.py @@ -0,0 +1,10 @@ +class Timer: + def __init__(self, pubsub, timer_length): + self.timer_length = timer_length + self.last_tick = 0 + self.pubsub = pubsub + self.topic = f"tick {self.timer_length}" + def tick(self, now): + if (now - self.last_tick) > self.timer_length: + self.last_tick = now + self.pubsub.publish(self.topic, {})