Allow for posting to appliance

This commit is contained in:
Kevin Temming
2023-07-07 15:44:43 +02:00
committed by GitHub
parent 88b47e98e3
commit 18304c5fc2
3 changed files with 158 additions and 23 deletions

View File

@@ -54,7 +54,7 @@ def now():
return datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
class HCDevice:
def __init__(self, ws, features):
def __init__(self, ws, features, name):
self.ws = ws
self.features = features
self.session_id = None
@@ -62,13 +62,14 @@ class HCDevice:
self.device_name = "hcpy"
self.device_id = "0badcafe"
self.debug = False
self.name = name
def parse_values(self, values):
if not self.features:
return values
result = {}
for msg in values:
uid = str(msg["uid"])
value = msg["value"]
@@ -92,19 +93,62 @@ class HCDevice:
return result
# 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.")
if isinstance(data['uid'], int) == False:
raise Exception("{self.name}. 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.")
# 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.")
feature = self.features[uid]
# check the access level of the feature
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. 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. Feature {feature['name']} with uid {uid} has got access {feature['access']}.")
# check if selected list with values is allowed
if 'values' in feature:
if isinstance(data['value'], int) == False:
raise Exception(f"Unable to configure appliance. The value {data['value']} must be an integer. 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. Value {data['value']} is not a valid value. Allowed values are {feature['values']}.")
if 'min' in feature:
min = int(feature['min'])
max = int(feature['min'])
if isinstance(data['value'], int) == False or data['value'] < min or data['value'] > max:
raise Exception(f"{self.name}. Unable to configure appliance. Value {data['value']} is not a valid value. The value must be an integer in the range {min} and {max}.")
return True
def recv(self):
try:
buf = self.ws.recv()
if buf is None:
return None
except Exception as e:
print("receive error", e, traceback.format_exc())
print(self.name, "receive error", e, traceback.format_exc())
return None
try:
return self.handle_message(buf)
except Exception as e:
print("error handling msg", e, buf, traceback.format_exc())
print(self.name, "error handling msg", e, buf, traceback.format_exc())
return None
# reply to a POST or GET message with new data
@@ -129,25 +173,32 @@ class HCDevice:
}
if data is not None:
msg["data"] = [data]
if action == "POST":
if self.test_feature(data) != True:
return
msg["data"] = [data]
else:
msg["data"] = [data]
self.ws.send(msg)
try:
self.ws.send(msg)
except Exception as e:
print(self.name, "Failed to send", e, msg, traceback.format_exc())
self.tx_msg_id += 1
def handle_message(self, buf):
msg = json.loads(buf)
if self.debug:
print(now(), "RX:", msg)
print(now(), self.name, "RX:", msg)
sys.stdout.flush()
resource = msg["resource"]
action = msg["action"]
values = {}
if "code" in msg:
#print(now(), "ERROR", msg["code"])
print(now(), self.name, "ERROR", msg["code"])
values = {
"error": msg["code"],
"resource": msg.get("resource", ''),
@@ -186,7 +237,7 @@ class HCDevice:
self.get("/ro/allMandatoryValues")
#self.get("/ro/values")
else:
print(now(), "Unknown resource", resource, file=sys.stderr)
print(now(), self.name, "Unknown resource", resource, file=sys.stderr)
elif action == "RESPONSE" or action == "NOTIFY":
if resource == "/iz/info" or resource == "/ci/info":
@@ -204,7 +255,10 @@ class HCDevice:
elif resource == "/ro/allMandatoryValues" \
or resource == "/ro/values":
values = self.parse_values(msg["data"])
if 'data' in msg:
values = self.parse_values(msg["data"])
else:
print(now(), self.name, f"received {msg}")
elif resource == "/ci/registeredDevices":
# we don't care
pass
@@ -215,7 +269,7 @@ class HCDevice:
self.services[service["service"]] = {
"version": service["version"],
}
#print(now(), "services", self.services)
#print(self.name, now(), "services", self.services)
# we should figure out which ones to query now
# if "iz" in self.services:
@@ -227,8 +281,8 @@ class HCDevice:
#self.get("/if/info")
else:
print(now(), "Unknown", msg)
else:
print(now(), self.name, "Unknown", msg)
# return whatever we've parsed out of it
return values

View File

@@ -352,6 +352,50 @@ The coffee machine needs a better mapping to MQTT messages.
```
</details>
## Posting to the appliance
Whereas the reading of de status is very beta, this is very very alpha. There is some basic error handling, but don't expect that everything will work.
In your config file you can find items that contain `readWrite` or `writeOnly`, some of them contain values so you know what to provide, ie:
```json
"539": {
"name": "BSH.Common.Setting.PowerState",
"access": "readWrite",
"available": "true",
"refCID": "03",
"refDID": "80",
"values": {
"2": "On",
"3": "Standby"
}
},
```
With this information you can build the JSON object you can send over mqtt to change the power state
Topic: `homeconnect/[devicename]/set`, ie `homeconnect/coffeemaker/set`
Payload:
```json
{"uid":539,"value":2}
```
As for now, the results will be displayed by the script only, there is no response to an mqtt topic.
There are properties that do not require predefined values, debugging is required to see what is needed. Here are some of those values found through debugging:
Set the time:
```json
{"uid":520,"value":"2023-07-07T15:01:21"}
```
Synchronize with time server, `false` is disabled
```json
{"uid":547,"value":false}
```
## FRIDA tools

55
hc2mqtt
View File

@@ -17,21 +17,41 @@ from HCSocket import HCSocket, now
@click.argument("config_file")
@click.option("-h", "--mqtt_host", default="localhost")
@click.option("-p", "--mqtt_prefix", default="homeconnect/")
def hc2mqtt(config_file: str, mqtt_host: str, mqtt_prefix: str):
click.echo(f"Hello {config_file=} {mqtt_host=} {mqtt_prefix=}")
@click.option("--mqtt_port", default=1883, type=int)
@click.option("--mqtt_username")
@click.option("--mqtt_password")
@click.option("--mqtt_ssl", is_flag=True)
@click.option("--mqtt_cafile")
@click.option("--mqtt_certfile")
@click.option("--mqtt_keyfile")
def hc2mqtt(config_file: str, mqtt_host: str, mqtt_prefix: str, mqtt_port: int, mqtt_username: str,
mqtt_password: str, mqtt_ssl: bool, mqtt_cafile: str, mqtt_certfile: str, mqtt_keyfile: str):
click.echo(f"Hello {config_file=} {mqtt_host=} {mqtt_prefix=} {mqtt_port=} {mqtt_username=} {mqtt_password=} "
f"{mqtt_ssl=} {mqtt_cafile=} {mqtt_certfile=} {mqtt_keyfile=}")
with open(config_file, "r") as f:
devices = json.load(f)
client = mqtt.Client()
client.connect(host=mqtt_host, port=1883, keepalive=70)
if mqtt_username and mqtt_password:
client.username_pw_set(mqtt_username, mqtt_password)
if mqtt_ssl:
if mqtt_cafile and mqtt_certfile and mqtt_keyfile:
client.tls_set(ca_certs=mqtt_cafile, certfile=mqtt_certfile, keyfile=mqtt_keyfile, cert_reqs=ssl.CERT_REQUIRED)
else:
client.tls_set(cert_reqs=ssl.CERT_NONE)
client.connect(host=mqtt_host, port=mqtt_port, keepalive=70)
for device in devices:
mqtt_topic = mqtt_prefix + device["name"]
print(now(), f"topic: {mqtt_topic}")
thread = Thread(target=client_connect, args=(client, device, mqtt_topic))
thread.start()
client.loop_forever()
# Map their value names to easier state names
topics = {
@@ -45,19 +65,36 @@ topics = {
"FatalErrorOccured": "error",
}
def client_connect(client, device, mqtt_topic):
def on_message(client, userdata, msg):
global dev
mqtt_state = msg.payload.decode()
print(now(),f"received mqtt message {mqtt_state}")
try:
msg = json.loads(mqtt_state)
if 'uid' in msg:
dev.get("/ro/values",1,"POST",msg)
else:
raise Exception(f"Payload {msg} is not correctly formatted")
except Exception as e:
print("ERROR", e, file=sys.stderr)
global dev
host = device["host"]
state = {}
for topic in topics:
state[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:
ws = HCSocket(host, device["key"], device.get("iv",None))
dev = HCDevice(ws, device.get("features", None))
dev = HCDevice(ws, device.get("features", None), device["name"])
#ws.debug = True
ws.reconnect()
@@ -67,7 +104,7 @@ def client_connect(client, device, mqtt_topic):
if msg is None:
break
if len(msg) > 0:
print(now(), msg)
print(now(), device["name"], msg)
update = False
for topic in topics:
@@ -93,7 +130,7 @@ def client_connect(client, device, mqtt_topic):
continue
msg = json.dumps(state)
print("publish", mqtt_topic, msg)
print(now(), device["name"], f"publish to {mqtt_topic} with {msg}")
client.publish(mqtt_topic + "/state", msg)
except Exception as e: