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")
|
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
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.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
30
hc2mqtt
@@ -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()
|
||||||
|
|
||||||
|
|||||||
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