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

|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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>
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user