#!/usr/bin/env python3 """ Weather Board → Home Assistant via MQTT with auto-discovery. Reads BME280 (temperature, humidity, pressure) and SI1132 (UV index, visible light, IR) over I2C and publishes to an MQTT broker using Home Assistant's MQTT discovery protocol. Usage: python3 mqtt_ha.py """ import json import signal import sys import time import paho.mqtt.client as mqtt import BME280 import SI1132 # ── MQTT settings ──────────────────────────────────────────────────────────── MQTT_HOST = "192.168.1.135" MQTT_PORT = 1883 MQTT_USER = "co2meter" MQTT_PASS = "co2meter" # ── I2C bus ────────────────────────────────────────────────────────────────── I2C = "/dev/i2c-1" # ── Home Assistant discovery identifiers ───────────────────────────────────── DEVICE_ID = "weather_board_office" DEVICE_NAME = "Weather Board" ROOM = "Office" HA_DISCOVERY_PREFIX = "homeassistant" STATE_TOPIC = f"{HA_DISCOVERY_PREFIX}/sensor/{DEVICE_ID}/state" # How often to read & publish (seconds) PUBLISH_INTERVAL = 10 # ── Sensor definitions for HA auto-discovery ───────────────────────────────── # Each entry maps to one Home Assistant sensor entity. SENSORS = [ { "key": "temperature", "name": "Temperature", "device_class": "temperature", "unit": "°C", "icon": None, "state_class": "measurement", }, { "key": "humidity", "name": "Humidity", "device_class": "humidity", "unit": "%", "icon": None, "state_class": "measurement", }, { "key": "pressure", "name": "Pressure", "device_class": "atmospheric_pressure", "unit": "hPa", "icon": None, "state_class": "measurement", }, { "key": "uv_index", "name": "UV Index", "device_class": None, "unit": "UV index", "icon": "mdi:sun-wireless", "state_class": "measurement", }, { "key": "visible_light", "name": "Visible Light", "device_class": "illuminance", "unit": "lx", "icon": None, "state_class": "measurement", }, { "key": "ir", "name": "Infrared", "device_class": None, "unit": "lx", "icon": "mdi:sun-wireless-outline", "state_class": "measurement", }, ] # ── Shared HA device block (appears once per entity, links them together) ──── HA_DEVICE = { "identifiers": [DEVICE_ID], "name": DEVICE_NAME, "manufacturer": "ODROID", "model": "Weather Board", "suggested_area": ROOM, } def publish_discovery(client: mqtt.Client) -> None: """Publish MQTT discovery config messages for every sensor entity.""" for sensor in SENSORS: uid = f"{DEVICE_ID}_{sensor['key']}" config_topic = ( f"{HA_DISCOVERY_PREFIX}/sensor/{DEVICE_ID}/{sensor['key']}/config" ) payload = { "name": sensor["name"], "unique_id": uid, "object_id": uid, "state_topic": STATE_TOPIC, "value_template": "{{ value_json." + sensor["key"] + " }}", "unit_of_measurement": sensor["unit"], "state_class": sensor["state_class"], "device": HA_DEVICE, } if sensor["device_class"]: payload["device_class"] = sensor["device_class"] if sensor["icon"]: payload["icon"] = sensor["icon"] client.publish(config_topic, json.dumps(payload), retain=True) print(f" discovery: {config_topic}") def read_sensors(bme: BME280.BME280, si: SI1132.SI1132) -> dict: """Read all sensors and return a dict ready for JSON serialisation.""" # Temperature must be read first — it sets t_fine used by humidity/pressure temperature = bme.read_temperature() humidity = bme.read_humidity() pressure = bme.read_pressure() / 100.0 # Pa → hPa uv_index = si.readUV() / 100.0 visible_light = si.readVisible() ir = si.readIR() return { "temperature": round(temperature, 2), "humidity": round(humidity, 2), "pressure": round(pressure, 2), "uv_index": round(uv_index, 2), "visible_light": round(visible_light, 1), "ir": round(ir, 1), } def main() -> None: # ── Initialise I2C sensors ─────────────────────────────────────────── print("Initialising sensors...") si = SI1132.SI1132(I2C) # Normal mode, 2× oversampling for humidity, pressure, temperature bme = BME280.BME280(I2C, 0x03, 0x02, 0x02, 0x02) print(" BME280 + SI1132 ready") # ── Connect to MQTT broker ─────────────────────────────────────────── client = mqtt.Client(client_id=DEVICE_ID) client.username_pw_set(MQTT_USER, MQTT_PASS) def on_connect(cl, _userdata, _flags, rc): if rc == 0: print(f" connected to MQTT broker at {MQTT_HOST}:{MQTT_PORT}") publish_discovery(cl) else: print(f" MQTT connection failed (rc={rc})") client.on_connect = on_connect client.will_set(STATE_TOPIC, payload="", retain=False) print("Connecting to MQTT...") client.connect(MQTT_HOST, MQTT_PORT, keepalive=60) client.loop_start() # ── Graceful shutdown ──────────────────────────────────────────────── running = True def _stop(signum, _frame): nonlocal running print(f"\nCaught signal {signum}, shutting down...") running = False signal.signal(signal.SIGINT, _stop) signal.signal(signal.SIGTERM, _stop) # ── Main loop ──────────────────────────────────────────────────────── print("Publishing sensor data...\n") while running: try: data = read_sensors(bme, si) payload = json.dumps(data) client.publish(STATE_TOPIC, payload, retain=False) print(f" {payload}") except Exception as e: print(f" sensor read error: {e}") # Sleep in small increments so we can react to signals promptly for _ in range(PUBLISH_INTERVAL * 10): if not running: break time.sleep(0.1) # ── Cleanup ────────────────────────────────────────────────────────── client.loop_stop() client.disconnect() print("Done.") if __name__ == "__main__": main()