diff --git a/HCDevice.py b/HCDevice.py index ad27f8e..03912a8 100755 --- a/HCDevice.py +++ b/HCDevice.py @@ -54,26 +54,17 @@ def now(): return datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") class HCDevice: - def __init__(self, ws): + def __init__(self, ws, features): self.ws = ws - self.machine = None + self.features = features self.session_id = None self.tx_msg_id = None self.device_name = "hcpy" self.device_id = "0badcafe" 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): - if not self.machine: + if not self.features: return values result = {} @@ -86,8 +77,8 @@ class HCDevice: name = uid status = None - if uid in self.machine["features"]: - status = self.machine["features"][uid] + if uid in self.features: + status = self.features[uid] if status: name = status["name"] @@ -199,11 +190,8 @@ class HCDevice: elif action == "RESPONSE" or action == "NOTIFY": if resource == "/iz/info" or resource == "/ci/info": - # see if we have a device file for this model - if not "data" in msg: - return values - values = msg["data"][0] - self.load_description(values["vib"]) + # we could validate that this matches our machine + pass elif resource == "/ro/descriptionChange" \ or resource == "/ro/allDescriptionChanges": diff --git a/HCxml2json.py b/HCxml2json.py new file mode 100755 index 0000000..80fad80 --- /dev/null +++ b/HCxml2json.py @@ -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, + } diff --git a/hc-login b/hc-login index bf66814..6b3e709 100755 --- a/hc-login +++ b/hc-login @@ -16,6 +16,7 @@ from base64 import urlsafe_b64encode as base64url_encode from Crypto.Random import get_random_bytes from Crypto.Hash import SHA256 from zipfile import ZipFile +from HCxml2json import xml2json email = sys.argv[1] password = sys.argv[2] @@ -121,6 +122,7 @@ if r.status_code != requests.codes.ok: #print(r.text) token = json.loads(r.text)["access_token"] +print("Received access token", file=sys.stderr) headers = { "Authorization": "Bearer " + token, } @@ -159,7 +161,7 @@ for app in account["data"]["homeAppliances"]: # Fetch the XML zip file for this device 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) if r.status_code != requests.codes.ok: 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 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)) diff --git a/hc2mqtt b/hc2mqtt index b1d52e6..5125892 100755 --- a/hc2mqtt +++ b/hc2mqtt @@ -11,22 +11,18 @@ from HCSocket import HCSocket, now from HCDevice import HCDevice 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/" client = mqtt.Client() 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 topics = { @@ -42,8 +38,8 @@ topics = { -def client_connect(device_name, device): - mqtt_topic = mqtt_prefix + device_name +def client_connect(device): + mqtt_topic = mqtt_prefix + device["name"] host = device["host"] state = {} @@ -52,8 +48,8 @@ def client_connect(device_name, device): while True: try: - ws = HCSocket(host, device["psk64"], device["iv64"]) - dev = HCDevice(ws) + ws = HCSocket(host, device["key"], device.get("iv",None)) + dev = HCDevice(ws, device.get("features", None)) #ws.debug = True ws.reconnect() @@ -98,6 +94,6 @@ def client_connect(device_name, device): time.sleep(5) for device in devices: - thread = Thread(target=client_connect, args=(device, devices[device])) + thread = Thread(target=client_connect, args=(device,)) thread.start() diff --git a/xml/hcpxml2json b/xml/hcpxml2json deleted file mode 100755 index 4313789..0000000 --- a/xml/hcpxml2json +++ /dev/null @@ -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))