--- /dev/null
+#!/usr/bin/env python
+# Iris -- the Burrow's goddess of rainbow
+# Controls LEDs on the Rainbow according to the state of the house
+# (c) 2022 Martin Mareš <mj@ucw.cz>
+
+import argparse
+import asyncio
+import asyncio_mqtt
+from datetime import datetime, timedelta
+import logging
+from logging.handlers import SysLogHandler
+import signal
+import ssl
+import sys
+
+
+class State:
+ def __init__(self, mqtt):
+ self.attrs = {}
+ self.leds = ["?"] * 12
+ self.new_leds = ["?"] * 12
+ self.mqtt = mqtt
+
+ def set_now(self):
+ self.now = datetime.now()
+
+ def received_msg(self, topic, val):
+ self.attrs[topic] = val
+ logger.debug(f'MQTT: {topic} -> {val}')
+
+ def get(self, key, default=None):
+ topic = "burrow/" + key
+ return self.attrs.get(topic, default)
+
+ def get_status(self, key, default=None):
+ topic = "status/" + key
+ return self.attrs.get(topic, default)
+
+ def get_sensor(self, key, default=None, timeout=120):
+ topic = "burrow/" + key
+ if topic in self.attrs:
+ s = self.attrs[topic].split(" ")
+ if len(s) >= 2 and timeout is not None and int(s[1]) < self.now.timestamp() - timeout:
+ logger.debug(f"< {key} EXPIRED")
+ return default
+ else:
+ logger.debug(f"< {key} = {s[0]}")
+ return float(s[0])
+ else:
+ logger.debug(f"< {key} UNDEFINED")
+ return default
+
+ async def set(self, key, val):
+ global mq
+ topic = "burrow/" + key
+ logger.debug(f'> {key} = {val}')
+ await self.mqtt.publish(topic, val, retain=True)
+
+ def set_led(self, i, color=None):
+ if color is None:
+ self.new_leds[i] = ""
+ else:
+ r, g, b = color
+ self.new_leds[i] = f"{r} {g} {b} iris"
+
+ async def update_leds(self):
+ for i in range(len(self.leds)):
+ if self.new_leds[i] != self.leds[i]:
+ await self.set(f"lights/rainbow/{i}", self.new_leds[i])
+ self.leds[i] = self.new_leds[i]
+
+
+st = None # Current State
+led_event = None
+
+
+def boiler_led():
+ stat = st.get_status('bsb', 'ok')
+ if stat != 'ok':
+ return (1, 1, 0)
+
+ err = st.get_sensor('heating/error', 0, timeout=None)
+ if err > 0:
+ return (1, 0, 0)
+
+ if st.get_sensor('heating/circuit1/pump', 0, timeout=3600) > 0:
+ return (0.2, 0, 0.2)
+
+ if st.get_sensor('heating/circuit2/active', 0, timeout=3600) > 0:
+ return (0, 0.3, 0)
+
+ if st.get_sensor('heating/water/active', 0, timeout=3600) > 0:
+ return (0, 0, 0.3)
+
+ return None
+
+
+def catarium_led():
+ temp = st.get_sensor('temp/catarium')
+ if temp is None:
+ return (1, 0, 0)
+
+ if temp < 22:
+ return (0, 0, 1)
+
+ if temp < 23:
+ return (0, 0.5, 0.5)
+
+ if temp > 26:
+ return (0.1, 0.1, 0)
+
+ return None
+
+
+def temperature_led():
+ for sensor in ['loft', 'ursarium', 'garage', 'terarium']:
+ if st.get_sensor(f"temp/{sensor}", timeout=7200) is None:
+ return (1, 0, 0)
+
+ return None
+
+
+def recalc_leds():
+ st.set_led(11, None)
+ st.set_led(10, boiler_led())
+ st.set_led(9, catarium_led())
+ st.set_led(8, temperature_led())
+
+
+async def mqtt_process_msg(topic, val):
+ st.received_msg(topic, val)
+ led_event.set()
+
+
+async def mqtt_loop():
+ sctx = ssl.SSLContext(ssl.PROTOCOL_TLS)
+ sctx.verify_mode = ssl.CERT_REQUIRED
+ sctx.load_cert_chain('/etc/burrow-mqtt/client.crt', '/etc/burrow-mqtt/client.key')
+ sctx.load_verify_locations(cafile='/etc/burrow-mqtt/ca.crt')
+
+ will = asyncio_mqtt.Will(topic='status/iris', payload='dead', qos=1, retain=True)
+
+ async with asyncio_mqtt.Client(client_id='iris', hostname="burrow-mqtt", port=8883, tls_context=sctx, will=will) as mqtt:
+ global st
+ st = State(mqtt)
+ async with mqtt.unfiltered_messages() as messages:
+ await mqtt.subscribe("burrow/heating/#")
+ await mqtt.subscribe("burrow/temp/#")
+ await mqtt.publish("status/iris", "ok", retain=True)
+ async for msg in messages:
+ await mqtt_process_msg(msg.topic, msg.payload.decode())
+
+
+async def mqtt_watcher():
+ while True:
+ try:
+ logger.info("Starting MQTT")
+ await mqtt_loop()
+ except asyncio_mqtt.MqttError as error:
+ logger.error(f"MQTT error: {error}")
+ await asyncio.sleep(10)
+
+
+async def led_watcher():
+ while True:
+ await led_event.wait()
+ led_event.clear()
+ logger.debug('Recalculating LEDs')
+ if st is not None:
+ st.set_now()
+ recalc_leds()
+ await st.update_leds()
+ await asyncio.sleep(0.1)
+
+
+async def main():
+ global loop, led_event
+ loop = asyncio.get_event_loop()
+ led_event = asyncio.Event()
+ coros = [
+ loop.create_task(mqtt_watcher()),
+ loop.create_task(led_watcher()),
+ ]
+ for coro in asyncio.as_completed(coros):
+ done = await coro
+ done.result() # The coroutine probably died of an exception, which is raised here.
+
+
+parser = argparse.ArgumentParser(description='The Goddess of Rainbow in the Burrow')
+parser.add_argument('--debug', default=False, action='store_true', help='Run in debug mode')
+args = parser.parse_args()
+
+logger = logging.getLogger()
+if args.debug:
+ formatter = logging.Formatter(fmt="%(asctime)s %(name)s.%(levelname)s: %(message)s", datefmt='%Y-%m-%d %H:%M:%S')
+ log_handler = logging.StreamHandler(stream=sys.stdout)
+ logger.setLevel(logging.DEBUG)
+ logging.getLogger('mqtt').setLevel(logging.DEBUG)
+else:
+ formatter = logging.Formatter(fmt="%(message)s") # systemd will handle the rest
+ log_handler = SysLogHandler('/dev/log', facility=SysLogHandler.LOG_LOCAL1)
+ log_handler.ident = 'burrow-telegram: '
+ logger.setLevel(logging.INFO)
+log_handler.setFormatter(formatter)
+logger.addHandler(log_handler)
+
+
+asyncio.run(main())