]> mj.ucw.cz Git - home-hw.git/blobdiff - auto/burrow-auto
Auto: Switch to Python 3, add air control
[home-hw.git] / auto / burrow-auto
index b7eb527fccab2f6f773ae750c26f5b1b1df5dc41..4c30f59cc813e50c8641f25b23aa0d648956ef86 100755 (executable)
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/python3
 
 import getopt
 import paho.mqtt.client as mqtt
 
 import getopt
 import paho.mqtt.client as mqtt
@@ -6,40 +6,78 @@ import sys
 import time
 
 debug_mode = False
 import time
 
 debug_mode = False
+indent = 0
 
 def debug(msg):
     if debug_mode:
 
 def debug(msg):
     if debug_mode:
-        print msg
+        print(("\t" * indent) + msg)
 
 class State:
     def __init__(self):
 
 class State:
     def __init__(self):
-       self.attrs = {}
+            self.attrs = {}
 
     def update(self):
 
     def update(self):
-       self.now = time.time()
-       tm = time.localtime(self.now)
-       self.year = tm.tm_year
-       self.month = tm.tm_mon
-       self.day = tm.tm_mday
-       self.hour = tm.tm_hour
-       self.min = tm.tm_min
-       self.wday = tm.tm_wday
+        self.now = time.time()
+        tm = time.localtime(self.now)
+        self.year = tm.tm_year
+        self.month = tm.tm_mon
+        self.day = tm.tm_mday
+        self.hour = tm.tm_hour
+        self.min = tm.tm_min
+        self.wday = tm.tm_wday
+        self.hyst_state = {}
+        debug("[{}-{:02d}-{:02d} {:02d}:{:02d}:{:02d} wday={}]".format(
+            self.year, self.month, self.day,
+            self.hour, self.min, tm.tm_sec,
+            self.wday
+            ))
 
     def get_sensor(self, key):
         topic = "burrow/" + key
 
     def get_sensor(self, key):
         topic = "burrow/" + key
-       if topic in self.attrs:
-           s = self.attrs[topic].split(" ")
-           if len(s) >= 2 and s[1] < self.now - 120:
-               return None
-           return float(s[0])
-       else:
-           return None
+        if topic in self.attrs:
+            s = self.attrs[topic].split(" ")
+            if len(s) >= 2 and int(s[1]) < self.now - 120:
+                debug("< {} EXPIRED".format(key))
+                return None
+            else:
+                debug("< {} = {}".format(key, s[0]))
+                return float(s[0])
+        else:
+            debug("< {} UNDEFINED".format(key))
+            return None
 
     def set(self, key, val):
         global mq
         topic = "burrow/" + key
 
     def set(self, key, val):
         global mq
         topic = "burrow/" + key
-        debug("Setting {} to {}".format(topic, val))
-       mq.publish(topic, val, qos=1, retain=True)
+        debug("> {} = {}".format(topic, val))
+        mq.publish(topic, val, qos=1, retain=True)
+
+    def auto_enabled(self, key):
+        topic = "burrow/auto/" + key
+        if topic in self.attrs:
+            return self.attrs[topic] != '0'
+        else:
+            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
+        if value is None:
+            new_state = 0
+        elif old_state <= 0:
+            if value >= high:
+                new_state = 1
+            else:
+                new_state = -1
+        else:
+            if value <= low:
+                new_state = -1
+            else:
+                new_state = 1
+        self.hyst_state[key] = new_state
+        return new_state
 
 st = State()
 
 
 st = State()
 
@@ -48,28 +86,80 @@ def on_connect(mq, userdata, flags, rc):
 
 def on_message(mq, userdata, msg):
     global st
 
 def on_message(mq, userdata, msg):
     global st
-    debug("Message {}: {}".format(msg.topic, msg.payload))
-    st.attrs[msg.topic] = msg.payload
+    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")
 
 def auto_loft_fan():
     global st
     lt = st.get_sensor("temp/loft")
-    if lt is not None and lt >= 30:
-       fs = 3
-    elif st.hour in range(10, 20):
-       fs = 2
+    lt_high = st.hysteresis('lt_high', lt, 29, 30)
+    lt_mid = st.hysteresis('lt_mid', lt, 24, 25)
+    if lt_high > 0:
+            fs = 3
+    elif lt_mid > 0:
+        if st.hour in range(10, 20):
+                fs = 3
+        else:
+            fs = 1
     else:
     else:
-        fs = 1
+        if st.hour in range(8, 22):
+            if st.min % 30 in range(0, 5):
+                fs = 3
+            else:
+                fs = 0
+        else:
+            fs = 0
+    # FIXME: Disabled for now
+    fs = 0
     st.set("loft/fan", fs)
 
 def auto_circ():
     global st
     st.set("loft/fan", fs)
 
 def auto_circ():
     global st
-    if st.hour in range(19, 23):
+    if st.hour in range(20, 22):
         c = 1
     else:
         c = 0;
     st.set("loft/circulation", c)
 
         c = 1
     else:
         c = 0;
     st.set("loft/circulation", c)
 
+def auto_air():
+    global st
+    tii = st.get_sensor('air/inside-intake')
+    tie = st.get_sensor('air/inside-exhaust')
+    toi = st.get_sensor('air/outside-intake')
+    tmix = st.get_sensor('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)
+
+    # Do we want to bypass the heat exchanger?
+    if toi is None or tie is None:
+        st.set('air/bypass', 0)
+    else:
+        outside_warmer = (st.hysteresis('bypass', toi, tii - 0.5, tii + 0.5) >= 0)
+        if (house_warm >= 0) == outside_warmer:
+            st.set('air/bypass', 0)
+        else:
+            st.set('air/bypass', 1)
+
+    # Is mixed air colder than air from the inside?
+    if tii is None or tmix is None:
+        mixed_warmer = 0
+    else:
+        mixed_warmer = st.hysteresis('mixed_warmer', tmix, tii - 1, tii)
+
+    # Do we want to boost heat exchanger fan?
+    if ac_off < 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))
+
 opts, args = getopt.gnu_getopt(sys.argv[1:], "", ["debug"])
 for opt in opts:
     o, arg = opt
 opts, args = getopt.gnu_getopt(sys.argv[1:], "", ["debug"])
 for opt in opts:
     o, arg = opt
@@ -79,13 +169,29 @@ for opt in opts:
 mq = mqtt.Client()
 mq.on_connect = on_connect
 mq.on_message = on_message
 mq = mqtt.Client()
 mq.on_connect = on_connect
 mq.on_message = on_message
-mq.will_set("status/auto", "failed", retain=True)
+mq.will_set("status/auto", "dead", retain=True)
 mq.connect("127.0.0.1")
 mq.publish("status/auto", "ok", retain=True)
 mq.loop_start()
 
 mq.connect("127.0.0.1")
 mq.publish("status/auto", "ok", retain=True)
 mq.loop_start()
 
+# Heuristic delay to get all attributes from MQTT
+time.sleep(3)
+
+checks = [
+    ('loft-fan', auto_loft_fan),
+    ('circ', auto_circ),
+    ('air', auto_air)
+]
+
 while True:
     st.update()
 while True:
     st.update()
-    auto_loft_fan()
-    auto_circ()
+    for name, func in checks:
+        if st.auto_enabled(name):
+            debug(name)
+            indent += 1
+            func()
+            indent -= 1
+        else:
+            debug("{} DISABLED".format(name))
+    debug("=" * 80)
     time.sleep(10)
     time.sleep(10)