Merge branch 'main' into mqtt_lwt

This commit is contained in:
Meatballs1
2024-03-21 13:23:55 +00:00
3 changed files with 86 additions and 282 deletions

View File

@@ -93,104 +93,107 @@ class HCDevice:
return result
# Based on PR submitted https://github.com/Skons/hcpy/pull/1
def test_program_data(self, data):
if "program" not in data:
raise TypeError("Message data invalid, no program specified.")
def test_program_data(self, data_array):
for data in data_array:
if "program" not in data:
raise TypeError("Message data invalid, no program specified.")
if isinstance(data["program"], int) is False:
raise TypeError("Message data invalid, UID in 'program' must be an integer.")
if isinstance(data["program"], int) is False:
raise TypeError("Message data invalid, UID in 'program' must be an integer.")
# devices.json stores UID as string
uid = str(data["program"])
if uid not in self.features:
raise ValueError(
f"Unable to configure appliance. Program UID {uid} is not valid"
" for this device."
)
# devices.json stores UID as string
uid = str(data["program"])
if uid not in self.features:
raise ValueError(
f"Unable to configure appliance. Program UID {uid} is not valid"
" for this device."
)
feature = self.features[uid]
# Diswasher is Dishcare.Dishwasher.Program.{name}
# Hood is Cooking.Common.Program.{name}
# May also be in the format BSH.Common.Program.Favorite.001
if ".Program." not in feature["name"]:
raise ValueError(
f"Unable to configure appliance. Program UID {uid} is not a valid"
f" program - {feature['name']}."
)
feature = self.features[uid]
# Diswasher is Dishcare.Dishwasher.Program.{name}
# Hood is Cooking.Common.Program.{name}
# May also be in the format BSH.Common.Program.Favorite.001
if ".Program." not in feature["name"]:
raise ValueError(
f"Unable to configure appliance. Program UID {uid} is not a valid"
f" program - {feature['name']}."
)
if "options" in data:
for option in data["options"]:
option_uid = option["uid"]
if str(option_uid) not in self.features:
raise ValueError(
f"Unable to configure appliance. Option UID {option_uid} is not"
" valid for this device."
)
if "options" in data:
for option in data["options"]:
option_uid = option["uid"]
if str(option_uid) not in self.features:
raise ValueError(
f"Unable to configure appliance. Option UID {option_uid} is not"
" valid for this device."
)
# Test the feature of an appliance agains a data object
def test_feature(self, data):
if "uid" not in data:
raise Exception("Unable to configure appliance. UID is required.")
def test_feature(self, data_array):
for data in data_array:
if "uid" not in data:
raise Exception("Unable to configure appliance. UID is required.")
if isinstance(data["uid"], int) is False:
raise Exception("Unable to configure appliance. UID must be an integer.")
if isinstance(data["uid"], int) is False:
raise Exception("Unable to configure appliance. UID must be an integer.")
if "value" not in data:
raise Exception("Unable to configure appliance. Value is required.")
if "value" not in data:
raise Exception("Unable to configure appliance. Value is required.")
# Check if the uid is present for this appliance
uid = str(data["uid"])
if uid not in self.features:
raise Exception(f"Unable to configure appliance. UID {uid} is not valid.")
# Check if the uid is present for this appliance
uid = str(data["uid"])
if uid not in self.features:
raise Exception(f"Unable to configure appliance. UID {uid} is not valid.")
feature = self.features[uid]
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(
"Unable to configure appliance. "
f"Feature {feature['name']} with uid {uid} does not have access."
)
access = feature["access"].lower()
if access != "readwrite" and access != "writeonly":
raise Exception(
"Unable to configure appliance. "
f"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."
f" 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"]:
# 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(
"Unable to configure appliance. "
f"Value {data['value']} is not a valid value. "
f"Allowed values are {feature['values']}. "
f"Feature {feature['name']} with uid {uid} does not have access."
)
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
):
access = feature["access"].lower()
if access != "readwrite" and access != "writeonly":
raise Exception(
"Unable to configure appliance. "
f"Value {data['value']} is not a valid value. "
f"The value must be an integer in the range {min} and {max}."
f"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 "
f"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(
"Unable to configure appliance. "
f"Value {data['value']} is not a valid value. "
f"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(
"Unable to configure appliance. "
f"Value {data['value']} is not a valid value. "
f"The value must be an integer in the range {min} and {max}."
)
def recv(self):
try:
buf = self.ws.recv()
@@ -230,6 +233,9 @@ class HCDevice:
}
if data is not None:
if isinstance(data, list) is False:
data = [data]
if action == "POST":
if resource == "/ro/values":
# Raises exceptions on failure
@@ -238,7 +244,7 @@ class HCDevice:
# Raises exception on failure
self.test_program_data(data)
msg["data"] = [data]
msg["data"] = data
try:
self.ws.send(msg)

