hc-login and hc2mqtt work together to allow device monitoring
This commit is contained in:
26
HCDevice.py
26
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":
|
||||
|
||||
110
HCxml2json.py
Executable file
110
HCxml2json.py
Executable 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,
|
||||
}
|
||||
13
hc-login
13
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))
|
||||
|
||||
30
hc2mqtt
30
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()
|
||||
|
||||
|
||||
109
xml/hcpxml2json
109
xml/hcpxml2json
@@ -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))
|
||||
Reference in New Issue
Block a user