Merge pull request #12 from Meatballs1/skonspr
Allows setting device values through MQTT /set endpoint
This commit is contained in:
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"githubPullRequests.ignoredPullRequestBranches": [
|
||||||
|
"main"
|
||||||
|
]
|
||||||
|
}
|
||||||
82
HCDevice.py
82
HCDevice.py
@@ -54,7 +54,7 @@ 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, features):
|
def __init__(self, ws, features, name):
|
||||||
self.ws = ws
|
self.ws = ws
|
||||||
self.features = features
|
self.features = features
|
||||||
self.session_id = None
|
self.session_id = None
|
||||||
@@ -62,13 +62,14 @@ class HCDevice:
|
|||||||
self.device_name = "hcpy"
|
self.device_name = "hcpy"
|
||||||
self.device_id = "0badcafe"
|
self.device_id = "0badcafe"
|
||||||
self.debug = False
|
self.debug = False
|
||||||
|
self.name = name
|
||||||
|
|
||||||
def parse_values(self, values):
|
def parse_values(self, values):
|
||||||
if not self.features:
|
if not self.features:
|
||||||
return values
|
return values
|
||||||
|
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
for msg in values:
|
for msg in values:
|
||||||
uid = str(msg["uid"])
|
uid = str(msg["uid"])
|
||||||
value = msg["value"]
|
value = msg["value"]
|
||||||
@@ -92,19 +93,62 @@ class HCDevice:
|
|||||||
|
|
||||||
return result
|
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) 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.")
|
||||||
|
|
||||||
|
# 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) 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}.")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def recv(self):
|
def recv(self):
|
||||||
try:
|
try:
|
||||||
buf = self.ws.recv()
|
buf = self.ws.recv()
|
||||||
if buf is None:
|
if buf is None:
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("receive error", e, traceback.format_exc())
|
print(self.name, "receive error", e, traceback.format_exc())
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.handle_message(buf)
|
return self.handle_message(buf)
|
||||||
except Exception as e:
|
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
|
return None
|
||||||
|
|
||||||
# reply to a POST or GET message with new data
|
# reply to a POST or GET message with new data
|
||||||
@@ -129,25 +173,32 @@ class HCDevice:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if data is not None:
|
if data is not None:
|
||||||
msg["data"] = [data]
|
if action == "POST":
|
||||||
|
if self.test_feature(data) is False:
|
||||||
|
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
|
self.tx_msg_id += 1
|
||||||
|
|
||||||
def handle_message(self, buf):
|
def handle_message(self, buf):
|
||||||
msg = json.loads(buf)
|
msg = json.loads(buf)
|
||||||
if self.debug:
|
if self.debug:
|
||||||
print(now(), "RX:", msg)
|
print(now(), self.name, "RX:", msg)
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
resource = msg["resource"]
|
resource = msg["resource"]
|
||||||
action = msg["action"]
|
action = msg["action"]
|
||||||
|
|
||||||
values = {}
|
values = {}
|
||||||
|
|
||||||
if "code" in msg:
|
if "code" in msg:
|
||||||
#print(now(), "ERROR", msg["code"])
|
print(now(), self.name, "ERROR", msg["code"])
|
||||||
values = {
|
values = {
|
||||||
"error": msg["code"],
|
"error": msg["code"],
|
||||||
"resource": msg.get("resource", ''),
|
"resource": msg.get("resource", ''),
|
||||||
@@ -186,7 +237,7 @@ class HCDevice:
|
|||||||
self.get("/ro/allMandatoryValues")
|
self.get("/ro/allMandatoryValues")
|
||||||
#self.get("/ro/values")
|
#self.get("/ro/values")
|
||||||
else:
|
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":
|
elif action == "RESPONSE" or action == "NOTIFY":
|
||||||
if resource == "/iz/info" or resource == "/ci/info":
|
if resource == "/iz/info" or resource == "/ci/info":
|
||||||
@@ -204,7 +255,10 @@ class HCDevice:
|
|||||||
|
|
||||||
elif resource == "/ro/allMandatoryValues" \
|
elif resource == "/ro/allMandatoryValues" \
|
||||||
or resource == "/ro/values":
|
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":
|
elif resource == "/ci/registeredDevices":
|
||||||
# we don't care
|
# we don't care
|
||||||
pass
|
pass
|
||||||
@@ -215,7 +269,7 @@ class HCDevice:
|
|||||||
self.services[service["service"]] = {
|
self.services[service["service"]] = {
|
||||||
"version": service["version"],
|
"version": service["version"],
|
||||||
}
|
}
|
||||||
#print(now(), "services", self.services)
|
#print(self.name, now(), "services", self.services)
|
||||||
|
|
||||||
# we should figure out which ones to query now
|
# we should figure out which ones to query now
|
||||||
# if "iz" in self.services:
|
# if "iz" in self.services:
|
||||||
@@ -227,8 +281,8 @@ class HCDevice:
|
|||||||
|
|
||||||
#self.get("/if/info")
|
#self.get("/if/info")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print(now(), "Unknown", msg)
|
print(now(), self.name, "Unknown", msg)
|
||||||
|
|
||||||
# return whatever we've parsed out of it
|
# return whatever we've parsed out of it
|
||||||
return values
|
return values
|
||||||
|
|||||||
69
README.md
69
README.md
@@ -17,7 +17,7 @@ and should prevent most any random attacker on your network from being able to
|
|||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
To avoid running into issues later with your default python installs, it's recommended to use a py virtual env for doing this. Go to your desired test directory, and:
|
To avoid running into issues later with your default python installs, it's recommended to use a py virtual env for doing this. Go to your desired test directory, and:
|
||||||
```
|
```bash
|
||||||
python3 -m venv venv
|
python3 -m venv venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
git clone https://github.com/osresearch/hcpy
|
git clone https://github.com/osresearch/hcpy
|
||||||
@@ -169,7 +169,6 @@ Example message published to `homeconnect/dishwasher`:
|
|||||||
'SilenceOnDemand': False
|
'SilenceOnDemand': False
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Clothes washer
|
### Clothes washer
|
||||||
@@ -262,14 +261,13 @@ Example message published to `homeconnect/washer`:
|
|||||||
'SelectedProgram': 28718
|
'SelectedProgram': 28718
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Coffee Machine
|
### Coffee Machine
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
The coffee machine needs a better mapping to MQTT messages.
|
Example message published to `homeconnect/coffeemaker`:
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Full state information</summary>
|
<summary>Full state information</summary>
|
||||||
@@ -367,9 +365,70 @@ The coffee machine needs a better mapping to MQTT messages.
|
|||||||
'ProcessPhase': 'None'
|
'ProcessPhase': 'None'
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
## Posting to the appliance
|
||||||
|
|
||||||
|
Whereas the reading of the 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
|
## FRIDA tools
|
||||||
|
|
||||||
Moved to [`README-frida.md`](README-frida.md)
|
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
changelog.md
Normal file
13
changelog.md
Normal file
@@ -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
|
||||||
104
hc2mqtt
104
hc2mqtt
@@ -4,6 +4,7 @@
|
|||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import ssl
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
import click
|
import click
|
||||||
@@ -12,81 +13,118 @@ import paho.mqtt.client as mqtt
|
|||||||
from HCDevice import HCDevice
|
from HCDevice import HCDevice
|
||||||
from HCSocket import HCSocket, now
|
from HCSocket import HCSocket, now
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.argument("config_file")
|
@click.argument("config_file")
|
||||||
@click.option("-h", "--mqtt_host", default="localhost")
|
@click.option("-h", "--mqtt_host", default="localhost")
|
||||||
@click.option("-p", "--mqtt_prefix", default="homeconnect/")
|
@click.option("-p", "--mqtt_prefix", default="homeconnect/")
|
||||||
def hc2mqtt(config_file: str, mqtt_host: str, mqtt_prefix: str):
|
@click.option("--mqtt_port", default=1883, type=int)
|
||||||
click.echo(f"Hello {config_file=} {mqtt_host=} {mqtt_prefix=}")
|
@click.option("--mqtt_username")
|
||||||
|
@click.option("--mqtt_password")
|
||||||
|
@click.option("--mqtt_ssl", is_flag=True)
|
||||||
|
@click.option("--mqtt_cafile")
|
||||||
|
@click.option("--mqtt_certfile")
|
||||||
|
@click.option("--mqtt_keyfile")
|
||||||
|
@click.option("--mqtt_clientname", default="hcpy")
|
||||||
|
|
||||||
|
def hc2mqtt(config_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 {config_file=} {mqtt_host=} {mqtt_prefix=} {mqtt_port=} {mqtt_username=} {mqtt_password=} "
|
||||||
|
f"{mqtt_ssl=} {mqtt_cafile=} {mqtt_certfile=} {mqtt_keyfile=} {mqtt_clientname=}")
|
||||||
|
|
||||||
with open(config_file, "r") as f:
|
with open(config_file, "r") as f:
|
||||||
devices = json.load(f)
|
devices = json.load(f)
|
||||||
|
|
||||||
client = mqtt.Client()
|
client = mqtt.Client(mqtt_clientname)
|
||||||
client.connect(host=mqtt_host, port=1883, keepalive=70)
|
|
||||||
|
if mqtt_username and mqtt_password:
|
||||||
|
client.username_pw_set(mqtt_username, mqtt_password)
|
||||||
|
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
client.tls_set(cert_reqs=ssl.CERT_NONE)
|
||||||
|
|
||||||
|
client.connect(host=mqtt_host, port=mqtt_port, keepalive=70)
|
||||||
|
|
||||||
for device in devices:
|
for device in devices:
|
||||||
mqtt_topic = mqtt_prefix + device["name"]
|
mqtt_topic = mqtt_prefix + device["name"]
|
||||||
|
print(now(), f"topic: {mqtt_topic}")
|
||||||
thread = Thread(target=client_connect, args=(client, device, mqtt_topic))
|
thread = Thread(target=client_connect, args=(client, device, mqtt_topic))
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
client.loop_forever()
|
client.loop_forever()
|
||||||
|
|
||||||
|
|
||||||
# Map their value names to easier state names
|
# Map their value names to easier state names
|
||||||
topics = {
|
topics = {
|
||||||
"OperationState": "state",
|
"InternalError": "Error",
|
||||||
"DoorState": "door",
|
"FatalErrorOccured": "Error",
|
||||||
"RemainingProgramTime": "remaining",
|
|
||||||
"PowerState": "power",
|
|
||||||
"LowWaterPressure": "lowwaterpressure",
|
|
||||||
"AquaStopOccured": "aquastop",
|
|
||||||
"InternalError": "error",
|
|
||||||
"FatalErrorOccured": "error",
|
|
||||||
}
|
}
|
||||||
|
global dev
|
||||||
|
dev = {}
|
||||||
|
|
||||||
def client_connect(client, device, mqtt_topic):
|
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('/')
|
||||||
|
print(now(), f"received mqtt message {mqtt_state}")
|
||||||
|
try:
|
||||||
|
msg = json.loads(mqtt_state)
|
||||||
|
if 'uid' in msg:
|
||||||
|
dev[mqtt_topic[-2]].get("/ro/values", 1, "POST", msg)
|
||||||
|
else:
|
||||||
|
raise Exception(f"Payload {msg} is not correctly formatted")
|
||||||
|
except Exception as e:
|
||||||
|
print("ERROR", e, file=sys.stderr)
|
||||||
|
|
||||||
host = device["host"]
|
host = device["host"]
|
||||||
|
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(".")
|
||||||
|
device_topics[name[-1]] = name[-1]
|
||||||
|
device_topics[value] = name[-1] #sometimes the returned key is a digit, making translation possible
|
||||||
|
|
||||||
state = {}
|
state = {}
|
||||||
for topic in topics:
|
for topic in device_topics:
|
||||||
state[topics[topic]] = None
|
if not topic.isdigit(): #We only want the named topics
|
||||||
|
state[device_topics[topic]] = None
|
||||||
|
|
||||||
|
mqtt_set_topic = mqtt_topic + "/set"
|
||||||
|
print(now(), device["name"], f"set topic: {mqtt_set_topic}")
|
||||||
|
client.subscribe(mqtt_set_topic)
|
||||||
|
client.on_message = on_message
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
print(now(), device["name"], f"connecting to {host}")
|
||||||
ws = HCSocket(host, device["key"], device.get("iv",None))
|
ws = HCSocket(host, device["key"], device.get("iv",None))
|
||||||
dev = HCDevice(ws, device.get("features", None))
|
dev[device["name"]] = HCDevice(ws, device.get("features", None), device["name"])
|
||||||
|
|
||||||
#ws.debug = True
|
#ws.debug = True
|
||||||
ws.reconnect()
|
ws.reconnect()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
msg = dev.recv()
|
msg = dev[device["name"]].recv()
|
||||||
if msg is None:
|
if msg is None:
|
||||||
break
|
break
|
||||||
if len(msg) > 0:
|
if len(msg) > 0:
|
||||||
print(now(), msg)
|
print(now(), device["name"], msg)
|
||||||
|
|
||||||
update = False
|
update = False
|
||||||
for topic in topics:
|
for topic in device_topics:
|
||||||
value = msg.get(topic, None)
|
value = msg.get(topic, None)
|
||||||
if value is None:
|
if value is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Convert "On" to True, "Off" to False
|
# new_topic = topics[topic]
|
||||||
if value == "On":
|
# if new_topic == "remaining":
|
||||||
value = True
|
# state["remainingseconds"] = value
|
||||||
elif value == "Off":
|
# value = "%d:%02d" % (value / 60 / 60, (value / 60) % 60)
|
||||||
value = False
|
|
||||||
|
|
||||||
new_topic = topics[topic]
|
|
||||||
if new_topic == "remaining":
|
|
||||||
state["remainingseconds"] = value
|
|
||||||
value = "%d:%02d" % (value / 60 / 60, (value / 60) % 60)
|
|
||||||
|
|
||||||
|
new_topic = device_topics[topic]
|
||||||
state[new_topic] = value
|
state[new_topic] = value
|
||||||
update = True
|
update = True
|
||||||
|
|
||||||
@@ -94,7 +132,7 @@ def client_connect(client, device, mqtt_topic):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
msg = json.dumps(state)
|
msg = json.dumps(state)
|
||||||
print("publish", mqtt_topic, msg)
|
print(now(), device["name"], f"publish to {mqtt_topic} with {msg}")
|
||||||
client.publish(mqtt_topic + "/state", msg)
|
client.publish(mqtt_topic + "/state", msg)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user