From: Martin Mares Date: Tue, 31 Dec 2024 12:40:52 +0000 (+0100) Subject: Bocktherm: WIP X-Git-Url: http://mj.ucw.cz/gitweb/?a=commitdiff_plain;h=99b32f540bd126f0e38a5ce10777bfd5fb204731;p=home-hw.git Bocktherm: WIP --- diff --git a/bocktherm/burrow-bocktherm.py b/bocktherm/burrow-bocktherm.py index 3f23315..4e28182 100755 --- a/bocktherm/burrow-bocktherm.py +++ b/bocktherm/burrow-bocktherm.py @@ -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)