diff options
| author | Yuval Adam <_@yuv.al> | 2026-03-17 11:53:22 +0100 |
|---|---|---|
| committer | Yuval Adam <_@yuv.al> | 2026-03-17 11:53:22 +0100 |
| commit | e97d3ae12b04745e785311d9125261557ac21fd3 (patch) | |
| tree | 7ca16a8695d78266efd1f039067936e5907507a7 | |
| parent | 3a3d6ed1adc009cc6efee8e0b416137a8d17bb89 (diff) | |
Add MQTT Home Assistant script with auto-discovery
Reads BME280 (temperature, humidity, pressure) and SI1132 (UV index,
visible light, IR) and publishes to MQTT with HA discovery. All six
sensors appear under a single 'Weather Board' device in the Office room.
MQTT broker: 192.168.1.135:1883 (co2meter/co2meter)
Publishes a shared JSON state topic every 10 seconds.
Discovery configs are retained so HA picks them up on restart.
| -rw-r--r-- | mqtt_ha.py | 213 |
1 files changed, 213 insertions, 0 deletions
diff --git a/mqtt_ha.py b/mqtt_ha.py new file mode 100644 index 0000000..5fa9197 --- /dev/null +++ b/mqtt_ha.py @@ -0,0 +1,213 @@ +#!/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() |
