Cleaned up inverter mqtt
This commit is contained in:
@@ -10,6 +10,7 @@ Each file below lands at the indicated path — the top-level `install.sh` does
|
||||
| `mqttbroker.py` | `libs/mqttbroker.py` | (c) broaden `connect()`'s `except ConnectionRefusedError` to `(ConnectionRefusedError, OSError)` and narrow `publish()`'s bare `except Exception` to `(OSError, RuntimeError, ValueError)`. Otherwise any broker blip (HA restart, `Errno 113 No route to host`) crashes the daemon. |
|
||||
| `port_config_model.py` | `configmodel/port_config_model.py` | (e) add `serial_number: None \| str \| int = Field(default=None)` to `UsbPortConfig`. The model is `NoExtraBaseModel`, so powermon rejects `serial_number:` at the port level without this. |
|
||||
| `ports_init.py` | `ports/__init__.py` | (f) in `from_config()`, make `port_config['serial_number'] = serial_number` a fallback (`if port_config.get('serial_number') is None:`). Device-level `serial_number` is the HA identifier (e.g. `lvx6048_1`); the port-level one is the hardware PI18 serial — they must not be conflated. |
|
||||
| `hass.py` | `outputformats/hass.py` | (g) cleaner HA-discovery payload: drop bogus `last_reset` (HA spec for `total` only, not `measurement`); drop default `force_update: "true"` (causes recorder bloat on slow-changing fields); add `suggested_display_precision` per-unit (so HA frontend doesn't truncate `52.5 V` → `52 V`); default `state_class: measurement` for numeric sensors (required for HA long-term stats + Energy dashboard). |
|
||||
|
||||
Patches (a)–(d) are load-bearing for the live setup. Patches (e) and (f) enable
|
||||
powermon's native wildcard-path + serial-matching flow for a single-daemon
|
||||
|
||||
164
LVX6048/powermon-patches/hass.py
Normal file
164
LVX6048/powermon-patches/hass.py
Normal file
@@ -0,0 +1,164 @@
|
||||
""" powermon / outputformats / hass.py
|
||||
|
||||
LOCAL PATCH (g) — see ../Install.md §5(g). Original behavior preserved
|
||||
except for these changes (all aimed at cleaner HA-frontend rendering and
|
||||
correct long-term-statistics behavior):
|
||||
|
||||
- Removed `last_reset` from discovery payload. That field is HA-spec
|
||||
for `state_class: total` cumulative counters; setting it on
|
||||
`measurement` sensors causes HA to warn and ignore. Upstream emits
|
||||
`str(datetime.now())` on every discovery — meaningless timestamp
|
||||
that fires a state-class warning per entity per startup.
|
||||
|
||||
- Removed `force_update: "true"` default. Upstream forces a recorder
|
||||
write on every state publish even if the value hasn't changed,
|
||||
bloating the HA history database with hundreds of duplicate rows
|
||||
per day per slowly-changing entity. HA defaults are correct.
|
||||
|
||||
- Added `suggested_display_precision` based on unit. Without this,
|
||||
HA's voltage/current/temperature device classes round to integers
|
||||
in the frontend (e.g. `52.5 V` displays as `52 V`).
|
||||
|
||||
- Default `state_class: measurement` for numeric sensors when the
|
||||
protocol definition didn't supply one. Without state_class, HA
|
||||
excludes the sensor from long-term statistics, the Energy
|
||||
dashboard, and the standard "show this as a trend" graph.
|
||||
"""
|
||||
import json as js
|
||||
import logging
|
||||
|
||||
from powermon.commands.command import Command
|
||||
from powermon.commands.reading import Reading
|
||||
from powermon.commands.result import Result
|
||||
from powermon.outputformats.abstractformat import AbstractFormat
|
||||
|
||||
log = logging.getLogger("hass")
|
||||
|
||||
|
||||
# Suggested decimal places per unit for the HA frontend. Keys must match
|
||||
# whatever powermon's reading_definitions emit as the unit string (the
|
||||
# value the BMS / inverter parser sets as `data_unit`).
|
||||
_PRECISION_BY_UNIT: dict[str, int] = {
|
||||
"V": 1,
|
||||
"mV": 0,
|
||||
"A": 1,
|
||||
"mA": 0,
|
||||
"W": 0,
|
||||
"VA": 0,
|
||||
"Hz": 1,
|
||||
"%": 0,
|
||||
"°C": 0,
|
||||
"Ah": 1,
|
||||
"Wh": 0,
|
||||
"kWh": 1,
|
||||
"ms": 0,
|
||||
"s": 0,
|
||||
}
|
||||
|
||||
|
||||
class Hass(AbstractFormat):
|
||||
""" formatter to generate home assistant auto config mqtt messages """
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
self.name = "hass"
|
||||
self.discovery_prefix = config.get("discovery_prefix", "homeassistant")
|
||||
self.entity_id_prefix = config.get("entity_id_prefix", None)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}: generates Home Assistant auto config and update mqtt messages"
|
||||
|
||||
def get_options(self):
|
||||
""" return a dict of all options and defaults """
|
||||
extra_options = {"discovery_prefix": "homeassistant", "entity_id_prefix": None}
|
||||
options = super().get_options()
|
||||
options.update(extra_options)
|
||||
return options
|
||||
|
||||
def format(self, command: Command, result: Result, device_info) -> list:
|
||||
log.info("Using output formatter: %s", self.name)
|
||||
|
||||
config_msgs = []
|
||||
value_msgs = []
|
||||
|
||||
_result = []
|
||||
if result.readings is None:
|
||||
return _result
|
||||
display_data: list[Reading] = self.format_and_filter_data(result)
|
||||
log.debug("displayData: %s", display_data)
|
||||
|
||||
for response in display_data:
|
||||
data_name = self.format_key(response.data_name)
|
||||
value = response.data_value
|
||||
unit = response.data_unit
|
||||
icon = response.icon
|
||||
device_class = response.device_class
|
||||
state_class = response.state_class
|
||||
|
||||
if unit == "bool" or value == "enabled" or value == "disabled":
|
||||
component = "binary_sensor"
|
||||
else:
|
||||
component = "sensor"
|
||||
|
||||
if component == "binary_sensor":
|
||||
if value == 0 or value == "0" or value == "disabled":
|
||||
value = "OFF"
|
||||
elif value == 1 or value == "1" or value == "enabled":
|
||||
value = "ON"
|
||||
|
||||
if self.entity_id_prefix is None:
|
||||
object_id = f"{data_name}".lower().replace(" ", "_")
|
||||
name = f"{data_name}"
|
||||
else:
|
||||
object_id = f"{self.entity_id_prefix}_{data_name}".lower().replace(" ", "_")
|
||||
name = f"{self.entity_id_prefix} {data_name}"
|
||||
|
||||
topic_base = f"{self.discovery_prefix}/{component}/{object_id}".replace(" ", "_")
|
||||
topic = f"{topic_base}/config"
|
||||
state_topic = f"{topic_base}/state"
|
||||
|
||||
payload = {
|
||||
"name": f"{name}",
|
||||
"state_topic": f"{state_topic}",
|
||||
"unique_id": f"{object_id}_{device_info.serial_number}",
|
||||
# PATCH (g): removed `force_update: "true"` and `last_reset`
|
||||
}
|
||||
|
||||
payload["device"] = {
|
||||
"name": device_info.name,
|
||||
"identifiers": [device_info.serial_number],
|
||||
"model": device_info.model,
|
||||
"manufacturer": device_info.manufacturer,
|
||||
}
|
||||
|
||||
if unit and unit != "bool":
|
||||
payload["unit_of_measurement"] = f"{unit}"
|
||||
|
||||
if icon:
|
||||
payload.update({"icon": icon})
|
||||
|
||||
if device_class:
|
||||
payload["device_class"] = device_class
|
||||
|
||||
# PATCH (g): default state_class to "measurement" for numeric
|
||||
# sensors that don't have one set in the reading definition.
|
||||
# Required for HA long-term statistics / Energy dashboard wiring.
|
||||
if not state_class and component == "sensor" and isinstance(value, (int, float)):
|
||||
state_class = "measurement"
|
||||
if state_class:
|
||||
payload["state_class"] = state_class
|
||||
|
||||
# PATCH (g): emit suggested_display_precision so HA frontend
|
||||
# renders the correct decimal count instead of rounding voltages
|
||||
# / currents / etc. to integers per device_class default.
|
||||
if component == "sensor" and unit in _PRECISION_BY_UNIT:
|
||||
payload["suggested_display_precision"] = _PRECISION_BY_UNIT[unit]
|
||||
|
||||
payloads = js.dumps(payload)
|
||||
msg = {"topic": topic, "payload": payloads}
|
||||
config_msgs.append(msg)
|
||||
|
||||
msg = {"topic": state_topic, "payload": value}
|
||||
value_msgs.append(msg)
|
||||
|
||||
# order value msgs after config to allow HA time to build entity before state data arrives
|
||||
return config_msgs + value_msgs
|
||||
@@ -320,7 +320,7 @@ QUERY_COMMANDS = {
|
||||
"2": "discharge",
|
||||
},
|
||||
},
|
||||
{"description": "DC-AC power direction", "reading_type": ReadingType.MESSAGE,
|
||||
{"description": "DC AC power direction", "reading_type": ReadingType.MESSAGE,
|
||||
"response_type": ResponseType.OPTION,
|
||||
"options": {
|
||||
"0": "donothing",
|
||||
|
||||
Reference in New Issue
Block a user