diff --git a/HCDevice.py b/HCDevice.py index 5d4ef86..ffc20e8 100755 --- a/HCDevice.py +++ b/HCDevice.py @@ -92,21 +92,55 @@ class HCDevice: return result + # Based on PR submitted https://github.com/Skons/hcpy/pull/1 + def test_program_data(self, data): + if "program" not in data: + raise TypeError("Message data invalid, no program specified.") + + if isinstance(data["program"], int) is False: + raise TypeError("Message data invalid, UID in 'program' must be an integer.") + + # devices.json stores UID as string + uid = str(data["program"]) + if uid not in self.features: + raise ValueError( + f"Unable to configure appliance. Program UID {uid} is not valid" + " for this device." + ) + + feature = self.features[uid] + # Diswasher is Dishcare.Dishwasher.Program.{name} + # Hood is Cooking.Common.Program.{name} + # May also be in the format BSH.Common.Program.Favorite.001 + if ".Program." not in feature["name"]: + raise ValueError( + f"Unable to configure appliance. Program UID {uid} is not a valid" + f" program - {feature['name']}." + ) + + if "options" in data: + for option_uid in data["options"]: + if str(option_uid) not in self.features: + raise ValueError( + f"Unable to configure appliance. Option UID {uid} is not" + " valid for this device." + ) + # Test the feature of an appliance agains a data object def test_feature(self, data): if "uid" not in data: - raise Exception("{self.name}. Unable to configure appliance. UID is required.") + raise Exception("Unable to configure appliance. UID is required.") if isinstance(data["uid"], int) is False: - raise Exception("{self.name}. Unable to configure appliance. UID must be an integer.") + raise Exception("Unable to configure appliance. UID must be an integer.") if "value" not in data: - raise Exception("{self.name}. Unable to configure appliance. Value is required.") + raise Exception("Unable to configure appliance. Value is required.") # Check if the uid is present for this appliance uid = str(data["uid"]) if uid not in self.features: - raise Exception(f"{self.name}. Unable to configure appliance. UID {uid} is not valid.") + raise Exception(f"Unable to configure appliance. UID {uid} is not valid.") feature = self.features[uid] @@ -114,14 +148,14 @@ class HCDevice: print(now(), self.name, f"Processing feature {feature['name']} with uid {uid}") if "access" not in feature: raise Exception( - f"{self.name}. Unable to configure appliance." + "Unable to configure appliance. " f"Feature {feature['name']} with uid {uid} does not have access." ) access = feature["access"].lower() if access != "readwrite" and access != "writeonly": raise Exception( - f"{self.name}. Unable to configure appliance." + "Unable to configure appliance. " f"Feature {feature['name']} with uid {uid} has got access {feature['access']}." ) @@ -130,16 +164,16 @@ class HCDevice: if isinstance(data["value"], int) is False: raise Exception( f"Unable to configure appliance. The value {data['value']} must be an integer." - f"Allowed values are {feature['values']}." + f" Allowed values are {feature['values']}." ) value = str(data["value"]) # values are strings in the feature list, # but always seem to be an integer. An integer must be provided if value not in feature["values"]: raise Exception( - f"{self.name}. Unable to configure appliance." - f"Value {data['value']} is not a valid value." - f"Allowed values are {feature['values']}." + "Unable to configure appliance. " + f"Value {data['value']} is not a valid value. " + f"Allowed values are {feature['values']}. " ) if "min" in feature: @@ -151,13 +185,11 @@ class HCDevice: or data["value"] > max ): raise Exception( - f"{self.name}. Unable to configure appliance." - f"Value {data['value']} is not a valid value." + "Unable to configure appliance. " + f"Value {data['value']} is not a valid value. " f"The value must be an integer in the range {min} and {max}." ) - return True - def recv(self): try: buf = self.ws.recv() @@ -198,11 +230,14 @@ class HCDevice: if data is not None: if action == "POST": - if self.test_feature(data) is False: - return - msg["data"] = [data] - else: - msg["data"] = [data] + if resource == "/ro/values": + # Raises exceptions on failure + self.test_feature(data) + elif resource == "/ro/activeProgram": + # Raises exception on failure + self.test_program_data(data) + + msg["data"] = [data] try: self.ws.send(msg) @@ -294,17 +329,6 @@ class HCDevice: self.services[service["service"]] = { "version": service["version"], } - # print(self.name, now(), "services", self.services) - - # we should figure out which ones to query now - # if "iz" in self.services: - # self.get("/iz/info", version=self.services["iz"]["version"]) - # if "ni" in self.services: - # self.get("/ni/info", version=self.services["ni"]["version"]) - # if "ei" in self.services: - # self.get("/ei/deviceReady", version=self.services["ei"]["version"], action="NOTIFY") - - # self.get("/if/info") else: print(now(), self.name, "Unknown", msg) diff --git a/README.md b/README.md index 32f3d55..2cdd651 100644 --- a/README.md +++ b/README.md @@ -435,6 +435,24 @@ Synchronize with time server, `false` is disabled {"uid":547,"value":false} ``` +### Starting a Program + +The MQTT client listens on /{prefix}/{devicename}/activeProgram for a JSON message to start a program. The JSON should be in the following format: + +```json +{"program":{uid},"options":[{"uid":{uid},"value":{value}}]} +``` + +To start a dishwasher on eco mode (`Dishcare.Dishwasher.Program.Eco50`): +```json +{"program":8196} +``` + +To start a dishwasher on eco mode in 10 miuntes (`BSH.Common.Option.StartInRelative`): +```json +{"program":8196,"options":[{"uid":558,"value":600}]} +``` + ## FRIDA tools Moved to [`README-frida.md`](README-frida.md) diff --git a/hc2mqtt b/hc2mqtt index 11676d3..dd1237c 100755 --- a/hc2mqtt +++ b/hc2mqtt @@ -88,23 +88,42 @@ 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"received mqtt message {mqtt_state}") + print(now(), f"{msg.topic} received mqtt message {mqtt_state}") + try: - msg = json.loads(mqtt_state) - if "uid" in msg: - dev[mqtt_topic[-2]].get("/ro/values", 1, "POST", msg) + if len(mqtt_topic) >= 2: + device_name = mqtt_topic[-2] + topic = mqtt_topic[-1] else: - raise Exception(f"Payload {msg} is not correctly formatted") + 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("ERROR", e, file=sys.stderr) + 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() @@ -123,6 +142,12 @@ def client_connect(client, device, mqtt_topic): mqtt_set_topic = mqtt_topic + "/set" 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: @@ -147,11 +172,6 @@ def client_connect(client, device, mqtt_topic): if value is None: continue - # new_topic = topics[topic] - # if new_topic == "remaining": - # state["remainingseconds"] = value - # value = "%d:%02d" % (value / 60 / 60, (value / 60) % 60) - new_topic = device_topics[topic] state[new_topic] = value update = True