View File

@@ -1,79 +0,0 @@
# Home Assistant
For integration with Home Assistant, the following MQTT examples can be used to create entities:
## Coffee Machine
```yaml
- unique_id: "coffee_machine"
name: "Coffee Machine"
state_topic: "homeconnect/coffeemaker/state"
value_template: "{{ value_json.PowerState }}"
json_attributes_topic: "homeconnect/coffeemaker/state"
json_attributes_template: "{{ value_json | tojson }}"
```
## Extractor Fan
```yaml
fan:
- name: "Hood"
state_topic: "homeconnect/hood/state"
state_value_template: "{{ value_json.PowerState }}"
command_topic: "homeconnect/hood/set"
command_template: "{{ iif(value == 'On', '{\"uid\":539,\"value\":2}', '{\"uid\":539,\"value\":1}') }}"
payload_on: "On"
payload_off: "Off"
light:
- name: "Hood Work Light" # We can only turn the light on, but not off as no command_template
state_topic: "homeconnect/hood/state"
state_value_template: "{{ value_json.Lighting }}"
brightness_state_topic: "homeconnect/hood/state"
brightness_value_template: "{{ value_json.LightingBrightness }}"
brightness_command_topic: "homeconnect/hood/set"
brightness_command_template: "{{ '{\"uid\":53254,\"value\":' + value|string + '}' }}"
brightness_scale: 100
command_topic: "homeconnect/hood/set"
on_command_type: brightness
#command_template: "{{ iif(value == 'on', '{\"uid\":53253,\"value\":true}', '{\"uid\":53253,\"value\":false}') }}" WIP - MQTT doesn't allow this to be configured
payload_on: true
payload_off: false
```
## Refrigerator/Freezers
```yaml
binary_sensor:
- name: "Freezer Door"
state_topic: "homeconnect/freezer/state"
value_template: "{{ value_json.Freezer }}"
payload_on: "Open"
payload_off: "Closed"
device_class: door
json_attributes_topic: "homeconnect/freezer/state"
- name: "Fridge Door"
state_topic: "homeconnect/refrigerator/state"
value_template: "{{ value_json.DoorState }}" # Also Refrigerator
payload_on: "Open"
payload_off: "Closed"
device_class: door
json_attributes_topic: "homeconnect/refrigerator/state"
```
## Dishwasher
```yaml
- name: "Dishwasher"
state_topic: "homeconnect/dishwasher/state"
value_template: "{{ value_json.PowerState }}"
payload_on: "On"
payload_off: "Off"
json_attributes_topic: "homeconnect/dishwasher/state"
- name: "Dishwasher Door"
state_topic: "homeconnect/dishwasher/state"
value_template: "{{ value_json.DoorState }}"
payload_on: "Open"
payload_off: "Closed"
device_class: door
```

View File

