]> mj.ucw.cz Git - home-hw.git/blob - rainbow/iris/burrow-iris.py
Iris: Init
[home-hw.git] / rainbow / iris / burrow-iris.py
1 #!/usr/bin/env python
2 # Iris -- the Burrow's goddess of rainbow
3 # Controls LEDs on the Rainbow according to the state of the house
4 # (c) 2022 Martin Mareš <mj@ucw.cz>
5
6 import argparse
7 import asyncio
8 import asyncio_mqtt
9 from datetime import datetime, timedelta
10 import logging
11 from logging.handlers import SysLogHandler
12 import signal
13 import ssl
14 import sys
15
16
17 class State:
18     def __init__(self, mqtt):
19         self.attrs = {}
20         self.leds = ["?"] * 12
21         self.new_leds = ["?"] * 12
22         self.mqtt = mqtt
23
24     def set_now(self):
25         self.now = datetime.now()
26
27     def received_msg(self, topic, val):
28         self.attrs[topic] = val
29         logger.debug(f'MQTT: {topic} -> {val}')
30
31     def get(self, key, default=None):
32         topic = "burrow/" + key
33         return self.attrs.get(topic, default)
34
35     def get_status(self, key, default=None):
36         topic = "status/" + key
37         return self.attrs.get(topic, default)
38
39     def get_sensor(self, key, default=None, timeout=120):
40         topic = "burrow/" + key
41         if topic in self.attrs:
42             s = self.attrs[topic].split(" ")
43             if len(s) >= 2 and timeout is not None and int(s[1]) < self.now.timestamp() - timeout:
44                 logger.debug(f"< {key} EXPIRED")
45                 return default
46             else:
47                 logger.debug(f"< {key} = {s[0]}")
48                 return float(s[0])
49         else:
50             logger.debug(f"< {key} UNDEFINED")
51             return default
52
53     async def set(self, key, val):
54         global mq
55         topic = "burrow/" + key
56         logger.debug(f'> {key} = {val}')
57         await self.mqtt.publish(topic, val, retain=True)
58
59     def set_led(self, i, color=None):
60         if color is None:
61             self.new_leds[i] = ""
62         else:
63             r, g, b = color
64             self.new_leds[i] = f"{r} {g} {b} iris"
65
66     async def update_leds(self):
67         for i in range(len(self.leds)):
68             if self.new_leds[i] != self.leds[i]:
69                 await self.set(f"lights/rainbow/{i}", self.new_leds[i])
70                 self.leds[i] = self.new_leds[i]
71
72
73 st = None       # Current State
74 led_event = None
75
76
77 def boiler_led():
78     stat = st.get_status('bsb', 'ok')
79     if stat != 'ok':
80         return (1, 1, 0)
81
82     err = st.get_sensor('heating/error', 0, timeout=None)
83     if err > 0:
84         return (1, 0, 0)
85
86     if st.get_sensor('heating/circuit1/pump', 0, timeout=3600) > 0:
87         return (0.2, 0, 0.2)
88
89     if st.get_sensor('heating/circuit2/active', 0, timeout=3600) > 0:
90         return (0, 0.3, 0)
91
92     if st.get_sensor('heating/water/active', 0, timeout=3600) > 0:
93         return (0, 0, 0.3)
94
95     return None
96
97
98 def catarium_led():
99     temp = st.get_sensor('temp/catarium')
100     if temp is None:
101         return (1, 0, 0)
102
103     if temp < 22:
104         return (0, 0, 1)
105
106     if temp < 23:
107         return (0, 0.5, 0.5)
108
109     if temp > 26:
110         return (0.1, 0.1, 0)
111
112     return None
113
114
115 def temperature_led():
116     for sensor in ['loft', 'ursarium', 'garage', 'terarium']:
117         if st.get_sensor(f"temp/{sensor}", timeout=7200) is None:
118             return (1, 0, 0)
119
120     return None
121
122
123 def recalc_leds():
124     st.set_led(11, None)
125     st.set_led(10, boiler_led())
126     st.set_led(9, catarium_led())
127     st.set_led(8, temperature_led())
128
129
130 async def mqtt_process_msg(topic, val):
131     st.received_msg(topic, val)
132     led_event.set()
133
134
135 async def mqtt_loop():
136     sctx = ssl.SSLContext(ssl.PROTOCOL_TLS)
137     sctx.verify_mode = ssl.CERT_REQUIRED
138     sctx.load_cert_chain('/etc/burrow-mqtt/client.crt', '/etc/burrow-mqtt/client.key')
139     sctx.load_verify_locations(cafile='/etc/burrow-mqtt/ca.crt')
140
141     will = asyncio_mqtt.Will(topic='status/iris', payload='dead', qos=1, retain=True)
142
143     async with asyncio_mqtt.Client(client_id='iris', hostname="burrow-mqtt", port=8883, tls_context=sctx, will=will) as mqtt:
144         global st
145         st = State(mqtt)
146         async with mqtt.unfiltered_messages() as messages:
147             await mqtt.subscribe("burrow/heating/#")
148             await mqtt.subscribe("burrow/temp/#")
149             await mqtt.publish("status/iris", "ok", retain=True)
150             async for msg in messages:
151                 await mqtt_process_msg(msg.topic, msg.payload.decode())
152
153
154 async def mqtt_watcher():
155     while True:
156         try:
157             logger.info("Starting MQTT")
158             await mqtt_loop()
159         except asyncio_mqtt.MqttError as error:
160             logger.error(f"MQTT error: {error}")
161         await asyncio.sleep(10)
162
163
164 async def led_watcher():
165     while True:
166         await led_event.wait()
167         led_event.clear()
168         logger.debug('Recalculating LEDs')
169         if st is not None:
170             st.set_now()
171             recalc_leds()
172             await st.update_leds()
173             await asyncio.sleep(0.1)
174
175
176 async def main():
177     global loop, led_event
178     loop = asyncio.get_event_loop()
179     led_event = asyncio.Event()
180     coros = [
181         loop.create_task(mqtt_watcher()),
182         loop.create_task(led_watcher()),
183     ]
184     for coro in asyncio.as_completed(coros):
185         done = await coro
186         done.result()       # The coroutine probably died of an exception, which is raised here.
187
188
189 parser = argparse.ArgumentParser(description='The Goddess of Rainbow in the Burrow')
190 parser.add_argument('--debug', default=False, action='store_true', help='Run in debug mode')
191 args = parser.parse_args()
192
193 logger = logging.getLogger()
194 if args.debug:
195     formatter = logging.Formatter(fmt="%(asctime)s %(name)s.%(levelname)s: %(message)s", datefmt='%Y-%m-%d %H:%M:%S')
196     log_handler = logging.StreamHandler(stream=sys.stdout)
197     logger.setLevel(logging.DEBUG)
198     logging.getLogger('mqtt').setLevel(logging.DEBUG)
199 else:
200     formatter = logging.Formatter(fmt="%(message)s")        # systemd will handle the rest
201     log_handler = SysLogHandler('/dev/log', facility=SysLogHandler.LOG_LOCAL1)
202     log_handler.ident = 'burrow-telegram: '
203     logger.setLevel(logging.INFO)
204 log_handler.setFormatter(formatter)
205 logger.addHandler(log_handler)
206
207
208 asyncio.run(main())