[pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
This commit is contained in:
pre-commit-ci[bot]
2024-03-19 18:38:07 +00:00
parent d8a5e22cb9
commit 30e12f54ba
5 changed files with 705 additions and 609 deletions

View File

@@ -39,20 +39,20 @@
#
# /iz/services
import sys
import json
import re
import time
import io
import sys
import traceback
from datetime import datetime
from base64 import urlsafe_b64encode as base64url_encode
from datetime import datetime
from Crypto.Random import get_random_bytes
def now():
return datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
class HCDevice:
def __init__(self, ws, features, name):
self.ws = ws
@@ -83,56 +83,79 @@ class HCDevice:
if status:
name = status["name"]
if "values" in status \
and value_str in status["values"]:
if "values" in status and value_str in status["values"]:
value = status["values"][value_str]
# trim everything off the name except the last part
name = re.sub(r'^.*\.', '', name)
name = re.sub(r"^.*\.", "", name)
result[name] = value
return result
# Test the feature of an appliance agains a data object
def test_feature(self, data):
if 'uid' not in data:
raise Exception("{self.name}. Unable to configure appliance. UID is required.")
if "uid" not in data:
raise Exception(
"{self.name}. Unable to configure appliance. UID is required."
)
if isinstance(data['uid'], int) is False:
raise Exception("{self.name}. Unable to configure appliance. UID must be an integer.")
if isinstance(data["uid"], int) is False:
raise Exception(
"{self.name}. Unable to configure appliance. UID must be an integer."
)
if 'value' not in data:
raise Exception("{self.name}. Unable to configure appliance. Value is required.")
if "value" not in data:
raise Exception(
"{self.name}. Unable to configure appliance. Value is required."
)
# Check if the uid is present for this appliance
uid = str(data['uid'])
uid = str(data["uid"])
if uid not in self.features:
raise Exception(f"{self.name}. Unable to configure appliance. UID {uid} is not valid.")
raise Exception(
f"{self.name}. Unable to configure appliance. UID {uid} is not valid."
)
feature = self.features[uid]
# check the access level of the feature
print(now(), self.name, f"Processing feature {feature['name']} with uid {uid}")
if 'access' not in feature:
raise Exception(f"{self.name}. Unable to configure appliance. Feature {feature['name']} with uid {uid} does not have access.")
if "access" not in feature:
raise Exception(
f"{self.name}. Unable to configure appliance. Feature {feature['name']} with uid {uid} does not have access."
)
access = feature['access'].lower()
if access != 'readwrite' and access != 'writeonly':
raise Exception(f"{self.name}. Unable to configure appliance. Feature {feature['name']} with uid {uid} has got access {feature['access']}.")
access = feature["access"].lower()
if access != "readwrite" and access != "writeonly":
raise Exception(
f"{self.name}. Unable to configure appliance. Feature {feature['name']} with uid {uid} has got access {feature['access']}."
)
# check if selected list with values is allowed
if 'values' in feature:
if isinstance(data['value'], int) is False:
raise Exception(f"Unable to configure appliance. The value {data['value']} must be an integer. Allowed values are {feature['values']}.")
value = str(data['value']) # values are strings in the feature list, but always seem to be an integer. An integer must be provided
if value not in feature['values']:
raise Exception(f"{self.name}. Unable to configure appliance. Value {data['value']} is not a valid value. Allowed values are {feature['values']}.")
if "values" in feature:
if isinstance(data["value"], int) is False:
raise Exception(
f"Unable to configure appliance. The value {data['value']} must be an integer. Allowed values are {feature['values']}."
)
value = str(
data["value"]
) # values are strings in the feature list, but always seem to be an integer. An integer must be provided
if value not in feature["values"]:
raise Exception(
f"{self.name}. Unable to configure appliance. Value {data['value']} is not a valid value. Allowed values are {feature['values']}."
)
if 'min' in feature:
min = int(feature['min'])
max = int(feature['max'])
if isinstance(data['value'], int) is False or data['value'] < min or data['value'] > max:
raise Exception(f"{self.name}. Unable to configure appliance. Value {data['value']} is not a valid value. The value must be an integer in the range {min} and {max}.")
if "min" in feature:
min = int(feature["min"])
max = int(feature["max"])
if (
isinstance(data["value"], int) is False
or data["value"] < min
or data["value"] > max
):
raise Exception(
f"{self.name}. Unable to configure appliance. Value {data['value']} is not a valid value. The value must be an integer in the range {min} and {max}."
)
return True
@@ -153,14 +176,16 @@ class HCDevice:
# reply to a POST or GET message with new data
def reply(self, msg, reply):
self.ws.send({
'sID': msg["sID"],
'msgID': msg["msgID"], # same one they sent to us
'resource': msg["resource"],
'version': msg["version"],
'action': 'RESPONSE',
'data': [reply],
})
self.ws.send(
{
"sID": msg["sID"],
"msgID": msg["msgID"], # same one they sent to us
"resource": msg["resource"],
"version": msg["version"],
"action": "RESPONSE",
"data": [reply],
}
)
# send a message to the device
def get(self, resource, version=1, action="GET", data=None):
@@ -201,7 +226,7 @@ class HCDevice:
print(now(), self.name, "ERROR", msg["code"])
values = {
"error": msg["code"],
"resource": msg.get("resource", ''),
"resource": msg.get("resource", ""),
}
elif action == "POST":
if resource == "/ei/initialValues":
@@ -210,11 +235,14 @@ class HCDevice:
self.session_id = msg["sID"]
self.tx_msg_id = msg["data"][0]["edMsgID"]
self.reply(msg, {
self.reply(
msg,
{
"deviceType": "Application",
"deviceName": self.device_name,
"deviceID": self.device_id,
})
},
)
# ask the device which services it supports
self.get("/ci/services")
@@ -222,20 +250,20 @@ class HCDevice:
# 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)
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("/ci/tzInfo", version=2)
self.get("/ni/info")
#self.get("/ni/config", data={"interfaceID": 0})
# 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")
# self.get("/ro/values")
else:
print(now(), self.name, "Unknown resource", resource, file=sys.stderr)
@@ -244,8 +272,10 @@ class HCDevice:
# we could validate that this matches our machine
pass
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
pass
@@ -253,9 +283,8 @@ class HCDevice:
# we're already talking, so maybe we don't care?
pass
elif resource == "/ro/allMandatoryValues" \
or resource == "/ro/values":
if 'data' in msg:
elif resource == "/ro/allMandatoryValues" or resource == "/ro/values":
if "data" in msg:
values = self.parse_values(msg["data"])
else:
print(now(), self.name, f"received {msg}")
@@ -269,17 +298,17 @@ class HCDevice:
self.services[service["service"]] = {
"version": service["version"],
}
#print(self.name, now(), "services", self.services)
# print(self.name, now(), "services", self.services)
# we should figure out which ones to query now
# if "iz" in self.services:
# self.get("/iz/info", version=self.services["iz"]["version"])
# if "ni" in self.services:
# self.get("/ni/info", version=self.services["ni"]["version"])
# if "ei" in self.services:
# self.get("/ei/deviceReady", version=self.services["ei"]["version"], action="NOTIFY")
# if "iz" in self.services:
# self.get("/iz/info", version=self.services["iz"]["version"])
# if "ni" in self.services:
# self.get("/ni/info", version=self.services["ni"]["version"])
# if "ei" in self.services:
# self.get("/ei/deviceReady", version=self.services["ei"]["version"], action="NOTIFY")
#self.get("/if/info")
# self.get("/if/info")
else:
print(now(), self.name, "Unknown", msg)

View File

@@ -1,49 +1,53 @@
# Create a websocket that wraps a connection to a
# Bosh-Siemens Home Connect device
import socket
import ssl
import sslpsk
import websocket
import sys
import json
import re
import time
import io
import socket
import ssl
import sys
from base64 import urlsafe_b64decode as base64url
from datetime import datetime
import sslpsk
import websocket
from Crypto.Cipher import AES
from Crypto.Hash import HMAC, SHA256
from Crypto.Random import get_random_bytes
# Convience to compute an HMAC on a message
def hmac(key,msg):
def hmac(key, msg):
mac = HMAC.new(key, msg=msg, digestmod=SHA256).digest()
return mac
def now():
return datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
# Monkey patch for sslpsk in pip using the old _sslobj
def _sslobj(sock):
if (3, 5) <= sys.version_info <= (3, 7):
return sock._sslobj._sslobj
else:
return sock._sslobj
sslpsk.sslpsk._sslobj = _sslobj
class HCSocket:
def __init__(self, host, psk64, iv64=None):
self.host = host
self.psk = base64url(psk64 + '===')
self.psk = base64url(psk64 + "===")
self.debug = False
if iv64:
# an HTTP self-encrypted socket
self.http = True
self.iv = base64url(iv64 + '===')
self.enckey = hmac(self.psk, b'ENC')
self.mackey = hmac(self.psk, b'MAC')
self.iv = base64url(iv64 + "===")
self.enckey = hmac(self.psk, b"ENC")
self.mackey = hmac(self.psk, b"MAC")
self.port = 80
self.uri = "ws://" + host + ":80/homeconnect"
else:
@@ -52,7 +56,7 @@ class HCSocket:
self.uri = "wss://" + host + ":443/homeconnect"
# don't connect automatically so that debug etc can be set
#self.reconnect()
# self.reconnect()
# restore the encryption state for a fresh connection
# this is only used by the HTTP connection
@@ -70,7 +74,7 @@ class HCSocket:
hmac_msg = self.iv + direction + enc_msg
return hmac(self.mackey, hmac_msg)[0:16]
def decrypt(self,buf):
def decrypt(self, buf):
if len(buf) < 32:
print("Short message?", buf.hex(), file=sys.stderr)
return None
@@ -82,7 +86,7 @@ class HCSocket:
their_hmac = buf[-16:]
# compute the expected hmac on the encrypted message
our_hmac = self.hmac_msg(b'\x43' + self.last_rx_hmac, enc_msg)
our_hmac = self.hmac_msg(b"\x43" + self.last_rx_hmac, enc_msg)
if their_hmac != our_hmac:
print("HMAC failure", their_hmac.hex(), our_hmac.hex(), file=sys.stderr)
@@ -103,13 +107,13 @@ class HCSocket:
def encrypt(self, clear_msg):
# convert the UTF-8 string into a byte array
clear_msg = bytes(clear_msg, 'utf-8')
clear_msg = bytes(clear_msg, "utf-8")
# pad the buffer, adding an extra block if necessary
pad_len = 16 - (len(clear_msg) % 16)
if pad_len == 1:
pad_len += 16
pad = b'\x00' + get_random_bytes(pad_len-2) + bytearray([pad_len])
pad = b"\x00" + get_random_bytes(pad_len - 2) + bytearray([pad_len])
clear_msg = clear_msg + pad
@@ -119,7 +123,7 @@ class HCSocket:
# compute the hmac of the encrypted message, chaining the
# hmac of the previous message plus direction 'E'
self.last_tx_hmac = self.hmac_msg(b'\x45' + self.last_tx_hmac, enc_msg)
self.last_tx_hmac = self.hmac_msg(b"\x45" + self.last_tx_hmac, enc_msg)
# append the new hmac to the message
return enc_msg + self.last_tx_hmac
@@ -127,25 +131,26 @@ class HCSocket:
def reconnect(self):
self.reset()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((self.host,self.port))
sock.connect((self.host, self.port))
if not self.http:
sock = sslpsk.wrap_socket(
sock,
ssl_version = ssl.PROTOCOL_TLSv1_2,
ciphers = 'ECDHE-PSK-CHACHA20-POLY1305',
psk = self.psk,
ssl_version=ssl.PROTOCOL_TLSv1_2,
ciphers="ECDHE-PSK-CHACHA20-POLY1305",
psk=self.psk,
)
print(now(), "CON:", self.uri)
self.ws = websocket.WebSocket()
self.ws.connect(self.uri,
socket = sock,
origin = "",
self.ws.connect(
self.uri,
socket=sock,
origin="",
)
def send(self, msg):
buf = json.dumps(msg, separators=(',', ':') )
buf = json.dumps(msg, separators=(",", ":"))
# swap " for '
buf = re.sub("'", '"', buf)
if self.debug:

View File

@@ -7,7 +7,6 @@
#
import sys
import json
import xml.etree.ElementTree as ET
#####################
@@ -16,6 +15,7 @@ import xml.etree.ElementTree as ET
# list of UIDs
#
def parse_xml_list(codes, entries, enums):
for el in entries:
# not sure how to parse refCID and refDID
@@ -24,7 +24,7 @@ def parse_xml_list(codes, entries, enums):
if not uid in codes:
print("UID", uid, " not known!", file=sys.stderr)
data = codes[uid];
data = codes[uid]
if "uid" in codes:
print("UID", uid, " used twice?", data, file=sys.stderr)
@@ -32,31 +32,32 @@ def parse_xml_list(codes, entries, enums):
data[key] = el.attrib[key]
# clean up later
#del data["uid"]
# 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
# codes[uid] = data
def parse_machine_description(entries):
description = {}
for el in entries:
prefix, has_namespace, tag = el.tag.partition('}')
prefix, has_namespace, tag = el.tag.partition("}")
if tag != "pairableDeviceTypes":
description[tag] = el.text
return description
def xml2json(features_xml,description_xml):
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()
featuremapping = ET.fromstring(features_xml) # .getroot()
description = ET.fromstring(description_xml) # .getroot()
#####################
#
@@ -68,7 +69,7 @@ def xml2json(features_xml,description_xml):
enums = {}
# Features are all possible UIDs
for child in featuremapping[1]: #.iter('feature'):
for child in featuremapping[1]: # .iter('feature'):
uid = int(child.attrib["refUID"], 16)
name = child.text
features[uid] = {
@@ -95,8 +96,7 @@ def xml2json(features_xml,description_xml):
"values": values,
}
for i in range(4,8):
for i in range(4, 8):
parse_xml_list(features, description[i], enums)
# remove the duplicate uid field

124
hc-login
View File

@@ -3,40 +3,38 @@
# https://github.com/openid/AppAuth-Android
# A really nice walk through of how it works is:
# https://auth0.com/docs/get-started/authentication-and-authorization-flow/call-your-api-using-the-authorization-code-flow-with-pkce
import requests
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
from lxml import html
import io
import json
import re
import sys
import json
from time import time
from base64 import b64decode as base64_decode
from base64 import urlsafe_b64encode as base64url_encode
from bs4 import BeautifulSoup
from Crypto.Random import get_random_bytes
from Crypto.Hash import SHA256
from urllib.parse import parse_qs, urlencode, urlparse
from zipfile import ZipFile
from HCxml2json import xml2json
import logging
import requests
from bs4 import BeautifulSoup
from Crypto.Hash import SHA256
from Crypto.Random import get_random_bytes
from HCxml2json import xml2json
# These two lines enable debugging at httplib level (requests->urllib3->http.client)
# You will see the REQUEST, including HEADERS and DATA, and RESPONSE with HEADERS but without DATA.
# The only thing missing will be the response.body which is not logged.
import http.client as http_client
#http_client.HTTPConnection.debuglevel = 1
# http_client.HTTPConnection.debuglevel = 1
# You must initialize logging, otherwise you'll not see debug output.
#logging.basicConfig()
#logging.getLogger().setLevel(logging.DEBUG)
#requests_log = logging.getLogger("requests.packages.urllib3")
#requests_log.setLevel(logging.DEBUG)
#requests_log.propagate = True
# logging.basicConfig()
# logging.getLogger().setLevel(logging.DEBUG)
# requests_log = logging.getLogger("requests.packages.urllib3")
# requests_log.setLevel(logging.DEBUG)
# requests_log.propagate = True
def debug(*args):
print(*args, file=sys.stderr)
email = sys.argv[1]
password = sys.argv[2]
@@ -45,8 +43,8 @@ headers = {"User-Agent": "hc-login/1.0"}
session = requests.Session()
session.headers.update(headers)
base_url = 'https://api.home-connect.com/security/oauth/'
asset_url = 'https://prod.reu.rest.homeconnectegw.com/'
base_url = "https://api.home-connect.com/security/oauth/"
asset_url = "https://prod.reu.rest.homeconnectegw.com/"
##############3
#
@@ -55,32 +53,49 @@ asset_url = 'https://prod.reu.rest.homeconnectegw.com/'
# even after the singlekey detour.
#
# The app_id and scope are hardcoded in the application
app_id = '9B75AC9EC512F36C84256AC47D813E2C1DD0D6520DF774B020E1E6E2EB29B1F3'
scope = ["ReadAccount","Settings","IdentifyAppliance","Control","DeleteAppliance","WriteAppliance","ReadOrigApi","Monitor","WriteOrigApi","Images"]
scope = ["ReadOrigApi",]
app_id = "9B75AC9EC512F36C84256AC47D813E2C1DD0D6520DF774B020E1E6E2EB29B1F3"
scope = [
"ReadAccount",
"Settings",
"IdentifyAppliance",
"Control",
"DeleteAppliance",
"WriteAppliance",
"ReadOrigApi",
"Monitor",
"WriteOrigApi",
"Images",
]
scope = [
"ReadOrigApi",
]
def b64(b):
return re.sub(r'=', '', base64url_encode(b).decode('UTF-8'))
return re.sub(r"=", "", base64url_encode(b).decode("UTF-8"))
def b64random(num):
return b64(base64url_encode(get_random_bytes(num)))
verifier = b64(get_random_bytes(32))
login_query = {
"response_type": "code",
"prompt": "login",
"code_challenge": b64(SHA256.new(verifier.encode('UTF-8')).digest()),
"code_challenge": b64(SHA256.new(verifier.encode("UTF-8")).digest()),
"code_challenge_method": "S256",
"client_id": app_id,
"scope": ' '.join(scope),
"scope": " ".join(scope),
"nonce": b64random(16),
"state": b64random(16),
"redirect_uri": 'hcauth://auth/prod',
"redirect_target": 'icore',
"redirect_uri": "hcauth://auth/prod",
"redirect_target": "icore",
}
loginpage_url = base_url + 'authorize?' + urlencode(login_query)
token_url = base_url + 'token'
loginpage_url = base_url + "authorize?" + urlencode(login_query)
token_url = base_url + "token"
debug(f"{loginpage_url=}")
r = session.get(loginpage_url)
@@ -102,8 +117,8 @@ debug("--------")
# now that we have a session id, contact the
# single key host to start the new login flow
singlekey_host = 'https://singlekey-id.com'
login_url = singlekey_host + '/auth/en-us/log-in/'
singlekey_host = "https://singlekey-id.com"
login_url = singlekey_host + "/auth/en-us/log-in/"
preauth_url = singlekey_host + "/auth/connect/authorize"
preauth_query = {
@@ -145,19 +160,38 @@ session.headers.update(headers)
debug("--------")
soup = BeautifulSoup(r.text, 'html.parser')
requestVerificationToken = soup.find('input', {'name': '__RequestVerificationToken'}).get('value')
r = session.post(preauth_url, data={"UserIdentifierInput.EmailInput.StringValue": email, "__RequestVerificationToken": requestVerificationToken }, allow_redirects=False)
soup = BeautifulSoup(r.text, "html.parser")
requestVerificationToken = soup.find(
"input", {"name": "__RequestVerificationToken"}
).get("value")
r = session.post(
preauth_url,
data={
"UserIdentifierInput.EmailInput.StringValue": email,
"__RequestVerificationToken": requestVerificationToken,
},
allow_redirects=False,
)
password_url = r.headers['location']
password_url = r.headers["location"]
if not bool(urlparse(password_url).netloc):
password_url = singlekey_host + password_url
r = session.get(password_url, allow_redirects=False)
soup = BeautifulSoup(r.text, 'html.parser')
requestVerificationToken = soup.find('input', {'name': '__RequestVerificationToken'}).get('value')
soup = BeautifulSoup(r.text, "html.parser")
requestVerificationToken = soup.find(
"input", {"name": "__RequestVerificationToken"}
).get("value")
r = session.post(password_url, data={"Password": password, "RememberMe": "false", "__RequestVerificationToken": requestVerificationToken }, allow_redirects=False)
r = session.post(
password_url,
data={
"Password": password,
"RememberMe": "false",
"__RequestVerificationToken": requestVerificationToken,
},
allow_redirects=False,
)
if return_url.startswith("/"):
return_url = singlekey_host + return_url
@@ -182,8 +216,8 @@ grant_type = query.get("grant_type")[0] # "authorization_code"
debug(f"{code=} {grant_type=} {state=}")
auth_url = base_url + 'login'
token_url = base_url + 'token'
auth_url = base_url + "login"
token_url = base_url + "token"
token_fields = {
"grant_type": grant_type,
@@ -201,7 +235,7 @@ if r.status_code != requests.codes.ok:
print(r.headers, r.text)
exit(1)
debug('--------- got token page ----------')
debug("--------- got token page ----------")
token = json.loads(r.text)["access_token"]
debug(f"Received access {token=}")
@@ -217,7 +251,7 @@ if r.status_code != requests.codes.ok:
print(r.headers, r.text)
exit(1)
#print(r.text)
# print(r.text)
account = json.loads(r.text)
configs = []
@@ -236,7 +270,7 @@ for app in account["data"]["homeAppliances"]:
if "tls" in app:
# fancy machine with TLS support
config["host"] =app_brand + "-" + app_type + "-" + app_id
config["host"] = app_brand + "-" + app_type + "-" + app_id
config["key"] = app["tls"]["key"]
else:
# less fancy machine with HTTP support
@@ -258,7 +292,7 @@ for app in account["data"]["homeAppliances"]:
with open(app_id + ".zip", "wb") as f:
f.write(content)
z = ZipFile(io.BytesIO(content))
#print(z.infolist())
# print(z.infolist())
features = z.open(app_id + "_FeatureMapping.xml").read()
description = z.open(app_id + "_DeviceDescription.xml").read()

60
hc2mqtt
View File

@@ -2,9 +2,9 @@
# Contact Bosh-Siemens Home Connect devices
# and connect their messages to the mqtt server
import json
import ssl
import sys
import time
import ssl
from threading import Thread
import click
@@ -14,6 +14,7 @@ import paho.mqtt.client as mqtt
from HCDevice import HCDevice
from HCSocket import HCSocket, now
@click.command()
@click.option("-d", "--devices_file", default="config/devices.json")
@click.option("-h", "--mqtt_host", default="localhost")
@@ -27,11 +28,23 @@ from HCSocket import HCSocket, now
@click.option("--mqtt_keyfile")
@click.option("--mqtt_clientname", default="hcpy")
@click_config_file.configuration_option()
def hc2mqtt(devices_file: str, mqtt_host: str, mqtt_prefix: str, mqtt_port: int, mqtt_username: str,
mqtt_password: str, mqtt_ssl: bool, mqtt_cafile: str, mqtt_certfile: str, mqtt_keyfile: str, mqtt_clientname: str):
click.echo(f"Hello {devices_file=} {mqtt_host=} {mqtt_prefix=} {mqtt_port=} {mqtt_username=} {mqtt_password=} "
f"{mqtt_ssl=} {mqtt_cafile=} {mqtt_certfile=} {mqtt_keyfile=} {mqtt_clientname=}")
def hc2mqtt(
devices_file: str,
mqtt_host: str,
mqtt_prefix: str,
mqtt_port: int,
mqtt_username: str,
mqtt_password: str,
mqtt_ssl: bool,
mqtt_cafile: str,
mqtt_certfile: str,
mqtt_keyfile: str,
mqtt_clientname: str,
):
click.echo(
f"Hello {devices_file=} {mqtt_host=} {mqtt_prefix=} {mqtt_port=} {mqtt_username=} {mqtt_password=} "
f"{mqtt_ssl=} {mqtt_cafile=} {mqtt_certfile=} {mqtt_keyfile=} {mqtt_clientname=}"
)
with open(devices_file, "r") as f:
devices = json.load(f)
@@ -43,7 +56,12 @@ def hc2mqtt(devices_file: str, mqtt_host: str, mqtt_prefix: str, mqtt_port: int,
if mqtt_ssl:
if mqtt_cafile and mqtt_certfile and mqtt_keyfile:
client.tls_set(ca_certs=mqtt_cafile, certfile=mqtt_certfile, keyfile=mqtt_keyfile, cert_reqs=ssl.CERT_REQUIRED)
client.tls_set(
ca_certs=mqtt_cafile,
certfile=mqtt_certfile,
keyfile=mqtt_keyfile,
cert_reqs=ssl.CERT_REQUIRED,
)
else:
client.tls_set(cert_reqs=ssl.CERT_NONE)
@@ -57,6 +75,7 @@ def hc2mqtt(devices_file: str, mqtt_host: str, mqtt_prefix: str, mqtt_port: int,
client.loop_forever()
# Map their value names to easier state names
topics = {
"InternalError": "Error",
@@ -65,15 +84,16 @@ topics = {
global dev
dev = {}
def client_connect(client, device, mqtt_topic):
def on_message(client, userdata, msg):
print(msg.topic)
mqtt_state = msg.payload.decode()
mqtt_topic = msg.topic.split('/')
mqtt_topic = msg.topic.split("/")
print(now(), f"received mqtt message {mqtt_state}")
try:
msg = json.loads(mqtt_state)
if 'uid' in msg:
if "uid" in msg:
dev[mqtt_topic[-2]].get("/ro/values", 1, "POST", msg)
else:
raise Exception(f"Payload {msg} is not correctly formatted")
@@ -84,14 +104,19 @@ def client_connect(client, device, mqtt_topic):
device_topics = topics
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(".")
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
device_topics[value] = name[
-1
] # sometimes the returned key is a digit, making translation possible
state = {}
for topic in device_topics:
if not topic.isdigit(): #We only want the named topics
if not topic.isdigit(): # We only want the named topics
state[device_topics[topic]] = None
mqtt_set_topic = mqtt_topic + "/set"
@@ -102,10 +127,12 @@ def client_connect(client, device, mqtt_topic):
while True:
try:
print(now(), device["name"], f"connecting to {host}")
ws = HCSocket(host, device["key"], device.get("iv",None))
dev[device["name"]] = HCDevice(ws, device.get("features", None), device["name"])
ws = HCSocket(host, device["key"], device.get("iv", None))
dev[device["name"]] = HCDevice(
ws, device.get("features", None), device["name"]
)
#ws.debug = True
# ws.debug = True
ws.reconnect()
while True:
@@ -142,5 +169,6 @@ def client_connect(client, device, mqtt_topic):
time.sleep(5)
if __name__ == "__main__":
hc2mqtt()