@@ -1,123 +0,0 @@
This is for historical info; it is no longer necessary
unless you're trying to make sense of the phone application
or XML code.
## Finding the PSK and IV (no longer necessary)
![application setup screen](images/network-setup.jpg)
You will need to set the dishwasher to "`Local network only`"
in the setup application so that your phone will connect
directly to it, rather than going through the cloud services.
You'll also need a rooted Android phone running `frida-server`
and the `find-psk.frida` script. This will hook the callback
from the OpenSSL library `hcp::client_psk_callback` that is called
when OpenSSL has made a connection and now needs to establish
the PSK.
```
frida --no-pause -f com.bshg.homeconnect.android.release -U -l find-psk.frida
```
It should start the Home Connect application and eventually
print a message like:
```
psk callback hint 'HCCOM_Local_App'
psk 32 0x6ee63fb2f0
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 0e c8 1f d8 c6 49 fa d8 bc e7 fd 34 33 54 13 d4 .....I.....43T..
00000010 73 f9 2e 01 fc d8 26 80 49 89 4c 19 d7 2e cd cb s.....&.I.L.....
```
Which gives you the 32-byte PSK value to copy into the `hcpy` program.
## SSL logging
The Frida script will also dump all of the SSL traffic so that you can
see different endpoints and things. Not much is documented yet.
Note that the TX from the phone on the websocket is "masked" with an
repeating 4-byte XOR that is sent in the first part of each messages.
The script could be augmented to decode those as well.
The replies from the device are not masked so they can be read in the clear.
## Retrieving home appliance configuration
```
frida-trace -o initHomeAppliance.log -f "com.bshg.homeconnect.android.release" -U -j '*!initHomeAppliance''
```
PSK can also be found in the last section of the config as base64url encoded.
```
echo 'Dsgf2MZJ-ti85_00M1QT1HP5LgH82CaASYlMGdcuzcs"' | tr '_\-"' '/+=' | base64 -d | xxd -g1
```
The IV is also there for devices that use it. This needs better documentation.
TODO: document the other frida scripts that do `sendmsg()` and `Encrypt()` / `Decrypt()` tracing
## hcpy
![laptop in a dishwasher](images/laptop.jpg)
The `hcpy` tool can contact your device, and if the PSK is correct, it will
register for notification of events.
```
RX: {'sID': 2354590730, 'msgID': 3734589701, 'resource': '/ei/initialValues', 'version': 2, 'action': 'POST', 'data': [{'edMsgID': 3182729968}]}
TX: {"sID":2354590730,"msgID":3734589701,"resource":"/ei/initialValues","version":2,"action":"RESPONSE","data":[{"deviceType":"Application","deviceName":"py-hca","deviceID":"1234"}]}
TX: {"sID":2354590730,"msgID":3182729968,"resource":"/ci/services","version":1,"action":"GET"}
TX: {"sID":2354590730,"msgID":3182729969,"resource":"/iz/info","version":1,"action":"GET"}
TX: {"sID":2354590730,"msgID":3182729970,"resource":"/ei/deviceReady","version":2,"action":"NOTIFY"}
RX: {'sID': 2354590730, 'msgID': 3182729968, 'resource': '/ci/services', 'version': 1, 'action': 'RESPONSE', 'data': [{'service': 'ci', 'version': 3}, {'service': 'ei', 'version': 2}, {'service': 'iz', 'version': 1}, {'service': 'ni', 'version': 1}, {'service': 'ro', 'version': 1}]}
RX: {'sID': 2354590730, 'msgID': 3182729969, 'resource': '/iz/info', 'version': 1, 'action': 'RESPONSE', 'data': [{'deviceID': '....', 'eNumber': 'SX65EX56CN/11', 'brand': 'SIEMENS', 'vib': 'SX65EX56CN', 'mac': '....', 'haVersion': '1.4', 'swVersion': '3.2.10.20200911163726', 'hwVersion': '2.0.0.2', 'deviceType': 'Dishwasher', 'deviceInfo': '', 'customerIndex': '11', 'serialNumber': '....', 'fdString': '0201', 'shipSki': '....'}]}
```
## Feature UID mapping
There are other things that can be hooked in the application
to get the mappings of the `uid` to actual menu settings and
XML files of the configuration parameters.
In the `xml/` directory are some of the device descriptions
and feature maps that the app downloads from the Home Connect
servers. Note that the XML has unadorned hex, while the
websocket messages are in decimal.
For instance, when the dishwasher door is closed and then
re-opened, it sends the messages for `'uid':512`, which is 0x020F hex:
```
RX: {... 'data': [{'uid': 527, 'value': 1}]}
RX: {... 'data': [{'uid': 527, 'value': 0}]}
```
In the `xml/dishwasher-description.xml` there is a `statusList`
that says uid 0x020f is a readonly value that uses enum 0x0201:
```
<status access="read" available="true" enumerationType="0201" refCID="03" refDID="80" uid="020F"/>
```
In the `xml/dishwasher-featuremap.xml` there is a mapping of feature
reference UIDs to names:
```
<feature refUID="020F">BSH.Common.Status.DoorState</feature>
```
as well as mappings of enum ids to enum names and values:
```
<enumDescription enumKey="BSH.Common.EnumType.DoorState" refENID="0201">
<enumMember refValue="0">Open</enumMember>
<enumMember refValue="1">Closed</enumMember>
</enumDescription>
```