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