hc-login and hc2mqtt work together to allow device monitoring

This commit is contained in:
Trammell Hudson
2022-02-19 19:00:29 +01:00
parent 8f80f43f05
commit b852cfd2b8
5 changed files with 140 additions and 148 deletions

View File

@@ -54,26 +54,17 @@ def now():
return datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") return datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
class HCDevice: class HCDevice:
def __init__(self, ws): def __init__(self, ws, features):
self.ws = ws self.ws = ws
self.machine = None self.features = features
self.session_id = None self.session_id = None
self.tx_msg_id = None self.tx_msg_id = None
self.device_name = "hcpy" self.device_name = "hcpy"
self.device_id = "0badcafe" self.device_id = "0badcafe"
self.debug = False self.debug = False
def load_description(self, device_type):
json_file = "xml/" + device_type + ".json"
try:
with io.open(json_file, "r") as fp:
self.machine = json.load(fp)
print(now(), json_file + ": parsed machine description")
except Exception as e:
print(now(), json_file + ": unable to load machine description", e)
def parse_values(self, values): def parse_values(self, values):
if not self.machine: if not self.features:
return values return values
result = {} result = {}
@@ -86,8 +77,8 @@ class HCDevice:
name = uid name = uid
status = None status = None
if uid in self.machine["features"]: if uid in self.features:
status = self.machine["features"][uid] status = self.features[uid]
if status: if status:
name = status["name"] name = status["name"]
@@ -199,11 +190,8 @@ class HCDevice:
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":
# see if we have a device file for this model # we could validate that this matches our machine
if not "data" in msg: pass
return values
values = msg["data"][0]
self.load_description(values["vib"])
elif resource == "/ro/descriptionChange" \ elif resource == "/ro/descriptionChange" \
or resource == "/ro/allDescriptionChanges": or resource == "/ro/allDescriptionChanges":

110
HCxml2json.py Executable file
View File

@@ -0,0 +1,110 @@
#!/usr/bin/env python3
# Convert the featuremap and devicedescription XML files into a single JSON
# this collapses the XML entities and duplicates some things, but makes for
# easier parsing later
#
# Program groups are ignored for now
#
import sys
import json
import xml.etree.ElementTree as ET
#####################
#
# Parse the description file and collapse everything into a single
# list of UIDs
#
def parse_xml_list(codes, entries, enums):
for el in entries:
# not sure how to parse refCID and refDID
uid = int(el.attrib["uid"], 16)
if not uid in codes:
print("UID", uid, " not known!", file=sys.stderr)
data = codes[uid];
if "uid" in codes:
print("UID", uid, " used twice?", data, file=sys.stderr)
for key in el.attrib:
data[key] = el.attrib[key]
# clean up later
#del data["uid"]
if "enumerationType" in el.attrib:
del data["enumerationType"]
enum_id = int(el.attrib["enumerationType"], 16)
data["values"] = enums[enum_id]["values"]
#codes[uid] = data
def parse_machine_description(entries):
description = {}
for el in entries:
prefix, has_namespace, tag = el.tag.partition('}')
if tag != "pairableDeviceTypes":
description[tag] = el.text
return description
def xml2json(features_xml,description_xml):
# the feature file has features, errors, and enums
# for now the ordering is hardcoded
featuremapping = ET.fromstring(features_xml) #.getroot()
description = ET.fromstring(description_xml) #.getroot()
#####################
#
# Parse the feature file
#
features = {}
errors = {}
enums = {}
# Features are all possible UIDs
for child in featuremapping[1]: #.iter('feature'):
uid = int(child.attrib["refUID"], 16)
name = child.text
features[uid] = {
"name": name,
}
# Errors
for child in featuremapping[2]:
uid = int(child.attrib["refEID"], 16)
name = child.text
errors[uid] = name
# Enums
for child in featuremapping[3]:
uid = int(child.attrib["refENID"], 16)
enum_name = child.attrib["enumKey"]
values = {}
for v in child:
value = int(v.attrib["refValue"])
name = v.text
values[value] = name
enums[uid] = {
"name": enum_name,
"values": values,
}
for i in range(4,8):
parse_xml_list(features, description[i], enums)
# remove the duplicate uid field
for uid in features:
if "uid" in features[uid]:
del features[uid]["uid"]
return {
"description": parse_machine_description(description[3]),
"features": features,
}

View File

@@ -16,6 +16,7 @@ from base64 import urlsafe_b64encode as base64url_encode
from Crypto.Random import get_random_bytes from Crypto.Random import get_random_bytes
from Crypto.Hash import SHA256 from Crypto.Hash import SHA256
from zipfile import ZipFile from zipfile import ZipFile
from HCxml2json import xml2json
email = sys.argv[1] email = sys.argv[1]
password = sys.argv[2] password = sys.argv[2]
@@ -121,6 +122,7 @@ if r.status_code != requests.codes.ok:
#print(r.text) #print(r.text)
token = json.loads(r.text)["access_token"] token = json.loads(r.text)["access_token"]
print("Received access token", file=sys.stderr)
headers = { headers = {
"Authorization": "Bearer " + token, "Authorization": "Bearer " + token,
} }
@@ -159,7 +161,7 @@ for app in account["data"]["homeAppliances"]:
# Fetch the XML zip file for this device # Fetch the XML zip file for this device
app_url = asset_url + "api/iddf/v1/iddf/" + app_id app_url = asset_url + "api/iddf/v1/iddf/" + app_id
print("fetching", app_url) print("fetching", app_url, file=sys.stderr)
r = requests.get(app_url, headers=headers) r = requests.get(app_url, headers=headers)
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
print(app_id, ": unable to fetch machine description?") print(app_id, ": unable to fetch machine description?")
@@ -167,7 +169,12 @@ for app in account["data"]["homeAppliances"]:
# we now have a zip file with XML, let's unpack them # we now have a zip file with XML, let's unpack them
z = ZipFile(io.BytesIO(r.content)) z = ZipFile(io.BytesIO(r.content))
print(z.infolist()) #print(z.infolist())
features = z.open(app_id + "_FeatureMapping.xml").read()
description = z.open(app_id + "_DeviceDescription.xml").read()
machine = xml2json(features, description)
config["description"] = machine["description"]
config["features"] = machine["features"]
print(configs) print(json.dumps(configs, indent=4))

