]> mj.ucw.cz Git - home-hw.git/commitdiff
Iris: Init
authorMartin Mares <mj@ucw.cz>
Sun, 27 Feb 2022 15:38:54 +0000 (16:38 +0100)
committerMartin Mares <mj@ucw.cz>
Sun, 27 Feb 2022 15:38:54 +0000 (16:38 +0100)
MQTT
rainbow/LEDS [new file with mode: 0644]
rainbow/iris/Makefile [new file with mode: 0644]
rainbow/iris/burrow-iris.py [new file with mode: 0755]
rainbow/iris/requirements.txt [new file with mode: 0644]

diff --git a/MQTT b/MQTT
index ca4a19a626cc7296c344a94ac853510d8d9efc29..96f1af87bd8e3ff50996ba2d97d892a99ba37b81 100644 (file)
--- 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 (file)
index 0000000..c46393b
--- /dev/null
@@ -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 (file)
index 0000000..09023c1
--- /dev/null
@@ -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 (executable)
index 0000000..4724803
--- /dev/null
@@ -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š <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())
diff --git a/rainbow/iris/requirements.txt b/rainbow/iris/requirements.txt
new file mode 100644 (file)
index 0000000..f6f80c2
--- /dev/null
@@ -0,0 +1 @@
+asyncio-mqtt