Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow for posting to appliance #26

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"githubPullRequests.ignoredPullRequestBranches": [
"main"
]
}
82 changes: 68 additions & 14 deletions HCDevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,22 @@ def now():
return datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")

class HCDevice:
def __init__(self, ws, features):
def __init__(self, ws, features, name):
self.ws = ws
self.features = features
self.session_id = None
self.tx_msg_id = None
self.device_name = "hcpy"
self.device_id = "0badcafe"
self.debug = False
self.name = name

def parse_values(self, values):
if not self.features:
return values

result = {}

for msg in values:
uid = str(msg["uid"])
value = msg["value"]
Expand All @@ -92,19 +93,62 @@ def parse_values(self, values):

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 isinstance(data['uid'], int) == 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.")

# Check if the uid is present for this appliance
uid = str(data['uid'])
if uid not in self.features:
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.")

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) == 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['min'])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be 'max'

if isinstance(data['value'], int) == 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

def recv(self):
try:
buf = self.ws.recv()
if buf is None:
return None
except Exception as e:
print("receive error", e, traceback.format_exc())
print(self.name, "receive error", e, traceback.format_exc())
return None

try:
return self.handle_message(buf)
except Exception as e:
print("error handling msg", e, buf, traceback.format_exc())
print(self.name, "error handling msg", e, buf, traceback.format_exc())
return None

# reply to a POST or GET message with new data
Expand All @@ -129,25 +173,32 @@ def get(self, resource, version=1, action="GET", data=None):
}

if data is not None:
msg["data"] = [data]
if action == "POST":
if self.test_feature(data) != True:
return
msg["data"] = [data]
else:
msg["data"] = [data]

self.ws.send(msg)
try:
self.ws.send(msg)
except Exception as e:
print(self.name, "Failed to send", e, msg, traceback.format_exc())
self.tx_msg_id += 1

def handle_message(self, buf):
msg = json.loads(buf)
if self.debug:
print(now(), "RX:", msg)
print(now(), self.name, "RX:", msg)
sys.stdout.flush()


resource = msg["resource"]
action = msg["action"]

values = {}

if "code" in msg:
#print(now(), "ERROR", msg["code"])
print(now(), self.name, "ERROR", msg["code"])
values = {
"error": msg["code"],
"resource": msg.get("resource", ''),
Expand Down Expand Up @@ -186,7 +237,7 @@ def handle_message(self, buf):
self.get("/ro/allMandatoryValues")
#self.get("/ro/values")
else:
print(now(), "Unknown resource", resource, file=sys.stderr)
print(now(), self.name, "Unknown resource", resource, file=sys.stderr)

elif action == "RESPONSE" or action == "NOTIFY":
if resource == "/iz/info" or resource == "/ci/info":
Expand All @@ -204,7 +255,10 @@ def handle_message(self, buf):

elif resource == "/ro/allMandatoryValues" \
or resource == "/ro/values":
values = self.parse_values(msg["data"])
if 'data' in msg:
values = self.parse_values(msg["data"])
else:
print(now(), self.name, f"received {msg}")
elif resource == "/ci/registeredDevices":
# we don't care
pass
Expand All @@ -215,7 +269,7 @@ def handle_message(self, buf):
self.services[service["service"]] = {
"version": service["version"],
}
#print(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:
Expand All @@ -227,8 +281,8 @@ def handle_message(self, buf):

#self.get("/if/info")

else:
print(now(), "Unknown", msg)
else:
print(now(), self.name, "Unknown", msg)

# return whatever we've parsed out of it
return values
95 changes: 62 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,22 +71,7 @@ library.

Example message published to `homeconnect/dishwasher`:

```
{
"state": "Run",
"door": "Closed",
"remaining": "2:49",
"power": true,
"lowwaterpressure": false,
"aquastop": false,
"error": false,
"remainingseconds": 10140
}
```

<details>
<summary>Full state information</summary>

```
{
'AllowBackendConnection': False,
Expand Down Expand Up @@ -154,7 +139,6 @@ Example message published to `homeconnect/dishwasher`:
```
</details>


### Clothes washer

![laptop in a clothes washer](images/clotheswasher.jpg)
Expand All @@ -167,21 +151,7 @@ binary data over the websocket (type 0x82).

Example message published to `homeconnect/washer`:

```
{
"state": "Ready",
"door": "Closed",
"remaining": "3:48",
"power": true,
"lowwaterpressure": false,
"aquastop": false,
"error": false,
"remainingseconds": 13680
}
```

<details>
<summary>Full state information</summary>

```
{
Expand Down Expand Up @@ -252,11 +222,9 @@ Example message published to `homeconnect/washer`:

![Image of the coffee machine from the Siemens website](images/coffee.jpg)

The coffee machine needs a better mapping to MQTT messages.
Example message published to `homeconnect/coffeemaker`:

<details>
<summary>Full state information</summary>

```
{
'LastSelectedBeverage': 8217,
Expand Down Expand Up @@ -352,7 +320,68 @@ The coffee machine needs a better mapping to MQTT messages.
```
</details>

## Posting to the appliance

Whereas the reading of de status is very beta, this is very very alpha. There is some basic error handling, but don't expect that everything will work.

In your config file you can find items that contain `readWrite` or `writeOnly`, some of them contain values so you know what to provide, ie:

```json
"539": {
"name": "BSH.Common.Setting.PowerState",
"access": "readWrite",
"available": "true",
"refCID": "03",
"refDID": "80",
"values": {
"2": "On",
"3": "Standby"
}
},
```

With this information you can build the JSON object you can send over mqtt to change the power state

Topic: `homeconnect/[devicename]/set`, ie `homeconnect/coffeemaker/set`

Payload:

```json
{"uid":539,"value":2}
```
As for now, the results will be displayed by the script only, there is no response to an mqtt topic.

There are properties that do not require predefined values, debugging is required to see what is needed. Here are some of those values found through debugging:

Set the time:

```json
{"uid":520,"value":"2023-07-07T15:01:21"}
```

Synchronize with time server, `false` is disabled

```json
{"uid":547,"value":false}
```

## FRIDA tools

Moved to [`README-frida.md`](README-frida.md)

## Home assistant

For integration with Home Assistant, the following MQTT sensor can be used to create a read only sensor

```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 }}"
```

## Notes
- Sometimes when the device is off, there is the error `ERROR [ip] [Errno 113] No route to host`
- There is a lot more information available, like the status of a program that is currently active. This needs to be integrated if possible. For now only the values that relate to the `config.json` are published
13 changes: 13 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Changelog

All notable changes to this project will be documented in this file.

## 2023.9.12.1
### Added
- Ability to configure MQTT clientname

### Changed
- There was a default set of values being published. Now the device publishes what is present as access read, or readWrite in the `config.json`

### Fixed
- MQTT was not always published to the correct topic
Loading