]> mj.ucw.cz Git - home-hw.git/commitdiff
Bocktherm: WIP
authorMartin Mares <mj@ucw.cz>
Tue, 31 Dec 2024 12:40:52 +0000 (13:40 +0100)
committerMartin Mares <mj@ucw.cz>
Tue, 31 Dec 2024 12:40:52 +0000 (13:40 +0100)
bocktherm/burrow-bocktherm.py

index 3f233150e9e50724961a5ba891eb56dc474f80ec..4e28182a20ff6f5900e2e4bd6c445b63f88188e3 100755 (executable)
@@ -1,8 +1,8 @@
 #!/usr/bin/python3
 
 import argparse
-from datetime import datetime
 import json
+import logging
 import paho.mqtt.client as mqtt
 import random
 import sys
@@ -16,61 +16,82 @@ class Thermostat:
     out_topic = ""
     # mqc = mqtt client
 
+    def process_message(self, m):
+        if len(m) > 6 and m[0] == 0x30 and m[1] == 0x0a:
+            return self.process_report(m)
+        else:
+            return 'Unknown message type'
+
+    def process_report(self, m):
+        main_len = 256*m[4] + m[5]
+        if 6 + main_len != len(m):
+            return 'Report length mismatch'
+
+        i = 6
+        while i < len(m):
+            if i + 4 > len(m):
+                return 'Malformed sub-packet header'
+            ln = 256*m[i+2] + m[i+3]
+            if 4 + ln > len(m):
+                return 'Truncated sub-packet'
+            self.process_subreport(m[i], m[i+1], m[i+4:i+4+ln])
+            i += 4 + ln
+
+        return None
+
+    def process_subreport(self, mjr, mnr, val):
+        # logger.debug(f'<<< {mjr:02x} {mnr:02x} = {val.hex(" ")}')
+
+        if mjr == 0x30 and mnr == 0x0b and len(val) == 5:
+            t_desired = val[0] / 2
+            t_actual = val[1] + (val[2] & 0x0f) / 10
+            if val[2] & 0x10 != 0:
+                t_actual = -t_actual
+            if val[2] & 0x40 != 0:
+                t_actual = -999
+            analog = val[2] & 0x20 != 0
+            t_manual = val[3] / 2
+            t_auto = val[4] / 2
+            logger.info(f'Temp: desired={t_desired} actual={t_actual} analog={analog} manual={t_manual} auto={t_auto}')
+
 
 def on_connect(mqc, therm, flags, rc):
-    print(f'# Connect: rc={rc}')
     if rc == 0:
+        logger.info('MQTT connected')
         therm.connected = True
         mqc.subscribe(therm.in_topic, 0)
         mqc.subscribe(therm.out_topic, 0)
+    else:
+        logger.warning(f'MQTT connect failed: rc={rc}')
 
 
 def on_disconnect(mqc, therm, rc):
-    print(f'# Disconnect: rc={rc}')
+    logger.info(f'MQTT disconnected: rc={rc}')
     therm.connected = False
 
 
 def on_message(mqc, therm, msg):
-    t = datetime.now().isoformat()
-    print(f'@ {t} {msg.topic}')
-    print('\t' + msg.payload.hex(' '))
+    logger.debug(f'<< {msg.topic} ' + msg.payload.hex(' '))
 
-    m = msg.payload
-    if len(m) > 6 and m[0] in (0x10, 0x30) and m[1] == 0x0a:
-        print(f'\t\t{m[:6].hex(" ")}')
-        i = 6
-        while i + 4 <= len(m):
-            ln = 256*m[i+2] + m[i+3]
-            if 4 + ln > len(m):
-                break
-            mj = m[i]
-            mn = m[i+1]
-            val = m[i+4:i+4+ln]
-            print(f'\t\t\t{mj:02x} {mn:02x} = {val.hex(" ")}')
-            if mj == 0x30 and mn == 0x0b and ln == 5:
-                t_desired = val[0] / 2
-                t_actual = val[1] + (val[2] & 0x0f) / 10
-                if val[2] & 0x10 != 0:
-                    t_actual = -t_actual
-                if val[2] & 0x40 != 0:
-                    t_actual = -999
-                analog = val[2] & 0x20 != 0
-                t_manual = val[3] / 2
-                t_auto = val[4] / 2
-                print(f'\t\t\t\t-> desired={t_desired} actual={t_actual} analog={analog} manual={t_manual} auto={t_auto}')
-            i += 4 + ln
-        if i != len(m):
-            print('\t\t!!! Partial Decode !!!')
-    else:
-        print('\t\t!!! Unknown Packet !!!')
+    if msg.topic == therm.out_topic:
+        err = therm.process_message(msg.payload)
+        if err:
+            logger.warning(f'Cannot parse message ({err}): {msg.payload.hex(" ")}')
 
 
 def on_subscribe(mqc, therm, mid, granted_qos):
-    print(f'# Subscribed: mid={mid}, granted={granted_qos}')
+    logger.debug(f'MQTT subscribed: mid={mid}, granted={granted_qos}')
 
 
 def on_log(mqc, therm, level, string):
-    print(f'# Log[{level}]: {string}')
+    if level == mqtt.MQTT_LOG_DEBUG:
+        logger.debug(f'MQTT: {string}')
+    elif level == mqtt.MQTT_LOG_WARNING:
+        logger.warning(f'MQTT: {string}')
+    elif level == mqtt.MQTT_LOG_ERROR:
+        logger.error(f'MQTT: {string}')
+    else:
+        logger.info(f'MQTT: {string}')
 
 
 def gen_client_id():
@@ -116,20 +137,30 @@ def init_therm(config):
 parser = argparse.ArgumentParser(description='A daemon watching Elektrobock thermostats')
 parser.add_argument('--config', required=True, type=str, help='Configuration file from the web app (devices.json)')
 parser.add_argument('--device', required=True, type=str, help='Device name as in the configuration file')
+parser.add_argument('--debug', default=False, action='store_true', help='Log debug messages')
 
 args = parser.parse_args()
 
 config = read_device_config(args.config, args.device)
+
+logging.basicConfig(
+    format='{asctime}.{msecs:03.0f} {levelname} ({name}) {message}',
+    style='{',
+    datefmt='%Y-%m-%d %H:%M:%S',
+    level=logging.DEBUG if args.debug else logging.INFO,
+)
+
+logger = logging.getLogger('bocktherm')
+
 therm = init_therm(config)
 
 time.sleep(1)
 while True:
     if therm.connected:
-        print('Brum: Connected')
+        logger.debug('Sending request')
         # Request message sent by the web interface, but with a random message ID
         request = b'\x10\x0a' + random.randbytes(2) + b'\x00\x24\x20\x01\x00\x00\x20\x02\x00\x00\x20\x03\x00\x00\x20\x06\x00\x00\x20\x07\x00\x00\x20\x05\x00\x00\x20\x09\x00\x00\x20\x0b\x00\x00\x20\x0c\x00\x00'
         therm.mqc.publish('TSWIFI/I/024299', request)
         time.sleep(60)
     else:
-        print('Brum: Not connected')
         time.sleep(5)