From 607f6fd6372575fd100d362baab05bd185550d36 Mon Sep 17 00:00:00 2001 From: Martin Mares Date: Tue, 31 Dec 2024 15:07:31 +0100 Subject: [PATCH] Bocktherm: Multi-thermostat mode --- bocktherm/Makefile | 8 +++ bocktherm/burrow-bocktherm.py | 103 +++++++++++++++++----------------- 2 files changed, 60 insertions(+), 51 deletions(-) create mode 100644 bocktherm/Makefile diff --git a/bocktherm/Makefile b/bocktherm/Makefile new file mode 100644 index 0000000..3983712 --- /dev/null +++ b/bocktherm/Makefile @@ -0,0 +1,8 @@ +all: + +install: burrow-bocktherm.py + install burrow-bocktherm.py /usr/local/sbin/burrow-bocktherm + +clean: + +.PHONY: all install clean diff --git a/bocktherm/burrow-bocktherm.py b/bocktherm/burrow-bocktherm.py index 595be14..783024a 100755 --- a/bocktherm/burrow-bocktherm.py +++ b/bocktherm/burrow-bocktherm.py @@ -15,12 +15,12 @@ class Thermostat: ident = "" bock_mqtt: 'BockMQTT' burrow_mqtt: 'BurrowMQTT' + burrow_name: str - def __init__(self, config): + def __init__(self, config, burrow_name): + self.config = config self.ident = str(config['uniqueIdentifier']).rjust(6, '0') - - def status_notify(self, stat): - self.burrow_mqtt.send_status(stat) + self.burrow_name = burrow_name def process_message(self, m): if len(m) > 6 and m[0] == 0x30 and m[1] == 0x0a: @@ -59,7 +59,7 @@ class Thermostat: 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}') - self.burrow_mqtt.send_measurement(t_actual) + self.burrow_mqtt.send_measurement(self.burrow_name, t_actual) def generic_mqtt_log(prefix, level, string): @@ -78,6 +78,7 @@ class BockMQTT: def __init__(self, therm: Thermostat): self.therm = therm + self.log_prefix = f'MQTT({therm.burrow_name})' self.in_topic = f'TSWIFI/I/{therm.ident}' self.out_topic = f'TSWIFI/O/{therm.ident}' self.connected = False @@ -92,7 +93,7 @@ class BockMQTT: mqc.ws_set_options("/wss") mqc.tls_set_context() - mqc.username_pw_set(f'TSWIFI_{therm.ident}', config['mqttPass']) + mqc.username_pw_set(f'TSWIFI_{therm.ident}', therm.config['mqttPass']) def start(self): self.mqc.connect("mqtt.elbock.cz", 8081, 60) @@ -100,59 +101,60 @@ class BockMQTT: def on_connect(self, mqc, userdata, flags, rc): if rc == 0: - logger.info('BockMQTT connected') + logger.info(f'{self.log_prefix} connected') self.connected = True mqc.subscribe(self.in_topic, 0) mqc.subscribe(self.out_topic, 0) else: - logger.warning(f'BockMQTT connect failed: rc={rc}') + logger.warning(f'{self.log_prefix} connect failed: rc={rc}') def on_disconnect(self, mqc, userdata, rc): - logger.info(f'BockMQTT disconnected: rc={rc}') + logger.info(f'{self.log_prefix} disconnected: rc={rc}') self.connected = False - self.therm.status_notify('error') def on_message(self, mqc, userdata, msg): - logger.debug(f'<< {msg.topic} ' + msg.payload.hex(' ')) + logger.debug(f'{self.log_prefix} << {msg.topic} ' + msg.payload.hex(' ')) if msg.topic == self.out_topic: err = self.therm.process_message(msg.payload) if err: - logger.warning(f'Cannot parse message ({err}): {msg.payload.hex(" ")}') + logger.warning(f'{self.log_prefix} parse error ({err}): {msg.payload.hex(" ")}') def on_subscribe(self, mqc, userdata, mid, granted_qos): - logger.debug(f'BockMQTT subscribed: mid={mid}') + logger.debug(f'{self.log_prefix} subscribed: mid={mid}') def on_log(self, mqc, userdata, level, string): - generic_mqtt_log('BockMQTT', level, string) + generic_mqtt_log(self.log_prefix, level, string) def gen_client_id(self): return 'APP_' + "".join(["abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRTUVWXYZ1234567890"[random.randrange(60)] for _ in range(18)]) def send_request(self): + logger.debug(f'{self.log_prefix} 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' - self.mqc.publish('TSWIFI/I/024299', request) + self.mqc.publish(self.in_topic, request) -def read_device_config(config_file, device_name): +def read_config(config_file): 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) + return json.load(f) + + +def find_device_config(config, device_name): + assert isinstance(config, list) + for cf in config: + assert isinstance(cf, dict) + if cf['name'] == device_name: + return cf + print(f'Device {device_name} not found in configuration file', file=sys.stderr) sys.exit(1) class BurrowMQTT: - def __init__(self, sensor): - self.sensor = sensor + def __init__(self): self.connected = False - self.status = '?' mqc = mqtt.Client() self.mqc = mqc @@ -168,9 +170,9 @@ class BurrowMQTT: mqc.on_log = self.on_log def start(self): - self.mqc.will_set(f"status/bocktherm/{self.sensor}", "dead", retain=True) + self.mqc.will_set("status/bocktherm", "dead", retain=True) self.mqc.connect("burrow-mqtt", 8883) - self.send_status('init') + self.mqc.publish("status/bocktherm", "ok", retain=True) self.mqc.loop_start() def on_connect(self, mqc, userdata, flags, rc): @@ -187,28 +189,19 @@ class BurrowMQTT: def on_log(self, mqc, userdata, level, string): generic_mqtt_log('BurrowMQTT', level, string) - def send_status(self, stat): - if stat != self.status: - self.status = stat - self.mqc.publish(f"status/bocktherm/{self.sensor}", stat, retain=True) - - def send_measurement(self, value): + def send_measurement(self, sensor, value): if self.connected: now = int(time.time()) - self.mqc.publish(f'mill/thermostat/{self.sensor}', f'{value} {now}') - self.send_status('ok') + self.mqc.publish(f'mill/thermostat/{sensor}', f'{value} {now}') 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('--sensor', required=True, type=str, help='Sensor name in Burrow MQTT') +parser.add_argument('--devices', nargs='+', type=str, help='Device names: :') 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='{', @@ -218,18 +211,26 @@ logging.basicConfig( logger = logging.getLogger('bocktherm') -therm = Thermostat(config) -therm.bock_mqtt = BockMQTT(therm) -therm.burrow_mqtt = BurrowMQTT(args.sensor) -therm.bock_mqtt.start() -therm.burrow_mqtt.start() +config = read_config(args.config) +burrow_mqtt = BurrowMQTT() +thermostats = [] + +for dev in args.devices: + burrow_name, config_name = dev.split(':') + dev_config = find_device_config(config, config_name) + therm = Thermostat(dev_config, burrow_name) + therm.bock_mqtt = BockMQTT(therm) + therm.burrow_mqtt = burrow_mqtt + thermostats.append(therm) + +burrow_mqtt.start() +for therm in thermostats: + therm.bock_mqtt.start() time.sleep(1) while True: - if therm.bock_mqtt.connected: - logger.debug('Sending request') - therm.bock_mqtt.send_request() - time.sleep(60) - else: - time.sleep(5) + for therm in thermostats: + if therm.bock_mqtt.connected: + therm.bock_mqtt.send_request() + time.sleep(60) -- 2.39.5