X-Git-Url: http://mj.ucw.cz/gitweb/?a=blobdiff_plain;f=auto%2Fburrow-auto;h=7aaca0f74da5a77011c14007872a5987e2d2ae47;hb=b5b6030611f2bd9adbe83a3e73d14ebe31c09fe2;hp=2adea5ae1bd0b2dca1e75cf3c85db390a3709ee4;hpb=24e79ec7dbe46b9a967154d59c25d6b75b9e276a;p=home-hw.git diff --git a/auto/burrow-auto b/auto/burrow-auto index 2adea5a..7aaca0f 100755 --- a/auto/burrow-auto +++ b/auto/burrow-auto @@ -1,16 +1,36 @@ #!/usr/bin/python3 +from collections import deque import getopt +import os import paho.mqtt.client as mqtt import sys import time debug_mode = False +log_name = '/run/burrow-auto' +log_file = None indent = 0 def debug(msg): if debug_mode: - print(("\t" * indent) + msg) + print(("\t" * indent) + msg, file=sys.stderr) + elif log_file is not None: + print(("\t" * indent) + msg, file=log_file) + +def debug_open(): + if not debug_mode: + global log_file + log_file = open(log_name + '.new', 'w') + +def debug_close(): + if debug_mode: + debug("=" * 80) + else: + global log_file + log_file.close() + log_file = None + os.rename(log_name + '.new', log_name) def diff(x, y): if x is None or y is None: @@ -59,6 +79,12 @@ class State: debug("> {} = {}".format(topic, val)) mq.publish(topic, val, qos=1, retain=True) + def send(self, key, val): + global mq + topic = "burrow/" + key + debug("> {} := {}".format(topic, val)) + mq.publish(topic, val, qos=1, retain=False) + def auto_enabled(self, key): topic = "burrow/auto/" + key if topic in self.attrs: @@ -67,10 +93,7 @@ class State: return True def hysteresis(self, key, value, low, high): - if key in self.hyst_state: - old_state = self.hyst_state[key] - else: - old_state = 0 + old_state = self.hyst_state.get(key, 0) if value is None: new_state = 0 elif old_state <= 0: @@ -88,14 +111,14 @@ class State: def update_average(self, key, window_seconds): if key not in self.running_averages: - self.running_averages[key] = ([], 0, 0, None) + self.running_averages[key] = (deque(), 0, 0, None) (history, sum, count, avg) = self.running_averages[key] while len(history) > 0 and history[0][0] <= self.now - window_seconds: if history[0][1] is not None: sum -= history[0][1] count -= 1 - history.pop(0) + history.popleft() curr = self.get_sensor(key) history.append((self.now, curr)) @@ -125,38 +148,37 @@ class State: st = State() -def on_connect(mq, userdata, flags, rc): - mq.subscribe("burrow/#") - -def on_message(mq, userdata, msg): - global st - # debug("Message {}: {}".format(msg.topic, msg.payload)) - st.attrs[msg.topic] = msg.payload.decode('utf-8') def auto_loft_fan(): global st - lt = st.get_sensor("temp/loft") - lt_high = st.hysteresis('lt_high', lt, 29, 30) - lt_mid = st.hysteresis('lt_mid', lt, 24, 25) - if lt_high > 0: + lt = st.get_sensor_avg("temp/loft") + out = st.get_sensor_avg('air/outside-intake') + + if lt is None or out is None: + fs = 0 + elif st.hysteresis('lf_out_cold', out, 5, 6) < 0: + fs = 0 + elif st.hysteresis('lf_out_cool', out, 14, 15) < 0: + if st.min in range(10, 15): + fs = 2 + else: + fs = 0 + elif ac_is_on() > 0: + fs = 3 + elif st.hysteresis('lf_loft_hot', lt, 25, 26) > 0: + if st.hysteresis('lf_loft_hotter_than_out', lt, out - 1, out + 1) > 0: fs = 3 - elif lt_mid > 0: - if st.hour in range(10, 20): - fs = 3 else: fs = 1 else: - if st.hour in range(8, 22): - if st.min % 30 in range(0, 5): - fs = 3 - else: - fs = 0 + if st.min in range(10, 15): + fs = 2 else: fs = 0 - # FIXME: Disabled for now - fs = 0 + st.set("loft/fan", fs) + def auto_circ(): global st if st.hour in range(7, 24): @@ -165,6 +187,23 @@ def auto_circ(): c = 0; st.set("loft/circulation", c) + +def ac_is_on(): + # Heuristics to tell if AC is currently on + + tie = st.get_sensor_avg('air/inside-exhaust') + tmix = st.get_sensor_avg('air/mixed') + if tie is None or tmix is None: + return 0 + + # Mixed air is significantly colder than inside exhaust + ac_on = -st.hysteresis('ac_off', tmix, tie - 5, tie - 4) + + # FIXME: It might also mean that the loft is cold + + return ac_on + + def auto_air(): global st tii = st.get_sensor_avg('air/inside-intake') @@ -173,17 +212,12 @@ def auto_air(): tmix = st.get_sensor_avg('air/mixed') house_warm = st.hysteresis('house_warm', tii, 23.5, 24.5) house_hot = st.hysteresis('house_hot', tii, 24.5, 25) - - # Is AC currently on (mixed air is significantly colder than inside exhaust)? - if tie is None or tmix is None: - ac_off = 1 - else: - ac_off = st.hysteresis('ac_off', tmix, tie - 5, tie - 4) + ac_on = ac_is_on() # XXX: Temporarily disabled - house_warm = -1 - house_hot = -1 - ac_off = 1 + #house_warm = -1 + #house_hot = -1 + #ac_on = -1 # Do we want to bypass the heat exchanger? outside_warmer = st.hysteresis('outside_warmer', diff(toi, tii), -0.5, 0.5) @@ -197,12 +231,57 @@ def auto_air(): mixed_warmer = st.hysteresis('mixed_warmer', diff(tmix, tii), -1, 0) # Do we want to boost heat exchanger fan? - if ac_off < 0 or (house_hot > 0 and mixed_warmer < 0): + if ac_on > 0 or (house_hot > 0 and mixed_warmer < 0): st.set('air/exchanger-fan', 255) else: st.set('air/exchanger-fan', 0) - debug("Air: house_warm={} house_hot={} ac_off={} outside_warmer={} mixed_warmer={}".format(house_warm, house_hot, ac_off, outside_warmer, mixed_warmer)) + debug("Air: house_warm={} house_hot={} ac_on={} outside_warmer={} mixed_warmer={}".format(house_warm, house_hot, ac_on, outside_warmer, mixed_warmer)) + + if ac_on != 0: + st.set("air/ac-on", "{} {}".format(1 if ac_on > 0 else 0, int(st.now))) + else: + st.set("air/ac-on", "") + + +def auto_aircon(): + global st + tii = st.get_sensor_avg('air/inside-intake') + tie = st.get_sensor_avg('air/inside-exhaust') + house_hot = st.hysteresis('ac_house_hot', tii, 23.5, 24) + outside_hot = st.hysteresis('ac_outside_hot', tie, 23, 24) + ac_on = ac_is_on() + + if house_hot > 0 and outside_hot > 0: + want_ac = 1 + else: + want_ac = -1 + + if not hasattr(st, 'last_ac_change'): + st.last_ac_change = st.now + need_wait = st.last_ac_change + 300 - st.now # FIXME: Increase + + if need_wait > 0: + action = f"wait({need_wait:.0f})" + elif ac_on == 0: + action = "need-data" + elif ac_on != want_ac: + action = "change" + st.send('air/aircon-remote', 'p') + st.last_ac_change = st.now + else: + action = "none" + + debug("AC: house_hot={} outside_hot={} ac_on={} want_ac={} action={}".format(house_hot, outside_hot, ac_on, want_ac, action)) + + +def on_connect(mq, userdata, flags, rc): + mq.subscribe("burrow/#") + +def on_message(mq, userdata, msg): + global st + # debug("Message {}: {}".format(msg.topic, msg.payload)) + st.attrs[msg.topic] = msg.payload.decode('utf-8') opts, args = getopt.gnu_getopt(sys.argv[1:], "", ["debug"]) for opt in opts: @@ -214,7 +293,7 @@ mq = mqtt.Client() mq.on_connect = on_connect mq.on_message = on_message mq.will_set("status/auto", "dead", retain=True) -mq.connect("127.0.0.1") +mq.connect("burrow-mqtt") mq.publish("status/auto", "ok", retain=True) mq.loop_start() @@ -224,18 +303,21 @@ time.sleep(3) checks = [ ('loft-fan', auto_loft_fan), ('circ', auto_circ), - ('air', auto_air) + ('air', auto_air), + ('aircon', auto_aircon), ] while True: + debug_open() st.update() debug("averages") indent += 1 - st.update_average('air/outside-intake', 60) - st.update_average('air/inside-intake', 60) - st.update_average('air/inside-exhaust', 60) - st.update_average('air/mixed', 60) + st.update_average('air/outside-intake', 180) + st.update_average('air/inside-intake', 180) + st.update_average('air/inside-exhaust', 180) + st.update_average('air/mixed', 180) + st.update_average('temp/loft', 180) indent -= 1 for name, func in checks: @@ -246,5 +328,6 @@ while True: indent -= 1 else: debug("{} DISABLED".format(name)) - debug("=" * 80) + + debug_close() time.sleep(10)