From: Martin Mares Date: Sun, 27 Feb 2022 15:38:54 +0000 (+0100) Subject: Iris: Init X-Git-Url: http://mj.ucw.cz/gitweb/?a=commitdiff_plain;h=86a49433fa2a105e36875c4fd9815dbd7da83fae;p=home-hw.git Iris: Init --- diff --git a/MQTT b/MQTT index ca4a19a..96f1af8 100644 --- a/MQTT +++ b/MQTT @@ -65,6 +65,7 @@ status/bifrost ok/dead status/bsb ok/dead status/clock ok/dead status/influx ok/dead +status/iris ok/dead status/loft-ssr ok/dead status/power-meter ok/dead status/prometheus ok/dead # Obsolete diff --git a/rainbow/LEDS b/rainbow/LEDS new file mode 100644 index 0000000..c46393b --- /dev/null +++ b/rainbow/LEDS @@ -0,0 +1,18 @@ +LEDs on the Rainbow +=================== + +0 bifrost general urgent window +1 bifrost Chrome urgent +2 bifrost Telegram urgent +3 +4 +5 +6 +7 +8 iris general temeperature alarm +9 iris catarium temperature +10 iris boiler status +11 rainbowd debugging + + +LED 0 is the one closest to the cable. diff --git a/rainbow/iris/Makefile b/rainbow/iris/Makefile new file mode 100644 index 0000000..09023c1 --- /dev/null +++ b/rainbow/iris/Makefile @@ -0,0 +1,10 @@ +VENV=/usr/local/lib/burrow-venv + +all: + +install: + [ -d $(VENV) ] || su -c "python3 -m venv $(VENV)" + su -c ". $(VENV)/bin/activate && pip install -r requirements.txt" + su -c "install -m 755 burrow-iris.py $(VENV)/bin/burrow-iris" + +.PHONY: all install diff --git a/rainbow/iris/burrow-iris.py b/rainbow/iris/burrow-iris.py new file mode 100755 index 0000000..4724803 --- /dev/null +++ b/rainbow/iris/burrow-iris.py @@ -0,0 +1,208 @@ +#!/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š + +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()) diff --git a/rainbow/iris/requirements.txt b/rainbow/iris/requirements.txt new file mode 100644 index 0000000..f6f80c2 --- /dev/null +++ b/rainbow/iris/requirements.txt @@ -0,0 +1 @@ +asyncio-mqtt