M5-T11: add HA discovery and state publishing wired to polling

This commit is contained in:
2026-06-22 15:32:43 +02:00
parent 9d05f5ea62
commit 35b95f5c88
7 changed files with 1657 additions and 23 deletions
+44 -19
View File
@@ -91,9 +91,10 @@ class ExposableEntity:
context, e.g. ``"SDM120 Voltage"``).
value_getter
Optional callable ``() -> object | None`` that returns the current
entity value. ``None`` means "not yet wired" (T11 will populate this).
T11 calls this to obtain the current state before publishing.
Optional callable ``(session) -> object | None`` that returns the current
entity value given an open SQLAlchemy session. ``None`` means "not yet
wired". T11 calls this to obtain the current state before publishing.
Signature: ``Callable[[Session], Any]``.
state_class
HA ``state_class`` string (e.g. ``"measurement"``, ``"total_increasing"``).
@@ -106,7 +107,7 @@ class ExposableEntity:
device_class: Optional[str]
unit: str
name: str
value_getter: Optional[Callable[[], Any]] = field(default=None, repr=False)
value_getter: Optional[Callable[["Session"], Any]] = field(default=None, repr=False)
state_class: Optional[str] = None
@@ -269,16 +270,29 @@ def _modbus_provider(session: Session) -> list[ExposableEntity]:
for metric in profile.metrics:
entity_key = f"modbus.{device.uuid}.{metric.key}"
# Capture metric and device for the value_getter closure.
_device_uuid = device.uuid
# Capture device id and metric key for the value_getter closure.
# The getter queries the most recent ModbusReading for this device
# and extracts the metric value from its payload.
_device_id = device.id
_metric_key = metric.key
def _make_value_getter(dev_uuid: str, m_key: str) -> Callable[[], Any]:
"""Return a getter stub for this metric; T11 will wire real retrieval."""
def _getter() -> Any:
# Stub — T11 will call build_catalog with a live session
# and replace this with a real value-fetch callable.
return None
def _make_value_getter(
dev_id: int, m_key: str
) -> Callable[["Session"], Any]:
"""Return a getter that fetches the latest reading value from DB."""
def _getter(sess: "Session") -> Any:
from app.models.modbus import ModbusReading
from sqlalchemy import desc
reading = (
sess.query(ModbusReading)
.filter(ModbusReading.device_id == dev_id)
.order_by(desc(ModbusReading.recorded_at))
.first()
)
if reading is None:
return None
return reading.payload.get(m_key)
return _getter
@@ -290,19 +304,30 @@ def _modbus_provider(session: Session) -> list[ExposableEntity]:
device_class=metric.device_class or None,
unit=metric.unit,
name=f"{device.friendly_name} {metric.key.replace('_', ' ').title()}",
value_getter=_make_value_getter(_device_uuid, _metric_key),
value_getter=_make_value_getter(_device_id, _metric_key),
state_class=metric.state_class,
)
)
# One binary_sensor entity for device "online" status.
online_key = f"modbus.{device.uuid}.online"
_dev_uuid_online = device.uuid
_dev_id_online = device.id
def _make_online_getter(dev_id: int) -> Callable[["Session"], Any]:
"""Return a getter that reports the device online status from DB."""
def _getter(sess: "Session") -> Any:
from app.models.modbus import ModbusDevice
dev = sess.query(ModbusDevice).filter(
ModbusDevice.id == dev_id
).first()
if dev is None:
return None
# Map last_poll_ok to HA binary_sensor payload strings.
if dev.last_poll_ok is True:
return "ON"
return "OFF"
def _make_online_getter(dev_uuid: str) -> Callable[[], Any]:
def _getter() -> Any:
# T11 will wire this to last_poll_ok.
return None
return _getter
entities.append(
@@ -313,7 +338,7 @@ def _modbus_provider(session: Session) -> list[ExposableEntity]:
device_class="connectivity",
unit="",
name=f"{device.friendly_name} Online",
value_getter=_make_online_getter(_dev_uuid_online),
value_getter=_make_online_getter(_dev_id_online),
state_class=None,
)
)