30
hc2mqtt
View File

@@ -11,22 +11,18 @@ from HCSocket import HCSocket, now
from HCDevice import HCDevice from HCDevice import HCDevice
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
if len(sys.argv) < 2:
print("Usage: hc2mqtt config.json", file=sys.stderr)
exit(1)
with open(sys.argv[1], "r") as f:
config_json = f.read()
devices = json.loads(config_json)
# these should probably be in the config too
mqtt_prefix = "homeconnect/" mqtt_prefix = "homeconnect/"
client = mqtt.Client() client = mqtt.Client()
client.connect("dashboard", 1883, 70) client.connect("dashboard", 1883, 70)
devices = {
'clothes': {
"host": '10.1.0.145',
"psk64": 'KlRQQyG8AkEfRFPr0v7vultz96zcal5lxj2fAc2ohaY',
"iv64": 'tTUvqcsBldtkhHvDwE2DpQ',
},
'dishwasher': {
"host": "10.1.0.133",
"psk64": "Dsgf2MZJ-ti85_00M1QT1HP5LgH82CaASYlMGdcuzcs=",
"iv64": None, # no iv == https
},
}
# Map their value names to easier state names # Map their value names to easier state names
topics = { topics = {
@@ -42,8 +38,8 @@ topics = {
def client_connect(device_name, device): def client_connect(device):
mqtt_topic = mqtt_prefix + device_name mqtt_topic = mqtt_prefix + device["name"]
host = device["host"] host = device["host"]
state = {} state = {}
@@ -52,8 +48,8 @@ def client_connect(device_name, device):
while True: while True:
try: try:
ws = HCSocket(host, device["psk64"], device["iv64"]) ws = HCSocket(host, device["key"], device.get("iv",None))
dev = HCDevice(ws) dev = HCDevice(ws, device.get("features", None))
#ws.debug = True #ws.debug = True
ws.reconnect() ws.reconnect()
@@ -98,6 +94,6 @@ def client_connect(device_name, device):
time.sleep(5) time.sleep(5)
for device in devices: for device in devices:
thread = Thread(target=client_connect, args=(device, devices[device])) thread = Thread(target=client_connect, args=(device,))
thread.start() thread.start()

View File

@@ -1,109 +0,0 @@
#!/usr/bin/env python3
# Convert the featuremap and devicedescription XML files into a single JSON
# this collapses the XML entities and duplicates some things, but makes for
# easier parsing later
#
# Program groups are ignored for now
#
import sys
import xml.etree.ElementTree as ET
import json
# the feature file has features, errors, and enums
# for now the ordering is hardcoded
featuremapping = ET.parse(sys.argv[1]).getroot()
description = ET.parse(sys.argv[2]).getroot()
#####################
#
# Parse the feature file
#
features = {}
errors = {}
enums = {}
# Features are all possible UIDs
for child in featuremapping[1]: #.iter('feature'):
uid = int(child.attrib["refUID"], 16)
name = child.text
features[uid] = {
"name": name,
}
# Errors
for child in featuremapping[2]:
uid = int(child.attrib["refEID"], 16)
name = child.text
errors[uid] = name
# Enums
for child in featuremapping[3]:
uid = int(child.attrib["refENID"], 16)
enum_name = child.attrib["enumKey"]
values = {}
for v in child:
value = int(v.attrib["refValue"])
name = v.text
values[value] = name
enums[uid] = {
"name": enum_name,
"values": values,
}
#####################
#
# Parse the description file and collapse everything into a single
# list of UIDs
#
def parse_xml_list(codes, entries):
for el in entries:
# not sure how to parse refCID and refDID
uid = int(el.attrib["uid"], 16)
if not uid in codes:
print("UID", uid, " not known!", file=sys.stderr)
data = codes[uid];
if "uid" in codes:
print("UID", uid, " used twice?", data, file=sys.stderr)
for key in el.attrib:
data[key] = el.attrib[key]
# clean up later
#del data["uid"]
if "enumerationType" in el.attrib:
del data["enumerationType"]
enum_id = int(el.attrib["enumerationType"], 16)
data["values"] = enums[enum_id]["values"]
#codes[uid] = data
def parse_machine_description(entries):
description = {}
for el in entries:
prefix, has_namespace, tag = el.tag.partition('}')
if tag != "pairableDeviceTypes":
description[tag] = el.text
return description
for i in range(4,8):
parse_xml_list(features, description[i])
# remove the duplicate uid field
for uid in features:
if "uid" in features[uid]:
del features[uid]["uid"]
machine = {
"description": parse_machine_description(description[3]),
"features": features,
}
print(json.dumps(machine, indent=4))