summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuval Adam <_@yuv.al>2026-03-17 11:53:22 +0100
committerYuval Adam <_@yuv.al>2026-03-17 11:53:22 +0100
commite97d3ae12b04745e785311d9125261557ac21fd3 (patch)
tree7ca16a8695d78266efd1f039067936e5907507a7
parent3a3d6ed1adc009cc6efee8e0b416137a8d17bb89 (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.py213
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()