[Issue #4] - Adding support for sht4x sensors, configurable via the SENSOR_TYPE parameter
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
@@ -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_NAME = "Room Weather" # Name that's displayed on HA Device Card
|
||||||
NODE_ID = "room_env" # Unique slug for entity_id and MQTT topic path
|
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
|
CLIENT_ID = f"pico2w_{NODE_ID}" # Unique client ID used by the MQTT broker
|
||||||
|
|
||||||
|
# Sensor selection: "bme280" or "sht41"
|
||||||
|
SENSOR_TYPE = "bme280"
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ except ImportError:
|
|||||||
while True:
|
while True:
|
||||||
time.sleep(2)
|
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)
|
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"
|
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 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
self.sock.connect(addr)
|
self.sock.connect(addr)
|
||||||
|
|
||||||
# Flags: Clean session (bit 1) + optional credentials
|
|
||||||
flags = 0x02
|
flags = 0x02
|
||||||
if username:
|
if username:
|
||||||
flags |= 0x80
|
flags |= 0x80
|
||||||
if password:
|
if password:
|
||||||
flags |= 0x40
|
flags |= 0x40
|
||||||
|
|
||||||
# Variable header: Protocol Name (MQTT) + Level (4) + Flags + KeepAlive (60s)
|
|
||||||
var_header = bytearray(
|
var_header = bytearray(
|
||||||
[0x00, 0x04, ord("M"), ord("Q"), ord("T"), ord("T"), 0x04, flags, 0x00, 0x3C]
|
[0x00, 0x04, ord("M"), ord("Q"), ord("T"), ord("T"), 0x04, flags, 0x00, 0x3C]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Payload
|
|
||||||
payload = bytearray()
|
payload = bytearray()
|
||||||
payload.extend(self._encode_str(client_id))
|
payload.extend(self._encode_str(client_id))
|
||||||
if username:
|
if username:
|
||||||
@@ -59,7 +56,6 @@ class SimpleMQTT:
|
|||||||
packet = bytearray([0x10]) + self._encode_len(len(body)) + body
|
packet = bytearray([0x10]) + self._encode_len(len(body)) + body
|
||||||
self.sock.write(packet)
|
self.sock.write(packet)
|
||||||
|
|
||||||
# Read CONNACK (4 bytes: 0x20, 0x02, ack_flags, return_code)
|
|
||||||
resp = self.sock.read(4)
|
resp = self.sock.read(4)
|
||||||
if not resp or resp[0] != 0x20 or resp[3] != 0x00:
|
if not resp or resp[0] != 0x20 or resp[3] != 0x00:
|
||||||
code = resp[3] if resp and len(resp) >= 4 else "nil"
|
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
|
# Home Assistant MQTT Discovery
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
def register_ha_discovery(mqtt):
|
def register_ha_discovery(mqtt, sensor_type):
|
||||||
# Device naming uses NODE_NAME so each board has its own clean device card in HA
|
|
||||||
device_info = (
|
device_info = (
|
||||||
f'"device":{{'
|
f'"device":{{'
|
||||||
f'"identifiers":["{Config.NODE_ID}"],'
|
f'"identifiers":["{Config.NODE_ID}"],'
|
||||||
f'"name":"{NODE_NAME}",'
|
f'"name":"{NODE_NAME}",'
|
||||||
f'"model":"Pico 2 W",'
|
f'"model":"Pico 2 W ({sensor_type.upper()})",'
|
||||||
f'"manufacturer":"Raspberry Pi"'
|
f'"manufacturer":"Raspberry Pi"'
|
||||||
f"}}"
|
f"}}"
|
||||||
)
|
)
|
||||||
@@ -231,15 +264,20 @@ def register_ha_discovery(mqtt):
|
|||||||
"cls": "humidity",
|
"cls": "humidity",
|
||||||
"val_tpl": "{{ value_json.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:
|
for c in configs:
|
||||||
topic = f"homeassistant/sensor/{Config.NODE_ID}/{c['id']}/config"
|
topic = f"homeassistant/sensor/{Config.NODE_ID}/{c['id']}/config"
|
||||||
unique_id = f"{Config.NODE_ID}_{c['id']}"
|
unique_id = f"{Config.NODE_ID}_{c['id']}"
|
||||||
@@ -281,7 +319,6 @@ def connect_wifi():
|
|||||||
# Panic when the sensor isn't ready
|
# Panic when the sensor isn't ready
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
def panic(err=None):
|
def panic(err=None):
|
||||||
"""Signals a fatal fault by blinking the onboard LED continuously."""
|
|
||||||
if err:
|
if err:
|
||||||
print(f"Fatal error: {err}")
|
print(f"Fatal error: {err}")
|
||||||
led = Pin("LED", Pin.OUT)
|
led = Pin("LED", Pin.OUT)
|
||||||
@@ -294,9 +331,17 @@ def panic(err=None):
|
|||||||
# Main Execution Loop
|
# Main Execution Loop
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
def main():
|
def main():
|
||||||
# Adjust SDA/SCL pins according to your wiring
|
i2c = SoftI2C(sda=Pin(4), scl=Pin(5), freq=100000)
|
||||||
i2c = SoftI2C(sda=Pin(16), scl=Pin(17), freq=100000)
|
|
||||||
bme = BME280(i2c)
|
# 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()
|
connect_wifi()
|
||||||
|
|
||||||
@@ -310,18 +355,27 @@ def main():
|
|||||||
print("MQTT connected.")
|
print("MQTT connected.")
|
||||||
|
|
||||||
# Publish discovery retained messages
|
# Publish discovery retained messages
|
||||||
register_ha_discovery(mqtt)
|
register_ha_discovery(mqtt, SENSOR_TYPE)
|
||||||
|
|
||||||
# Telemetry loop
|
# Telemetry loop
|
||||||
interval = getattr(Config, "UPDATE_INTERVAL", 30)
|
interval = getattr(Config, "UPDATE_INTERVAL", 30)
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
data = bme.read_values()
|
data = sensor.read_values()
|
||||||
state_json = (
|
|
||||||
f'{{"temperature":{data["temperature"]},'
|
# Format payload conditionally based on available readings
|
||||||
f'"humidity":{data["humidity"]},'
|
if data.get("pressure") is not None:
|
||||||
f'"pressure":{data["pressure"]}}}'
|
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)
|
mqtt.publish(STATE_TOPIC, state_json)
|
||||||
print(f"[{NODE_NAME}] Published: {state_json}")
|
print(f"[{NODE_NAME}] Published: {state_json}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -338,10 +392,7 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Allow 3.3V power rails and I2C registers to settle on cold boot
|
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
# Indicate startup has begun
|
|
||||||
Pin("LED", Pin.OUT).on()
|
Pin("LED", Pin.OUT).on()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user