4 import paho.mqtt.client as mqtt
13 print(("\t" * indent) + msg)
16 if x is None or y is None:
25 self.running_averages = {}
28 self.now = time.time()
29 tm = time.localtime(self.now)
30 self.year = tm.tm_year
31 self.month = tm.tm_mon
33 self.hour = tm.tm_hour
35 self.wday = tm.tm_wday
36 debug("[{}-{:02d}-{:02d} {:02d}:{:02d}:{:02d} wday={}]".format(
37 self.year, self.month, self.day,
38 self.hour, self.min, tm.tm_sec,
42 def get_sensor(self, key):
43 topic = "burrow/" + key
44 if topic in self.attrs:
45 s = self.attrs[topic].split(" ")
46 if len(s) >= 2 and int(s[1]) < self.now - 120:
47 debug("< {} EXPIRED".format(key))
50 debug("< {} = {}".format(key, s[0]))
53 debug("< {} UNDEFINED".format(key))
56 def set(self, key, val):
58 topic = "burrow/" + key
59 debug("> {} = {}".format(topic, val))
60 mq.publish(topic, val, qos=1, retain=True)
62 def auto_enabled(self, key):
63 topic = "burrow/auto/" + key
64 if topic in self.attrs:
65 return self.attrs[topic] != '0'
69 def hysteresis(self, key, value, low, high):
70 if key in self.hyst_state:
71 old_state = self.hyst_state[key]
86 self.hyst_state[key] = new_state
89 def update_average(self, key, window_seconds):
90 if key not in self.running_averages:
91 self.running_averages[key] = ([], 0, 0, None)
92 (history, sum, count, avg) = self.running_averages[key]
94 while len(history) > 0 and history[0][0] <= self.now - window_seconds:
95 if history[0][1] is not None:
100 curr = self.get_sensor(key)
101 history.append((self.now, curr))
106 if count > len(history) // 2:
111 self.running_averages[key] = (history, sum, count, avg)
113 debug("= avg NONE ({} samples, {} non-null)".format(len(history), count))
115 debug("= avg {:.6} ({} samples, {} non-null)".format(avg, len(history), count))
116 self.set("avg/" + key, "{:.6} {}".format(avg, int(self.now)))
118 def get_sensor_avg(self, key):
119 val = self.running_averages[key][3]
121 debug("< {} = avg NONE".format(key))
123 debug("< {} = avg {:.6}".format(key, val))
128 def on_connect(mq, userdata, flags, rc):
129 mq.subscribe("burrow/#")
131 def on_message(mq, userdata, msg):
133 # debug("Message {}: {}".format(msg.topic, msg.payload))
134 st.attrs[msg.topic] = msg.payload.decode('utf-8')
138 lt = st.get_sensor("temp/loft")
139 lt_high = st.hysteresis('lt_high', lt, 29, 30)
140 lt_mid = st.hysteresis('lt_mid', lt, 24, 25)
144 if st.hour in range(10, 20):
149 if st.hour in range(8, 22):
150 if st.min % 30 in range(0, 5):
156 # FIXME: Disabled for now
158 st.set("loft/fan", fs)
162 if st.hour in range(7, 24):
166 st.set("loft/circulation", c)
170 tii = st.get_sensor_avg('air/inside-intake')
171 tie = st.get_sensor_avg('air/inside-exhaust')
172 toi = st.get_sensor_avg('air/outside-intake')
173 tmix = st.get_sensor_avg('air/mixed')
174 house_warm = st.hysteresis('house_warm', tii, 23.5, 24.5)
175 house_hot = st.hysteresis('house_hot', tii, 24.5, 25)
177 # Is AC currently on (mixed air is significantly colder than inside exhaust)?
178 if tie is None or tmix is None:
181 ac_off = st.hysteresis('ac_off', tmix, tie - 5, tie - 4)
183 # XXX: Temporarily disabled
188 # Do we want to bypass the heat exchanger?
189 outside_warmer = st.hysteresis('outside_warmer', diff(toi, tii), -0.5, 0.5)
190 if (house_warm > 0) and (outside_warmer > 0) or \
191 (house_warm < 0) and (outside_warmer < 0):
192 st.set('air/bypass', 0)
194 st.set('air/bypass', 1)
196 # Is mixed air colder than air from the inside?
197 mixed_warmer = st.hysteresis('mixed_warmer', diff(tmix, tii), -1, 0)
199 # Do we want to boost heat exchanger fan?
200 if ac_off < 0 or (house_hot > 0 and mixed_warmer < 0):
201 st.set('air/exchanger-fan', 255)
203 st.set('air/exchanger-fan', 0)
205 debug("Air: house_warm={} house_hot={} ac_off={} outside_warmer={} mixed_warmer={}".format(house_warm, house_hot, ac_off, outside_warmer, mixed_warmer))
207 opts, args = getopt.gnu_getopt(sys.argv[1:], "", ["debug"])
214 mq.on_connect = on_connect
215 mq.on_message = on_message
216 mq.will_set("status/auto", "dead", retain=True)
217 mq.connect("burrow-mqtt")
218 mq.publish("status/auto", "ok", retain=True)
221 # Heuristic delay to get all attributes from MQTT
225 ('loft-fan', auto_loft_fan),
235 st.update_average('air/outside-intake', 60)
236 st.update_average('air/inside-intake', 60)
237 st.update_average('air/inside-exhaust', 60)
238 st.update_average('air/mixed', 60)
241 for name, func in checks:
242 if st.auto_enabled(name):
248 debug("{} DISABLED".format(name))