diff --git a/app/integrations/expose.py b/app/integrations/expose.py index 2ab71cf..1a4e70c 100644 --- a/app/integrations/expose.py +++ b/app/integrations/expose.py @@ -1028,7 +1028,7 @@ def _m8_energy_provider(session: Session) -> list[ExposableEntity]: elif meter.commodity == "heating": unit, device_class = "GJ", "energy" else: - unit, device_class = "m³", "volume" + unit, device_class = "m³", "water" for suffix, getter in ( ("total", _meter_total_getter(binding.id, source.id, channel.id)), ("today", _meter_today_getter(binding.id, source.id, channel.id)), diff --git a/docs/design/m8-warmtelink-energy.md b/docs/design/m8-warmtelink-energy.md index de8b083..b9ad1f0 100644 --- a/docs/design/m8-warmtelink-energy.md +++ b/docs/design/m8-warmtelink-energy.md @@ -1214,6 +1214,12 @@ M8 收尾的前置条件。agent 不得执行、记录为已执行,或以 mock ## 14. Post-M8 lifecycle repair(M8-R08~R10) +### 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 修复 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`。 diff --git a/tests/test_energy_expose.py b/tests/test_energy_expose.py index 9021bf3..a610021 100644 --- a/tests/test_energy_expose.py +++ b/tests/test_energy_expose.py @@ -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.device_class == "energy" assert entries[f"meter.{water.uuid}.total"].entity.unit == "m³" - 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 == "m³" + 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.device_class == "energy" 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) +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"] == "m³" + 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: from app.integrations.expose import build_catalog