Merge branch 'device_details' into websocket_keepalive
This commit is contained in:
108
HCDevice.py
108
HCDevice.py
@@ -36,6 +36,7 @@
|
|||||||
# /ce/status
|
# /ce/status
|
||||||
#
|
#
|
||||||
# /ni/config
|
# /ni/config
|
||||||
|
# /ni/info
|
||||||
#
|
#
|
||||||
# /iz/services
|
# /iz/services
|
||||||
|
|
||||||
@@ -63,6 +64,9 @@ class HCDevice:
|
|||||||
self.device_id = "0badcafe"
|
self.device_id = "0badcafe"
|
||||||
self.debug = False
|
self.debug = False
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.services_initialized = False
|
||||||
|
self.services = {}
|
||||||
|
self.token = None
|
||||||
|
|
||||||
def parse_values(self, values):
|
def parse_values(self, values):
|
||||||
if not self.features:
|
if not self.features:
|
||||||
@@ -223,6 +227,15 @@ class HCDevice:
|
|||||||
|
|
||||||
# send a message to the device
|
# send a message to the device
|
||||||
def get(self, resource, version=1, action="GET", data=None):
|
def get(self, resource, version=1, action="GET", data=None):
|
||||||
|
if self.services_initialized:
|
||||||
|
resource_parts = resource.split("/")
|
||||||
|
if len(resource_parts) > 1:
|
||||||
|
service = resource.split("/")[1]
|
||||||
|
if service in self.services.keys():
|
||||||
|
version = self.services[service]["version"]
|
||||||
|
else:
|
||||||
|
print(now(), self.name, "ERROR service not known")
|
||||||
|
|
||||||
msg = {
|
msg = {
|
||||||
"sID": self.session_id,
|
"sID": self.session_id,
|
||||||
"msgID": self.tx_msg_id,
|
"msgID": self.tx_msg_id,
|
||||||
@@ -251,6 +264,47 @@ class HCDevice:
|
|||||||
print(self.name, "Failed to send", e, msg, traceback.format_exc())
|
print(self.name, "Failed to send", e, msg, traceback.format_exc())
|
||||||
self.tx_msg_id += 1
|
self.tx_msg_id += 1
|
||||||
|
|
||||||
|
def reconnect(self):
|
||||||
|
self.ws.reconnect()
|
||||||
|
# Receive initialization message /ei/initialValues
|
||||||
|
# Automatically responds in the handle_message function
|
||||||
|
self.recv()
|
||||||
|
|
||||||
|
# ask the device which services it supports
|
||||||
|
# registered devices gets pushed down too hence the loop
|
||||||
|
self.get("/ci/services")
|
||||||
|
while True:
|
||||||
|
self.recv()
|
||||||
|
if self.services_initialized:
|
||||||
|
break
|
||||||
|
|
||||||
|
# We override the version based on the registered services received above
|
||||||
|
|
||||||
|
# the clothes washer wants this, the token doesn't matter,
|
||||||
|
# although they do not handle padding characters
|
||||||
|
# they send a response, not sure how to interpet it
|
||||||
|
self.token = base64url_encode(get_random_bytes(32)).decode("UTF-8")
|
||||||
|
self.token = re.sub(r"=", "", self.token)
|
||||||
|
self.get("/ci/authentication", version=2, data={"nonce": self.token})
|
||||||
|
|
||||||
|
self.get("/ci/info") # clothes washer
|
||||||
|
self.get("/iz/info") # dish washer
|
||||||
|
|
||||||
|
# Retrieves registered clients like phone/hcpy itself
|
||||||
|
# self.get("/ci/registeredDevices")
|
||||||
|
|
||||||
|
# tzInfo all returns empty?
|
||||||
|
# self.get("/ci/tzInfo")
|
||||||
|
|
||||||
|
# We need to send deviceReady for some devices or /ni/ will come back as 403 unauth
|
||||||
|
self.get("/ei/deviceReady", version=2, action="NOTIFY")
|
||||||
|
self.get("/ni/info")
|
||||||
|
# self.get("/ni/config", data={"interfaceID": 0})
|
||||||
|
|
||||||
|
# self.get("/ro/allDescriptionChanges")
|
||||||
|
self.get("/ro/allMandatoryValues")
|
||||||
|
self.get("/ro/values")
|
||||||
|
|
||||||
def handle_message(self, buf):
|
def handle_message(self, buf):
|
||||||
msg = json.loads(buf)
|
msg = json.loads(buf)
|
||||||
if self.debug:
|
if self.debug:
|
||||||
@@ -263,7 +317,6 @@ class HCDevice:
|
|||||||
values = {}
|
values = {}
|
||||||
|
|
||||||
if "code" in msg:
|
if "code" in msg:
|
||||||
print(now(), self.name, "ERROR", msg["code"])
|
|
||||||
values = {
|
values = {
|
||||||
"error": msg["code"],
|
"error": msg["code"],
|
||||||
"resource": msg.get("resource", ""),
|
"resource": msg.get("resource", ""),
|
||||||
@@ -283,41 +336,27 @@ class HCDevice:
|
|||||||
"deviceID": self.device_id,
|
"deviceID": self.device_id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# ask the device which services it supports
|
|
||||||
self.get("/ci/services")
|
|
||||||
|
|
||||||
# the clothes washer wants this, the token doesn't matter,
|
|
||||||
# although they do not handle padding characters
|
|
||||||
# they send a response, not sure how to interpet it
|
|
||||||
token = base64url_encode(get_random_bytes(32)).decode("UTF-8")
|
|
||||||
token = re.sub(r"=", "", token)
|
|
||||||
self.get("/ci/authentication", version=2, data={"nonce": token})
|
|
||||||
|
|
||||||
self.get("/ci/info", version=2) # clothes washer
|
|
||||||
self.get("/iz/info") # dish washer
|
|
||||||
# self.get("/ci/tzInfo", version=2)
|
|
||||||
self.get("/ni/info")
|
|
||||||
# self.get("/ni/config", data={"interfaceID": 0})
|
|
||||||
self.get("/ei/deviceReady", version=2, action="NOTIFY")
|
|
||||||
self.get("/ro/allDescriptionChanges")
|
|
||||||
self.get("/ro/allDescriptionChanges")
|
|
||||||
self.get("/ro/allMandatoryValues")
|
|
||||||
# self.get("/ro/values")
|
|
||||||
else:
|
else:
|
||||||
print(now(), self.name, "Unknown resource", resource, file=sys.stderr)
|
print(now(), self.name, "Unknown resource", resource, file=sys.stderr)
|
||||||
|
|
||||||
elif action == "RESPONSE" or action == "NOTIFY":
|
elif action == "RESPONSE" or action == "NOTIFY":
|
||||||
if resource == "/iz/info" or resource == "/ci/info":
|
if resource == "/iz/info" or resource == "/ci/info":
|
||||||
# we could validate that this matches our machine
|
if "data" in msg and len(msg["data"]) > 0:
|
||||||
pass
|
# Return Device Information such as Serial Number, SW Versions, MAC Address
|
||||||
|
values = msg["data"][0]
|
||||||
|
|
||||||
elif resource == "/ro/descriptionChange" or resource == "/ro/allDescriptionChanges":
|
elif resource == "/ro/descriptionChange" or resource == "/ro/allDescriptionChanges":
|
||||||
# we asked for these but don't know have to parse yet
|
# we asked for these but don't know have to parse yet
|
||||||
pass
|
pass
|
||||||
|
|
||||||
elif resource == "/ni/info":
|
elif resource == "/ni/info":
|
||||||
# we're already talking, so maybe we don't care?
|
if "data" in msg and len(msg["data"]) > 0:
|
||||||
|
# Return Network Information/IP Address etc
|
||||||
|
values = msg["data"][0]
|
||||||
|
|
||||||
|
elif resource == "/ni/config":
|
||||||
|
# Returns some data about network interfaces e.g.
|
||||||
|
# [{'interfaceID': 0, 'automaticIPv4': True, 'automaticIPv6': True}]
|
||||||
pass
|
pass
|
||||||
|
|
||||||
elif resource == "/ro/allMandatoryValues" or resource == "/ro/values":
|
elif resource == "/ro/allMandatoryValues" or resource == "/ro/values":
|
||||||
@@ -325,19 +364,32 @@ class HCDevice:
|
|||||||
values = self.parse_values(msg["data"])
|
values = self.parse_values(msg["data"])
|
||||||
else:
|
else:
|
||||||
print(now(), self.name, f"received {msg}")
|
print(now(), self.name, f"received {msg}")
|
||||||
|
|
||||||
elif resource == "/ci/registeredDevices":
|
elif resource == "/ci/registeredDevices":
|
||||||
# we don't care
|
# This contains details of Phone/HCPY registered as clients to the device
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
elif resource == "/ci/tzInfo":
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif resource == "/ci/authentication":
|
||||||
|
if "data" in msg and len(msg["data"]) > 0:
|
||||||
|
# Grab authentication token - unsure if this is for us to use
|
||||||
|
# or to authenticate the server. Doesn't appear to be needed
|
||||||
|
self.token = msg["data"][0]["response"]
|
||||||
|
|
||||||
elif resource == "/ci/services":
|
elif resource == "/ci/services":
|
||||||
self.services = {}
|
|
||||||
for service in msg["data"]:
|
for service in msg["data"]:
|
||||||
self.services[service["service"]] = {
|
self.services[service["service"]] = {
|
||||||
"version": service["version"],
|
"version": service["version"],
|
||||||
}
|
}
|
||||||
|
self.services_initialized = True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print(now(), self.name, "Unknown", msg)
|
print(now(), self.name, "Unknown response or notify:", msg)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(now(), self.name, "Unknown message", msg)
|
||||||
|
|
||||||
# return whatever we've parsed out of it
|
# return whatever we've parsed out of it
|
||||||
return values
|
return values
|
||||||
|
|||||||
43
hc2mqtt.py
43
hc2mqtt.py
@@ -134,48 +134,28 @@ def hc2mqtt(
|
|||||||
client.loop_forever()
|
client.loop_forever()
|
||||||
|
|
||||||
|
|
||||||
# Map their value names to easier state names
|
|
||||||
topics = {
|
|
||||||
"InternalError": "Error",
|
|
||||||
"FatalErrorOccured": "Error",
|
|
||||||
}
|
|
||||||
global dev
|
global dev
|
||||||
dev = {}
|
dev = {}
|
||||||
|
|
||||||
|
|
||||||
def client_connect(client, device, mqtt_topic):
|
def client_connect(client, device, mqtt_topic):
|
||||||
host = device["host"]
|
host = device["host"]
|
||||||
device_topics = topics.copy()
|
|
||||||
|
|
||||||
for value in device["features"]:
|
|
||||||
if (
|
|
||||||
"access" in device["features"][value]
|
|
||||||
and "read" in device["features"][value]["access"].lower()
|
|
||||||
):
|
|
||||||
name = device["features"][value]["name"].split(".")
|
|
||||||
device_topics[name[-1]] = name[-1]
|
|
||||||
device_topics[value] = name[
|
|
||||||
-1
|
|
||||||
] # sometimes the returned key is a digit, making translation possible
|
|
||||||
|
|
||||||
state = {}
|
state = {}
|
||||||
for topic in device_topics:
|
|
||||||
if not topic.isdigit(): # We only want the named topics
|
|
||||||
state[device_topics[topic]] = None
|
|
||||||
|
|
||||||
mqtt_set_topic = mqtt_topic + "/set"
|
mqtt_set_topic = mqtt_topic + "/set"
|
||||||
print(now(), device["name"], f"set topic: {mqtt_set_topic}")
|
print(now(), device["name"], f"set topic: {mqtt_set_topic}")
|
||||||
client.subscribe(mqtt_set_topic)
|
client.subscribe(mqtt_set_topic)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
time.sleep(20)
|
time.sleep(3)
|
||||||
try:
|
try:
|
||||||
print(now(), device["name"], f"connecting to {host}")
|
print(now(), device["name"], f"connecting to {host}")
|
||||||
ws = HCSocket(host, device["key"], device.get("iv", None))
|
ws = HCSocket(host, device["key"], device.get("iv", None))
|
||||||
dev[device["name"]] = HCDevice(ws, device.get("features", None), device["name"])
|
dev[device["name"]] = HCDevice(ws, device.get("features", None), device["name"])
|
||||||
|
|
||||||
# ws.debug = True
|
# ws.debug = True
|
||||||
ws.reconnect()
|
dev[device["name"]].reconnect()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
if client.is_connected():
|
if client.is_connected():
|
||||||
@@ -191,13 +171,18 @@ def client_connect(client, device, mqtt_topic):
|
|||||||
print(now(), device["name"], msg)
|
print(now(), device["name"], msg)
|
||||||
|
|
||||||
update = False
|
update = False
|
||||||
for topic in device_topics:
|
for key in msg.keys():
|
||||||
value = msg.get(topic, None)
|
val = msg.get(key, None)
|
||||||
if value is None:
|
if key in state:
|
||||||
|
# Override existing values with None if they have changed
|
||||||
|
state[key] = val
|
||||||
|
update = True
|
||||||
|
else:
|
||||||
|
# Dont store None values until something useful is populated?
|
||||||
|
if val is None:
|
||||||
continue
|
continue
|
||||||
|
else:
|
||||||
new_topic = device_topics[topic]
|
state[key] = val
|
||||||
state[new_topic] = value
|
|
||||||
update = True
|
update = True
|
||||||
|
|
||||||
if not update:
|
if not update:
|
||||||
@@ -218,7 +203,7 @@ def client_connect(client, device, mqtt_topic):
|
|||||||
print(now(), device["name"], "ERROR", e, file=sys.stderr)
|
print(now(), device["name"], "ERROR", e, file=sys.stderr)
|
||||||
client.publish(f"{mqtt_topic}/LWT", "offline", retain=True)
|
client.publish(f"{mqtt_topic}/LWT", "offline", retain=True)
|
||||||
|
|
||||||
time.sleep(40)
|
time.sleep(57)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user