]> mj.ucw.cz Git - home-hw.git/blobdiff - auto/burrow-auto
Auto: Meditation mode turned off
[home-hw.git] / auto / burrow-auto
index 47707181ad1d0a2a85e42358c328599fac8f6870..b876223dae335efe839a193bb07c71600fa0d8db 100755 (executable)
@@ -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,39 @@ 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 False and st.hour in range(21, 24) or st.hour in range(6, 10):
+        fs = 0
+    elif 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 +189,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 +214,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 +233,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, 24, 25)
+    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:
@@ -224,18 +305,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 +330,6 @@ while True:
             indent -= 1
         else:
             debug("{} DISABLED".format(name))
-    debug("=" * 80)
+
+    debug_close()
     time.sleep(10)