Skip to content
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,6 @@ python_protocol_gateway.egg-info/*
inflxudb_backlog/*

#ignore dumps for dev
tools/dumps/*
tools/dumps/*
/.vs/PythonProtocolGateway.slnx/FileContentIndex
/.vs
50 changes: 31 additions & 19 deletions classes/protocol_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from enum import Enum
from typing import TYPE_CHECKING, Union

from defs.common import strtoint
from defs.common import strtoint, strtobool_or_og

if TYPE_CHECKING:
from configparser import SectionProxy
Expand Down Expand Up @@ -224,6 +224,8 @@ class registry_map_entry:
write_mode : WriteMode = WriteMode.READ
''' enable disable reading/writing '''

ha_disc : dict[str, any] = None

def __str__(self):
return self.variable_name

Expand Down Expand Up @@ -677,6 +679,15 @@ def process_row(row):
if "write" in row:
writeMode = WriteMode.fromString(row["write"])

ha_discovery = {}
if "ha discovery" in row and row["ha discovery"]:
ha_discovery = {key.strip().lower(): strtobool_or_og(value.strip().lower()) for key, value in dict(disc.split(":") for disc in row["ha discovery"].split(",")).items()}

elif writeMode == WriteMode.WRITE or WriteMode.WRITEONLY:
ha_discovery = dict({'p': 'number', 'enabled_by_default' : True})
else:
ha_discovery = dict({'p': 'sensor', 'enabled_by_default' : True})

for i in r:
item = registry_map_entry(
registry_type = registry_type,
Expand All @@ -698,7 +709,8 @@ def process_row(row):
value_regex=value_regex,
read_command = read_command,
read_interval=read_interval,
write_mode=writeMode
write_mode=writeMode,
ha_disc = ha_discovery
)
registry_map.append(item)

Expand Down Expand Up @@ -750,29 +762,29 @@ def process_row(row):
item = registry_map[index]
if index > 0:
#if high/low, its a double
if (
item.documented_name.endswith("_l")
and registry_map[index-1].documented_name.replace("_h", "_l") == item.documented_name
):
combined_item = registry_map[index-1]
if (item.documented_name.endswith("_l")):
if (registry_map[index-1].documented_name.replace("_h", "_l") == item.documented_name):
combined_item = registry_map[index-1]
elif (registry_map[index+1].documented_name.replace("_h", "_l") == item.documented_name):
combined_item = registry_map[index+1]

if not combined_item.data_type or combined_item.data_type == Data_Type.USHORT:
if registry_map[index].data_type != Data_Type.USHORT:
combined_item.data_type = registry_map[index].data_type
else:
combined_item.data_type = Data_Type.UINT
if not combined_item.data_type or combined_item.data_type == Data_Type.USHORT:
if registry_map[index].data_type != Data_Type.USHORT:
combined_item.data_type = registry_map[index].data_type
else:
combined_item.data_type = Data_Type.UINT


if combined_item.documented_name == combined_item.variable_name:
combined_item.variable_name = combined_item.variable_name[:-2].strip()
if combined_item.documented_name == combined_item.variable_name:
combined_item.variable_name = combined_item.variable_name[:-2].strip()

combined_item.documented_name = combined_item.documented_name[:-2].strip()
combined_item.documented_name = combined_item.documented_name[:-2].strip()

if not combined_item.unit: #fix inconsistsent documentation
combined_item.unit = registry_map[index].unit
combined_item.unit_mod = registry_map[index].unit_mod
if not combined_item.unit: #fix inconsistsent documentation
combined_item.unit = registry_map[index].unit
combined_item.unit_mod = registry_map[index].unit_mod

del registry_map[index]
del registry_map[index]

#apply mask
if self.variable_mask:
Expand Down
64 changes: 46 additions & 18 deletions classes/transports/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import random
import time
import warnings
import copy
from configparser import SectionProxy

import paho.mqtt.client
Expand Down Expand Up @@ -163,7 +164,10 @@ def write_data(self, data : dict[str, str], from_transport : transport_base):
self._log.info(f"write data from [{from_transport.transport_name}] to mqtt transport")
self._log.info(data)
#have to send this every loop, because mqtt doesnt disconnect when HA restarts. HA bug.
info = self.client.publish(self.base_topic + "/" + from_transport.device_identifier + "/availability","online", qos=0,retain=True)

#If this is retain= false I think it'll stp showing everything online when PPG disconnects
info = self.client.publish(self.base_topic + "/" + from_transport.device_identifier + "/availability","online", qos=0,retain=False)

if info.rc == MQTT_ERR_NO_CONN:
self.connected = False

Expand Down Expand Up @@ -215,10 +219,25 @@ def mqtt_discovery(self, from_transport : transport_base):
device["identifiers"] = "hotnoob_" + from_transport.device_model + "_" + from_transport.device_serial_number
device["name"] = from_transport.device_name

#these should probably be read dynamically so it updates automatically on releases...
origin = {}
origin["name"] = "python-protocol-gateway"
origin["sw"] = "1.1.11-dev"
origin["url"] = "https://github.com/HotNoob/PythonProtocolGateway"

registry_map : list[registry_map_entry] = []
for entries in from_transport.protocolSettings.registry_map.values():
registry_map.extend(entries)

disc_payload_base = {}
disc_payload_base["availability_topic"] = self.base_topic + "/" + from_transport.device_identifier + "/availability"
disc_payload_base["device"] = device
disc_payload_base["origin"] = origin
disc_payload_base["cmps"] = {}
disc_payload = copy.deepcopy(disc_payload_base)

write_only = {}

length = len(registry_map)
count = 0
for item in registry_map:
Expand All @@ -230,7 +249,6 @@ def mqtt_discovery(self, from_transport : transport_base):
if item.write_mode == WriteMode.READDISABLED: #disabled
continue


clean_name = item.variable_name.lower().replace(" ", "_").strip()
if not clean_name: #if name is empty, skip
continue
Expand All @@ -242,37 +260,47 @@ def mqtt_discovery(self, from_transport : transport_base):
if self.__holding_register_prefix and item.registry_type == Registry_Type.HOLDING:
clean_name = self.__holding_register_prefix + clean_name


print(("#Publishing Topic "+str(count)+" of " + str(length) + ' "'+str(clean_name)+'"').ljust(100)+"#", end="\r", flush=True)

#device['sw_version'] = bms_version
disc_payload = {}
disc_payload["availability_topic"] = self.base_topic + "/" + from_transport.device_identifier + "/availability"
disc_payload["device"] = device
disc_payload["name"] = clean_name
disc_payload["unique_id"] = "hotnoob_" + from_transport.device_serial_number + "_"+clean_name
unique_id = "hotnoob_" + from_transport.device_serial_number + "_" + clean_name
disc_payload["cmps"][unique_id] = {}
disc_payload["cmps"][unique_id]["name"] = clean_name
disc_payload["cmps"][unique_id]["unique_id"] = unique_id
disc_payload["cmps"][unique_id].update(item.ha_disc)


writePrefix = ""
if from_transport.write_enabled and ( item.write_mode == WriteMode.WRITE or item.write_mode == WriteMode.WRITEONLY ):
writePrefix = "" #home assistant doesnt like write prefix

disc_payload["state_topic"] = self.base_topic + "/" +from_transport.device_identifier + writePrefix+ "/"+clean_name
disc_payload["cmps"][unique_id]["state_topic"] = self.base_topic + "/" + from_transport.device_identifier + writePrefix+ "/" + clean_name

if item.unit:
disc_payload["unit_of_measurement"] = item.unit

disc_payload["cmps"][unique_id]["unit_of_measurement"] = item.unit

discovery_topic = self.discovery_topic+"/sensor/HN-" + from_transport.device_serial_number + writePrefix + "/" + disc_payload["name"].replace(" ", "_") + "/config"
discovery_topic = self.discovery_topic+"/device/HN-" + from_transport.device_serial_number + writePrefix + "/config"

self.client.publish(discovery_topic,
json.dumps(disc_payload),qos=1, retain=True)

#send WO message to indicate topic is write only
#add WO message to be sent later to indicate topic is write only
if item.write_mode == WriteMode.WRITEONLY:
self.client.publish(disc_payload["state_topic"], "WRITEONLY")
write_only[disc_payload["cmps"][unique_id]["state_topic"]] = "WRITEONLY"

#break up items into batches to make the messages smaller
if len(disc_payload["cmps"]) > 10:
self.client.publish(discovery_topic, json.dumps(disc_payload),qos=1, retain=True)
#reset component list for next batch
disc_payload = copy.deepcopy(disc_payload_base)
time.sleep(0.07)

#publish whatever is left
if len(disc_payload["cmps"]) > 0:
self.client.publish(discovery_topic, json.dumps(disc_payload),qos=1, retain=True)
time.sleep(0.07) #slow down for better reliability

for t, val in write_only.items():
self.client.publish(t, val)
time.sleep(0.07) #slow down for better reliability

self.client.publish(disc_payload["availability_topic"],"online",qos=0, retain=True)
self.client.publish(disc_payload["availability_topic"],"online",qos=0, retain=False)
print()
self._log.info("Published HA "+str(count)+"x Discovery Topics")
13 changes: 13 additions & 0 deletions defs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ def strtobool (val):

return 0

def strtobool_or_og(val):
"""Convert a string representation of truth to boolean if it represents a boolean otherwise returns the original value.
True values are 'y', 'yes', 't', 'true', 'on', and '1'
False values are 'n', 'no', 'f', 'false', 'off', and '0'
"""
if isinstance(val, str):
clean_val = val.strip().lower()
if clean_val in ("y", "yes", "t", "true", "on", "1"):
return True
elif clean_val in ("n", "no", "f", "false", "off", "0"):
return False
return val

def strtoint(val : str) -> int:
''' converts str to int, but allows for hex string input, identified by x prefix'''

Expand Down
Loading