]> mj.ucw.cz Git - home-hw.git/blob - auto/burrow-auto
Auto: Switch to Python 3, add air control
[home-hw.git] / auto / burrow-auto
1 #!/usr/bin/python3
2
3 import getopt
4 import paho.mqtt.client as mqtt
5 import sys
6 import time
7
8 debug_mode = False
9 indent = 0
10
11 def debug(msg):
12     if debug_mode:
13         print(("\t" * indent) + msg)
14
15 class State:
16     def __init__(self):
17             self.attrs = {}
18
19     def update(self):
20         self.now = time.time()
21         tm = time.localtime(self.now)
22         self.year = tm.tm_year
23         self.month = tm.tm_mon
24         self.day = tm.tm_mday
25         self.hour = tm.tm_hour
26         self.min = tm.tm_min
27         self.wday = tm.tm_wday
28         self.hyst_state = {}
29         debug("[{}-{:02d}-{:02d} {:02d}:{:02d}:{:02d} wday={}]".format(
30             self.year, self.month, self.day,
31             self.hour, self.min, tm.tm_sec,
32             self.wday
33             ))
34
35     def get_sensor(self, key):
36         topic = "burrow/" + key
37         if topic in self.attrs:
38             s = self.attrs[topic].split(" ")
39             if len(s) >= 2 and int(s[1]) < self.now - 120:
40                 debug("< {} EXPIRED".format(key))
41                 return None
42             else:
43                 debug("< {} = {}".format(key, s[0]))
44                 return float(s[0])
45         else:
46             debug("< {} UNDEFINED".format(key))
47             return None
48
49     def set(self, key, val):
50         global mq
51         topic = "burrow/" + key
52         debug("> {} = {}".format(topic, val))
53         mq.publish(topic, val, qos=1, retain=True)
54
55     def auto_enabled(self, key):
56         topic = "burrow/auto/" + key
57         if topic in self.attrs:
58             return self.attrs[topic] != '0'
59         else:
60             return True
61
62     def hysteresis(self, key, value, low, high):
63         if key in self.hyst_state:
64             old_state = self.hyst_state[key]
65         else:
66             old_state = 0
67         if value is None:
68             new_state = 0
69         elif old_state <= 0:
70             if value >= high:
71                 new_state = 1
72             else:
73                 new_state = -1
74         else:
75             if value <= low:
76                 new_state = -1
77             else:
78                 new_state = 1
79         self.hyst_state[key] = new_state
80         return new_state
81
82 st = State()
83
84 def on_connect(mq, userdata, flags, rc):
85     mq.subscribe("burrow/#")
86
87 def on_message(mq, userdata, msg):
88     global st
89     # debug("Message {}: {}".format(msg.topic, msg.payload))
90     st.attrs[msg.topic] = msg.payload.decode('utf-8')
91
92 def auto_loft_fan():
93     global st
94     lt = st.get_sensor("temp/loft")
95     lt_high = st.hysteresis('lt_high', lt, 29, 30)
96     lt_mid = st.hysteresis('lt_mid', lt, 24, 25)
97     if lt_high > 0:
98             fs = 3
99     elif lt_mid > 0:
100         if st.hour in range(10, 20):
101                 fs = 3
102         else:
103             fs = 1
104     else:
105         if st.hour in range(8, 22):
106             if st.min % 30 in range(0, 5):
107                 fs = 3
108             else:
109                 fs = 0
110         else:
111             fs = 0
112     # FIXME: Disabled for now
113     fs = 0
114     st.set("loft/fan", fs)
115
116 def auto_circ():
117     global st
118     if st.hour in range(20, 22):
119         c = 1
120     else:
121         c = 0;
122     st.set("loft/circulation", c)
123
124 def auto_air():
125     global st
126     tii = st.get_sensor('air/inside-intake')
127     tie = st.get_sensor('air/inside-exhaust')
128     toi = st.get_sensor('air/outside-intake')
129     tmix = st.get_sensor('air/mixed')
130     house_warm = st.hysteresis('house_warm', tii, 23.5, 24.5)
131     house_hot = st.hysteresis('house_hot', tii, 24.5, 25)
132
133     # Is AC currently on (mixed air is significantly colder than inside exhaust)?
134     if tie is None or tmix is None:
135             ac_off = 1
136     else:
137         ac_off = st.hysteresis('ac_off', tmix, tie - 5, tie - 4)
138
139     # Do we want to bypass the heat exchanger?
140     if toi is None or tie is None:
141         st.set('air/bypass', 0)
142     else:
143         outside_warmer = (st.hysteresis('bypass', toi, tii - 0.5, tii + 0.5) >= 0)
144         if (house_warm >= 0) == outside_warmer:
145             st.set('air/bypass', 0)
146         else:
147             st.set('air/bypass', 1)
148
149     # Is mixed air colder than air from the inside?
150     if tii is None or tmix is None:
151         mixed_warmer = 0
152     else:
153         mixed_warmer = st.hysteresis('mixed_warmer', tmix, tii - 1, tii)
154
155     # Do we want to boost heat exchanger fan?
156     if ac_off < 0 or (house_hot > 0 and mixed_warmer < 0):
157         st.set('air/exchanger-fan', 255)
158     else:
159         st.set('air/exchanger-fan', 0)
160
161     debug("Air: house_warm={} house_hot={} ac_off={} outside_warmer={} mixed_warmer={}".format(house_warm, house_hot, ac_off, outside_warmer, mixed_warmer))
162
163 opts, args = getopt.gnu_getopt(sys.argv[1:], "", ["debug"])
164 for opt in opts:
165     o, arg = opt
166     if o == "--debug":
167         debug_mode = True
168
169 mq = mqtt.Client()
170 mq.on_connect = on_connect
171 mq.on_message = on_message
172 mq.will_set("status/auto", "dead", retain=True)
173 mq.connect("127.0.0.1")
174 mq.publish("status/auto", "ok", retain=True)
175 mq.loop_start()
176
177 # Heuristic delay to get all attributes from MQTT
178 time.sleep(3)
179
180 checks = [
181     ('loft-fan', auto_loft_fan),
182     ('circ', auto_circ),
183     ('air', auto_air)
184 ]
185
186 while True:
187     st.update()
188     for name, func in checks:
189         if st.auto_enabled(name):
190             debug(name)
191             indent += 1
192             func()
193             indent -= 1
194         else:
195             debug("{} DISABLED".format(name))
196     debug("=" * 80)
197     time.sleep(10)