From 0e1094d9e8fb84ae7d7115a790e471fb7c7313d2 Mon Sep 17 00:00:00 2001 From: Meatballs1 Date: Tue, 19 Mar 2024 21:31:31 +0000 Subject: [PATCH 01/12] Set MQTT LWT --- hc2mqtt | 1 + 1 file changed, 1 insertion(+) diff --git a/hc2mqtt b/hc2mqtt index 11676d3..71abdc1 100755 --- a/hc2mqtt +++ b/hc2mqtt @@ -66,6 +66,7 @@ def hc2mqtt( else: client.tls_set(cert_reqs=ssl.CERT_NONE) + client.will_set(f"{mqtt_prefix}status", payload="Offline", qos=0, retain=True) client.connect(host=mqtt_host, port=mqtt_port, keepalive=70) for device in devices: From 4a5449dbd685be4061f72f18236880cf5d63b6a4 Mon Sep 17 00:00:00 2001 From: Meatballs1 Date: Wed, 20 Mar 2024 12:14:40 +0000 Subject: [PATCH 02/12] Add online message --- hc2mqtt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/hc2mqtt b/hc2mqtt index 71abdc1..c0311c6 100755 --- a/hc2mqtt +++ b/hc2mqtt @@ -41,6 +41,10 @@ def hc2mqtt( mqtt_keyfile: str, mqtt_clientname: str, ): + + def on_connect(client, userdata, flags, rc): + client.publish(f"{mqtt_prefix}LWT", payload="Online", qos=0, retain=True) + click.echo( f"Hello {devices_file=} {mqtt_host=} {mqtt_prefix=} " f"{mqtt_port=} {mqtt_username=} {mqtt_password=} " @@ -66,7 +70,8 @@ def hc2mqtt( else: client.tls_set(cert_reqs=ssl.CERT_NONE) - client.will_set(f"{mqtt_prefix}status", payload="Offline", qos=0, retain=True) + client.will_set(f"{mqtt_prefix}LWT", payload="Offline", qos=0, retain=True) + client.on_connect = on_connect client.connect(host=mqtt_host, port=mqtt_port, keepalive=70) for device in devices: From 7e66669365694d4187ee0ada2821c11adf97fd8a Mon Sep 17 00:00:00 2001 From: Meatballs1 Date: Wed, 20 Mar 2024 12:56:46 +0000 Subject: [PATCH 03/12] Add some basic MQTT connectivity messages --- hc2mqtt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/hc2mqtt b/hc2mqtt index c0311c6..f9fb5c0 100755 --- a/hc2mqtt +++ b/hc2mqtt @@ -43,7 +43,16 @@ def hc2mqtt( ): def on_connect(client, userdata, flags, rc): - client.publish(f"{mqtt_prefix}LWT", payload="Online", qos=0, retain=True) + if rc == 5: + print(now(), f"ERROR MQTT connection failed: unauthorized - {rc}") + elif rc == 0: + print(now(), f"MQTT connection established: {rc}") + client.publish(f"{mqtt_prefix}LWT", payload="Online", qos=0, retain=True) + else: + print(now(), f"ERROR MQTT connection failed: {rc}") + + def on_disconnect(client, userdata, rc): + print(now(), "ERROR MQTT client disconnected") click.echo( f"Hello {devices_file=} {mqtt_host=} {mqtt_prefix=} " @@ -72,6 +81,7 @@ def hc2mqtt( client.will_set(f"{mqtt_prefix}LWT", payload="Offline", qos=0, retain=True) client.on_connect = on_connect + client.on_disconnect = on_disconnect client.connect(host=mqtt_host, port=mqtt_port, keepalive=70) for device in devices: From 09d883582c97d1fe773669f086976b3a77504f1a Mon Sep 17 00:00:00 2001 From: Meatballs1 Date: Wed, 20 Mar 2024 13:25:16 +0000 Subject: [PATCH 04/12] Improve MQTT disconnection handling --- hc2mqtt | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/hc2mqtt b/hc2mqtt index f9fb5c0..b7ba60e 100755 --- a/hc2mqtt +++ b/hc2mqtt @@ -48,6 +48,10 @@ def hc2mqtt( elif rc == 0: print(now(), f"MQTT connection established: {rc}") client.publish(f"{mqtt_prefix}LWT", payload="Online", qos=0, retain=True) + for device in devices: + mqtt_set_topic = f"{mqtt_prefix}{device['name']}/set" + print(now(), device["name"], f"set topic: {mqtt_set_topic}") + client.subscribe(mqtt_set_topic) else: print(now(), f"ERROR MQTT connection failed: {rc}") @@ -119,6 +123,7 @@ def client_connect(client, device, mqtt_topic): host = device["host"] device_topics = topics + client.on_message = on_message for value in device["features"]: if ( @@ -136,10 +141,6 @@ def client_connect(client, device, mqtt_topic): if not topic.isdigit(): # We only want the named topics state[device_topics[topic]] = None - mqtt_set_topic = mqtt_topic + "/set" - print(now(), device["name"], f"set topic: {mqtt_set_topic}") - client.subscribe(mqtt_set_topic) - client.on_message = on_message while True: try: @@ -175,12 +176,15 @@ def client_connect(client, device, mqtt_topic): if not update: continue - msg = json.dumps(state) - print(now(), device["name"], f"publish to {mqtt_topic} with {msg}") - client.publish(mqtt_topic + "/state", msg) + if client.is_connected(): + msg = json.dumps(state) + print(now(), device["name"], f"publish to {mqtt_topic} with {msg}") + client.publish(mqtt_topic + "/state", msg) + else: + raise Exception("Unable to publish update as mqtt is not connected.") except Exception as e: - print("ERROR", host, e, file=sys.stderr) + print("ERROR", device["name"], e, file=sys.stderr) time.sleep(5) From 6ce753dc114330de052e5c80505bf6ad60a1ed08 Mon Sep 17 00:00:00 2001 From: Meatballs1 Date: Wed, 20 Mar 2024 13:50:16 +0000 Subject: [PATCH 05/12] Subscribe to activeProgram and move on_message --- hc2mqtt | 85 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/hc2mqtt b/hc2mqtt index 1eb0783..a487b75 100755 --- a/hc2mqtt +++ b/hc2mqtt @@ -52,12 +52,48 @@ def hc2mqtt( mqtt_set_topic = f"{mqtt_prefix}{device['name']}/set" print(now(), device["name"], f"set topic: {mqtt_set_topic}") client.subscribe(mqtt_set_topic) + for value in device["features"]: + # If the device has the ActiveProgram feature it allows programs to be started and + # scheduled via /ro/activeProgram + if "BSH.Common.Root.ActiveProgram" == device["features"][value]["name"]: + mqtt_active_program_topic = f"{mqtt_prefix}{device['name']}/activeProgram" + print(now(), device["name"], f"program topic: {mqtt_active_program_topic}") + client.subscribe(mqtt_active_program_topic) else: print(now(), f"ERROR MQTT connection failed: {rc}") def on_disconnect(client, userdata, rc): print(now(), "ERROR MQTT client disconnected") + def on_message(client, userdata, msg): + mqtt_state = msg.payload.decode() + mqtt_topic = msg.topic.split("/") + print(now(), f"{msg.topic} received mqtt message {mqtt_state}") + + try: + if len(mqtt_topic) >= 2: + device_name = mqtt_topic[-2] + topic = mqtt_topic[-1] + else: + raise Exception(f"Invalid mqtt topic {msg.topic}.") + + try: + msg = json.loads(mqtt_state) + except ValueError as e: + raise ValueError(f"Invalid JSON in message: {mqtt_state}.") from e + + if topic == "set": + resource = "/ro/values" + elif topic == "activeProgram": + resource = "/ro/activeProgram" + else: + raise Exception(f"Payload topic {topic} is unknown.") + + dev[device_name].get(resource, 1, "POST", msg) + except Exception as e: + print(now(), device_name, "ERROR", e, file=sys.stderr) + + click.echo( f"Hello {devices_file=} {mqtt_host=} {mqtt_prefix=} " f"{mqtt_port=} {mqtt_username=} {mqtt_password=} " @@ -86,6 +122,7 @@ def hc2mqtt( client.will_set(f"{mqtt_prefix}LWT", payload="Offline", qos=0, retain=True) client.on_connect = on_connect client.on_disconnect = on_disconnect + client.on_message = on_message client.connect(host=mqtt_host, port=mqtt_port, keepalive=70) for device in devices: @@ -107,44 +144,11 @@ dev = {} def client_connect(client, device, mqtt_topic): - def on_message(client, userdata, msg): - print(msg.topic) - mqtt_state = msg.payload.decode() - mqtt_topic = msg.topic.split("/") - print(now(), f"{msg.topic} received mqtt message {mqtt_state}") - - try: - if len(mqtt_topic) >= 2: - device_name = mqtt_topic[-2] - topic = mqtt_topic[-1] - else: - raise Exception(f"Invalid mqtt topic {msg.topic}.") - - try: - msg = json.loads(mqtt_state) - except ValueError as e: - raise ValueError(f"Invalid JSON in message: {mqtt_state}.") from e - - if topic == "set": - resource = "/ro/values" - elif topic == "activeProgram": - resource = "/ro/activeProgram" - else: - raise Exception(f"Payload topic {topic} is unknown.") - - dev[device_name].get(resource, 1, "POST", msg) - except Exception as e: - print(now(), device_name, "ERROR", e, file=sys.stderr) - host = device["host"] device_topics = topics active_program = False for value in device["features"]: - # If the device has the ActiveProgram feature it allows programs to be started and - # scheduled via /ro/activeProgram - if "BSH.Common.Root.ActiveProgram" == device["features"][value]["name"]: - active_program = True if ( "access" in device["features"][value] and "read" in device["features"][value]["access"].lower() @@ -164,13 +168,6 @@ def client_connect(client, device, mqtt_topic): print(now(), device["name"], f"set topic: {mqtt_set_topic}") client.subscribe(mqtt_set_topic) - if active_program: - mqtt_active_program_topic = mqtt_topic + "/activeProgram" - print(now(), device["name"], f"program topic: {mqtt_active_program_topic}") - client.subscribe(mqtt_active_program_topic) - - client.on_message = on_message - while True: try: print(now(), device["name"], f"connecting to {host}") @@ -203,14 +200,14 @@ def client_connect(client, device, mqtt_topic): if client.is_connected(): msg = json.dumps(state) print(now(), device["name"], f"publish to {mqtt_topic} with {msg}") - client.publish(mqtt_topic + "/state", msg) + client.publish(f"{mqtt_topic}/state", msg) else: - raise Exception("Unable to publish update as mqtt is not connected.") + print(now(), device["name"], "ERROR Unable to publish update as mqtt is not connected.") except Exception as e: - print("ERROR", device["name"], e, file=sys.stderr) + print(device["name"], "ERROR", e, file=sys.stderr) - time.sleep(5) + time.sleep(60) if __name__ == "__main__": From a71c58208f292d8132cf3ceb75f54f0c8f855447 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 20 Mar 2024 13:51:02 +0000 Subject: [PATCH 06/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- hc2mqtt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/hc2mqtt b/hc2mqtt index a487b75..9753ea3 100755 --- a/hc2mqtt +++ b/hc2mqtt @@ -93,7 +93,6 @@ def hc2mqtt( except Exception as e: print(now(), device_name, "ERROR", e, file=sys.stderr) - click.echo( f"Hello {devices_file=} {mqtt_host=} {mqtt_prefix=} " f"{mqtt_port=} {mqtt_username=} {mqtt_password=} " @@ -202,7 +201,11 @@ def client_connect(client, device, mqtt_topic): print(now(), device["name"], f"publish to {mqtt_topic} with {msg}") client.publish(f"{mqtt_topic}/state", msg) else: - print(now(), device["name"], "ERROR Unable to publish update as mqtt is not connected.") + print( + now(), + device["name"], + "ERROR Unable to publish update as mqtt is not connected.", + ) except Exception as e: print(device["name"], "ERROR", e, file=sys.stderr) From d6e26d0bfbe8a1dccf97cfb6adfdb117de7bb5d5 Mon Sep 17 00:00:00 2001 From: Meatballs1 Date: Wed, 20 Mar 2024 13:53:35 +0000 Subject: [PATCH 07/12] Fix linting --- hc2mqtt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hc2mqtt b/hc2mqtt index a487b75..fbc9cf2 100755 --- a/hc2mqtt +++ b/hc2mqtt @@ -48,13 +48,14 @@ def hc2mqtt( elif rc == 0: print(now(), f"MQTT connection established: {rc}") client.publish(f"{mqtt_prefix}LWT", payload="Online", qos=0, retain=True) + # Re-subscribe to all device topics on reconnection for device in devices: mqtt_set_topic = f"{mqtt_prefix}{device['name']}/set" print(now(), device["name"], f"set topic: {mqtt_set_topic}") client.subscribe(mqtt_set_topic) for value in device["features"]: - # If the device has the ActiveProgram feature it allows programs to be started and - # scheduled via /ro/activeProgram + # If the device has the ActiveProgram feature it allows programs to be started + # andi scheduled via /ro/activeProgram if "BSH.Common.Root.ActiveProgram" == device["features"][value]["name"]: mqtt_active_program_topic = f"{mqtt_prefix}{device['name']}/activeProgram" print(now(), device["name"], f"program topic: {mqtt_active_program_topic}") @@ -146,7 +147,6 @@ dev = {} def client_connect(client, device, mqtt_topic): host = device["host"] device_topics = topics - active_program = False for value in device["features"]: if ( From 846b1abca039eed9007395fcbdc97d7dc2e45dd2 Mon Sep 17 00:00:00 2001 From: Meatballs1 Date: Wed, 20 Mar 2024 14:03:27 +0000 Subject: [PATCH 08/12] Small typo --- hc2mqtt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hc2mqtt b/hc2mqtt index d807b03..692259e 100755 --- a/hc2mqtt +++ b/hc2mqtt @@ -55,7 +55,7 @@ def hc2mqtt( client.subscribe(mqtt_set_topic) for value in device["features"]: # If the device has the ActiveProgram feature it allows programs to be started - # andi scheduled via /ro/activeProgram + # and scheduled via /ro/activeProgram if "BSH.Common.Root.ActiveProgram" == device["features"][value]["name"]: mqtt_active_program_topic = f"{mqtt_prefix}{device['name']}/activeProgram" print(now(), device["name"], f"program topic: {mqtt_active_program_topic}") From c89962de302fcacb8c98c4f6674231ebc401103d Mon Sep 17 00:00:00 2001 From: Meatballs1 Date: Wed, 20 Mar 2024 14:10:03 +0000 Subject: [PATCH 09/12] Add 20s delay on initial connect, and 60s for exception retries --- hc2mqtt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hc2mqtt b/hc2mqtt index 692259e..4636a9d 100755 --- a/hc2mqtt +++ b/hc2mqtt @@ -168,6 +168,7 @@ def client_connect(client, device, mqtt_topic): client.subscribe(mqtt_set_topic) while True: + time.sleep(20) try: print(now(), device["name"], f"connecting to {host}") ws = HCSocket(host, device["key"], device.get("iv", None)) @@ -210,7 +211,7 @@ def client_connect(client, device, mqtt_topic): except Exception as e: print(device["name"], "ERROR", e, file=sys.stderr) - time.sleep(60) + time.sleep(40) if __name__ == "__main__": From 612786a3c851d0cf9a52b1f7c977cbe077e18b0e Mon Sep 17 00:00:00 2001 From: Meatballs1 Date: Wed, 20 Mar 2024 16:08:53 +0000 Subject: [PATCH 10/12] HA uses offline/online so will use that for LWT --- HCDevice.py | 7 ++++--- hc2mqtt | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/HCDevice.py b/HCDevice.py index ffc20e8..54fa5c1 100755 --- a/HCDevice.py +++ b/HCDevice.py @@ -119,10 +119,11 @@ class HCDevice: ) if "options" in data: - for option_uid in data["options"]: + for option in data["options"]: + option_uid = option["uid"] if str(option_uid) not in self.features: - raise ValueError( - f"Unable to configure appliance. Option UID {uid} is not" + raise ValueError( + f"Unable to configure appliance. Option UID {option_uid} is not" " valid for this device." ) diff --git a/hc2mqtt b/hc2mqtt index 4636a9d..fb574ad 100755 --- a/hc2mqtt +++ b/hc2mqtt @@ -47,7 +47,7 @@ def hc2mqtt( print(now(), f"ERROR MQTT connection failed: unauthorized - {rc}") elif rc == 0: print(now(), f"MQTT connection established: {rc}") - client.publish(f"{mqtt_prefix}LWT", payload="Online", qos=0, retain=True) + client.publish(f"{mqtt_prefix}LWT", payload="online", qos=0, retain=True) # Re-subscribe to all device topics on reconnection for device in devices: mqtt_set_topic = f"{mqtt_prefix}{device['name']}/set" @@ -119,7 +119,7 @@ def hc2mqtt( else: client.tls_set(cert_reqs=ssl.CERT_NONE) - client.will_set(f"{mqtt_prefix}LWT", payload="Offline", qos=0, retain=True) + client.will_set(f"{mqtt_prefix}LWT", payload="offline", qos=0, retain=True) client.on_connect = on_connect client.on_disconnect = on_disconnect client.on_message = on_message From e03938839b0aea97ad19582ccaab4b1f1f61ebe4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 20 Mar 2024 16:09:14 +0000 Subject: [PATCH 11/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- HCDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HCDevice.py b/HCDevice.py index 54fa5c1..b72ff21 100755 --- a/HCDevice.py +++ b/HCDevice.py @@ -122,7 +122,7 @@ class HCDevice: for option in data["options"]: option_uid = option["uid"] if str(option_uid) not in self.features: - raise ValueError( + raise ValueError( f"Unable to configure appliance. Option UID {option_uid} is not" " valid for this device." ) From 8b9caf045eaba4a54b4e39f59a26978e2e69bdf4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 20 Mar 2024 23:41:08 +0000 Subject: [PATCH 12/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- hc2mqtt | 1 + 1 file changed, 1 insertion(+) diff --git a/hc2mqtt b/hc2mqtt index 06607dd..7e42737 100755 --- a/hc2mqtt +++ b/hc2mqtt @@ -213,5 +213,6 @@ def client_connect(client, device, mqtt_topic): time.sleep(40) + if __name__ == "__main__": hc2mqtt()