From: Martin Mares Date: Tue, 31 Dec 2024 12:06:28 +0000 (+0100) Subject: Bocktherm: WIP X-Git-Url: http://mj.ucw.cz/gitweb/?a=commitdiff_plain;h=4bc5ebcfc985de725eb2c04be51740fdc11ffcd0;p=home-hw.git Bocktherm: WIP --- diff --git a/bocktherm/burrow-bocktherm.py b/bocktherm/burrow-bocktherm.py new file mode 100755 index 0000000..3f23315 --- /dev/null +++ b/bocktherm/burrow-bocktherm.py @@ -0,0 +1,135 @@ +#!/usr/bin/python3 + +import argparse +from datetime import datetime +import json +import paho.mqtt.client as mqtt +import random +import sys +import time + + +class Thermostat: + connected = False + ident = "" + in_topic = "" + out_topic = "" + # mqc = mqtt client + + +def on_connect(mqc, therm, flags, rc): + print(f'# Connect: rc={rc}') + if rc == 0: + therm.connected = True + mqc.subscribe(therm.in_topic, 0) + mqc.subscribe(therm.out_topic, 0) + + +def on_disconnect(mqc, therm, rc): + print(f'# Disconnect: 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(' ')) + + 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 !!!') + + +def on_subscribe(mqc, therm, mid, granted_qos): + print(f'# Subscribed: mid={mid}, granted={granted_qos}') + + +def on_log(mqc, therm, level, string): + print(f'# Log[{level}]: {string}') + + +def gen_client_id(): + return 'APP_' + "".join(["abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRTUVWXYZ1234567890"[random.randrange(60)] for _ in range(18)]) + + +def read_device_config(config_file, device_name): + with open(config_file) as f: + js = json.load(f) + assert isinstance(js, list) + for cf in js: + assert isinstance(cf, dict) + if cf['name'] == device_name: + return cf + print('Device not found in configuration file', file=sys.stderr) + sys.exit(1) + + +def init_therm(config): + therm = Thermostat() + therm.ident = str(config['uniqueIdentifier']).rjust(6, '0') + therm.in_topic = f'TSWIFI/I/{therm.ident}' + therm.out_topic = f'TSWIFI/O/{therm.ident}' + + mqc = mqtt.Client(transport='websockets', client_id=gen_client_id(), userdata=therm) + mqc.on_message = on_message + mqc.on_connect = on_connect + mqc.on_disconnect = on_disconnect + mqc.on_subscribe = on_subscribe + mqc.on_log = on_log + + mqc.ws_set_options("/wss") + mqc.tls_set_context() + mqc.username_pw_set(f'TSWIFI_{therm.ident}', config['mqttPass']) + mqc.connect("mqtt.elbock.cz", 8081, 60) + + mqc.loop_start() + + therm.mqc = mqc + return therm + + +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') + +args = parser.parse_args() + +config = read_device_config(args.config, args.device) +therm = init_therm(config) + +time.sleep(1) +while True: + if therm.connected: + print('Brum: Connected') + # 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) diff --git a/bocktherm/test.py b/bocktherm/test.py deleted file mode 100755 index 9a1533a..0000000 --- a/bocktherm/test.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/python3 - -import argparse -from datetime import datetime -import paho.mqtt.client as mqtt -import random -import time - - -connected = False - - -def on_connect(mqc, userdata, flags, rc): - print(f'# Connect: rc={rc}') - if rc == 0: - global connected - connected = True - - -def on_disconnect(mqc, userdata, rc): - print(f'# Disconnect: rc={rc}') - global connected - connected = False - - -def on_message(mqc, userdata, msg): - t = datetime.now().isoformat() - print(f'@ {t} {msg.topic}') - print('\t' + 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 !!!') - - -def on_subscribe(mqc, userdata, mid, granted_qos): - print(f'# Subscribed: mid={mid}, granted={granted_qos}') - - -def on_log(mqc, userdata, level, string): - print(f'# Log[{level}]: {string}') - - -def gen_client_id(): - return 'APP_' + "".join(["abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRTUVWXYZ1234567890"[random.randrange(60)] for _ in range(18)]) - - -mqc = mqtt.Client(transport='websockets', client_id=gen_client_id()) -mqc.on_message = on_message -mqc.on_connect = on_connect -mqc.on_disconnect = on_disconnect -mqc.on_subscribe = on_subscribe -mqc.on_log = on_log - -mqc.ws_set_options("/wss") -mqc.tls_set_context() -mqc.username_pw_set('TSWIFI_024299', 'iqjYBfrZGe') -mqc.connect("mqtt.elbock.cz", 8081, 60) - -# FIXME: Move to connect callback -mqc.subscribe("TSWIFI/I/024299", 0) -mqc.subscribe("TSWIFI/O/024299", 0) - -mqc.loop_start() - -while True: - if connected: - print('Brum: Connected') - # 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' - mqc.publish('TSWIFI/I/024299', request) - time.sleep(60) - else: - print('Brum: Not connected') - time.sleep(5)