M8-R16: expose hot water to HA Energy

This commit is contained in:
2026-08-28 10:02:53 +02:00
parent 018f13d73d
commit ed99d4410b
3 changed files with 37 additions and 2 deletions
+1 -1
View File
@@ -1028,7 +1028,7 @@ def _m8_energy_provider(session: Session) -> list[ExposableEntity]:
elif meter.commodity == "heating": elif meter.commodity == "heating":
unit, device_class = "GJ", "energy" unit, device_class = "GJ", "energy"
else: else:
unit, device_class = "", "volume" unit, device_class = "", "water"
for suffix, getter in ( for suffix, getter in (
("total", _meter_total_getter(binding.id, source.id, channel.id)), ("total", _meter_total_getter(binding.id, source.id, channel.id)),
("today", _meter_today_getter(binding.id, source.id, channel.id)), ("today", _meter_today_getter(binding.id, source.id, channel.id)),
+6
View File
@@ -1214,6 +1214,12 @@ M8 收尾的前置条件。agent 不得执行、记录为已执行,或以 mock
## 14. Post-M8 lifecycle repairM8-R08R10 ## 14. Post-M8 lifecycle repairM8-R08R10
### M8-R16 — HA Water Energy Discovery metadata
active `hot_water` Meter 的 total/today MQTT Discovery metadata 从通用 `volume` 修正为 Home
Assistant Energy Water 所要求的 `water`;保留 `m³``total_increasing`。区域供暖继续使用合法的
`energy + GJ` 组合,thermal EUR 成本实体以及既有 key、identity、unique_id 和 topic 均不变。
### M8-R15 — HA Discovery identity/topic repair ### M8-R15 — HA Discovery identity/topic repair
修复 HA 将多值 `device.identifiers` 任一匹配合并的风险:Source、Meter、Modbus 与成本 epoch 均使用单一完整 namespaced identifier,内部 MQTT identity 独立保存。thermal 的既有点号 identity/key 保持兼容,node/object topic segment 单独规范化;仅对 v1.6.1 实际发布过的 dot topic 做可重试、成功后抑制的精确 retained cleanup。新增默认关闭的 `Thermal Hot Water Total/Today`,金额为 `hot_water_heating + hot_water`,不含 `hot_water_tax` 修复 HA 将多值 `device.identifiers` 任一匹配合并的风险:Source、Meter、Modbus 与成本 epoch 均使用单一完整 namespaced identifier,内部 MQTT identity 独立保存。thermal 的既有点号 identity/key 保持兼容,node/object topic segment 单独规范化;仅对 v1.6.1 实际发布过的 dot topic 做可重试、成功后抑制的精确 retained cleanup。新增默认关闭的 `Thermal Hot Water Total/Today`,金额为 `hot_water_heating + hot_water`,不含 `hot_water_tax`
+30 -1
View File
@@ -2788,7 +2788,11 @@ def test_m8_catalog_has_source_meter_and_thermal_entities_disabled(energy_db) ->
assert entries[f"meter.{heating.uuid}.total"].entity.unit == "GJ" assert entries[f"meter.{heating.uuid}.total"].entity.unit == "GJ"
assert entries[f"meter.{heating.uuid}.total"].entity.device_class == "energy" assert entries[f"meter.{heating.uuid}.total"].entity.device_class == "energy"
assert entries[f"meter.{water.uuid}.total"].entity.unit == "" assert entries[f"meter.{water.uuid}.total"].entity.unit == ""
assert entries[f"meter.{water.uuid}.total"].entity.device_class == "volume" for suffix in ("total", "today"):
water_entity = entries[f"meter.{water.uuid}.{suffix}"].entity
assert water_entity.device_class == "water"
assert water_entity.unit == ""
assert water_entity.state_class == "total_increasing"
assert entries[f"meter.{electricity.uuid}.total"].entity.unit == "kWh" assert entries[f"meter.{electricity.uuid}.total"].entity.unit == "kWh"
assert entries[f"meter.{electricity.uuid}.total"].entity.device_class == "energy" assert entries[f"meter.{electricity.uuid}.total"].entity.device_class == "energy"
assert entries[f"meter.{electricity.uuid}.today"].entity.state_class == "total_increasing" assert entries[f"meter.{electricity.uuid}.today"].entity.state_class == "total_increasing"
@@ -2812,6 +2816,31 @@ def test_m8_catalog_has_source_meter_and_thermal_entities_disabled(energy_db) ->
assert all(entry.entity.unit == "EUR" and entry.entity.device_class == "monetary" for entry in thermal) assert all(entry.entity.unit == "EUR" and entry.entity.device_class == "monetary" for entry in thermal)
def test_m8_hot_water_discovery_payload_is_ha_energy_compatible(energy_db) -> None:
"""Hot-water Meter discovery exposes the exact HA Energy Water metadata."""
from app.integrations.expose import build_catalog
from app.services.ha_discovery import build_discovery_payload
now = datetime.now(timezone.utc)
with Session(energy_db) as session:
_source, _channel, meter = _make_thermal_source_and_meter(session, "hot_water", now)
session.commit()
entries = {item.entity.key: item.entity for item in build_catalog(session)}
for suffix in ("total", "today"):
entity = entries[f"meter.{meter.uuid}.{suffix}"]
topic, payload = build_discovery_payload(entity, "homeassistant")
assert payload["device_class"] == "water"
assert payload["unit_of_measurement"] == ""
assert payload["state_class"] == "total_increasing"
assert payload["unique_id"] == f"{meter.uuid}_meter_{meter.uuid}_{suffix}"
assert payload["device"]["identifiers"] == [f"home-automation:meter:{meter.uuid}"]
assert topic == (
f"homeassistant/sensor/{meter.uuid.replace('-', '_')}/"
f"meter_{meter.uuid.replace('-', '_')}_{suffix}/config"
)
def test_m8_meter_getter_hides_stale_or_offline_source(energy_db) -> None: def test_m8_meter_getter_hides_stale_or_offline_source(energy_db) -> None:
from app.integrations.expose import build_catalog from app.integrations.expose import build_catalog