From 31c7f870e0568c5762cdced299260584f636ccd6 Mon Sep 17 00:00:00 2001 From: Gregory Ballantine Date: Sat, 12 Sep 2026 17:18:41 -0400 Subject: [PATCH] [Issue #4] - Adding support for sht4x sensors, configurable via the SENSOR_TYPE parameter --- config.example.py | 3 ++ main.py | 109 ++++++++++++++++++++++++++++++++++------------ 2 files changed, 83 insertions(+), 29 deletions(-) diff --git a/config.example.py b/config.example.py index 47580f5..eb7a548 100644 --- a/config.example.py +++ b/config.example.py @@ -12,3 +12,6 @@ MQTT_PASS = "mqtt_password" # MQTT password (leave blank for no login) NODE_NAME = "Room Weather" # Name that's displayed on HA Device Card NODE_ID = "room_env" # Unique slug for entity_id and MQTT topic path CLIENT_ID = f"pico2w_{NODE_ID}" # Unique client ID used by the MQTT broker + +# Sensor selection: "bme280" or "sht41" +SENSOR_TYPE = "bme280" diff --git a/main.py b/main.py index 9cba5e7..8c3c55c 100644 --- a/main.py +++ b/main.py @@ -15,8 +15,8 @@ except ImportError: while True: time.sleep(2) -# Default NODE_NAME to NODE_ID if not explicitly provided in config.py NODE_NAME = getattr(Config, "NODE_NAME", Config.NODE_ID) +SENSOR_TYPE = getattr(Config, "SENSOR_TYPE", "bme280").lower() STATE_TOPIC = f"homeassistant/sensor/{Config.NODE_ID}/state" @@ -35,19 +35,16 @@ class SimpleMQTT: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect(addr) - # Flags: Clean session (bit 1) + optional credentials flags = 0x02 if username: flags |= 0x80 if password: flags |= 0x40 - # Variable header: Protocol Name (MQTT) + Level (4) + Flags + KeepAlive (60s) var_header = bytearray( [0x00, 0x04, ord("M"), ord("Q"), ord("T"), ord("T"), 0x04, flags, 0x00, 0x3C] ) - # Payload payload = bytearray() payload.extend(self._encode_str(client_id)) if username: @@ -59,7 +56,6 @@ class SimpleMQTT: packet = bytearray([0x10]) + self._encode_len(len(body)) + body self.sock.write(packet) - # Read CONNACK (4 bytes: 0x20, 0x02, ack_flags, return_code) resp = self.sock.read(4) if not resp or resp[0] != 0x20 or resp[3] != 0x00: code = resp[3] if resp and len(resp) >= 4 else "nil" @@ -202,16 +198,53 @@ class BME280: } +# ------------------------------------------------------------- +# SHT41 Driver (I2C) +# ------------------------------------------------------------- +class SHT41: + DEFAULT_ADDR = 0x44 + CMD_MEASURE_HIGH_PRECISION = 0xFD + + def __init__(self, i2c, addr=DEFAULT_ADDR): + self.i2c = i2c + self.addr = addr + # Soft reset + self.i2c.writeto(self.addr, b"\x94") + time.sleep(0.01) + + def read_values(self): + # Trigger high precision measurement + self.i2c.writeto(self.addr, bytes([self.CMD_MEASURE_HIGH_PRECISION])) + # Conversion takes up to 8.2ms + time.sleep(0.01) + + # 6 bytes: [Temp_MSB, Temp_LSB, CRC, Hum_MSB, Hum_LSB, CRC] + data = self.i2c.readfrom(self.addr, 6) + + raw_t = (data[0] << 8) | data[1] + raw_h = (data[3] << 8) | data[4] + + # Official Sensirion conversion equations + temp = -45.0 + 175.0 * (raw_t / 65535.0) + hum = -6.0 + 125.0 * (raw_h / 65535.0) + hum = max(0.0, min(hum, 100.0)) + + return { + "temperature": round(temp, 2), + "humidity": round(hum, 2), + "pressure": None, + } + + # ------------------------------------------------------------- # Home Assistant MQTT Discovery # ------------------------------------------------------------- -def register_ha_discovery(mqtt): - # Device naming uses NODE_NAME so each board has its own clean device card in HA +def register_ha_discovery(mqtt, sensor_type): device_info = ( f'"device":{{' f'"identifiers":["{Config.NODE_ID}"],' f'"name":"{NODE_NAME}",' - f'"model":"Pico 2 W",' + f'"model":"Pico 2 W ({sensor_type.upper()})",' f'"manufacturer":"Raspberry Pi"' f"}}" ) @@ -231,15 +264,20 @@ def register_ha_discovery(mqtt): "cls": "humidity", "val_tpl": "{{ value_json.humidity }}", }, - { - "id": "pressure", - "name": "Pressure", - "unit": "hPa", - "cls": "atmospheric_pressure", - "val_tpl": "{{ value_json.pressure }}", - }, ] + # Only register atmospheric pressure if the sensor provides it + if sensor_type == "bme280": + configs.append( + { + "id": "pressure", + "name": "Pressure", + "unit": "hPa", + "cls": "atmospheric_pressure", + "val_tpl": "{{ value_json.pressure }}", + } + ) + for c in configs: topic = f"homeassistant/sensor/{Config.NODE_ID}/{c['id']}/config" unique_id = f"{Config.NODE_ID}_{c['id']}" @@ -281,7 +319,6 @@ def connect_wifi(): # Panic when the sensor isn't ready # ------------------------------------------------------------- def panic(err=None): - """Signals a fatal fault by blinking the onboard LED continuously.""" if err: print(f"Fatal error: {err}") led = Pin("LED", Pin.OUT) @@ -294,9 +331,17 @@ def panic(err=None): # Main Execution Loop # ------------------------------------------------------------- def main(): - # Adjust SDA/SCL pins according to your wiring - i2c = SoftI2C(sda=Pin(16), scl=Pin(17), freq=100000) - bme = BME280(i2c) + i2c = SoftI2C(sda=Pin(4), scl=Pin(5), freq=100000) + + # Initialize selected sensor + if SENSOR_TYPE == "sht41": + print("Using SHT41 sensor (0x44)") + sensor = SHT41(i2c) + elif SENSOR_TYPE == "bme280": + print("Using BME280 sensor (0x77)") + sensor = BME280(i2c) + else: + raise ValueError(f"Unsupported SENSOR_TYPE '{SENSOR_TYPE}'. Use 'bme280' or 'sht41'.") connect_wifi() @@ -310,18 +355,27 @@ def main(): print("MQTT connected.") # Publish discovery retained messages - register_ha_discovery(mqtt) + register_ha_discovery(mqtt, SENSOR_TYPE) # Telemetry loop interval = getattr(Config, "UPDATE_INTERVAL", 30) while True: try: - data = bme.read_values() - state_json = ( - f'{{"temperature":{data["temperature"]},' - f'"humidity":{data["humidity"]},' - f'"pressure":{data["pressure"]}}}' - ) + data = sensor.read_values() + + # Format payload conditionally based on available readings + if data.get("pressure") is not None: + state_json = ( + f'{{"temperature":{data["temperature"]},' + f'"humidity":{data["humidity"]},' + f'"pressure":{data["pressure"]}}}' + ) + else: + state_json = ( + f'{{"temperature":{data["temperature"]},' + f'"humidity":{data["humidity"]}}}' + ) + mqtt.publish(STATE_TOPIC, state_json) print(f"[{NODE_NAME}] Published: {state_json}") except Exception as e: @@ -338,10 +392,7 @@ def main(): if __name__ == "__main__": - # Allow 3.3V power rails and I2C registers to settle on cold boot time.sleep(2) - - # Indicate startup has begun Pin("LED", Pin.OUT).on() try: