Add all remaining guntamatic sensors (#179770)

This commit is contained in:
Jens Timmerman
2026-08-26 15:51:33 +02:00
committed by GitHub
parent 2f09628dce
commit c5852702eb
6 changed files with 2229 additions and 39 deletions
@@ -59,12 +59,10 @@ rules:
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices:
status: exempt
comment: Single device
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default: todo
entity-disabled-by-default: done
entity-translations: done
exception-translations: todo
icon-translations: todo
+307 -25
View File
@@ -1,5 +1,7 @@
"""Support for Guntamatic sensors in Home Assistant."""
from datetime import date, timedelta
import re
from typing import override
from homeassistant.components.sensor import (
@@ -9,17 +11,28 @@ from homeassistant.components.sensor import (
SensorStateClass,
StateType,
)
from homeassistant.const import PERCENTAGE, UnitOfTemperature
from homeassistant.const import (
PERCENTAGE,
EntityCategory,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import ChildDeviceInfo, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from .const import DOMAIN
from .coordinator import GuntamaticConfigEntry, GuntamaticCoordinator
PARALLEL_UPDATES = 0
HEATING_CIRCUIT_REGEX = re.compile(
r"^(?:room|circuit|heating_circulation_pump|heating_circulation_program)_(\d+)"
)
GUNTAMATIC_SENSORS: list[SensorEntityDescription] = [
SensorEntityDescription(
key="program",
@@ -35,6 +48,12 @@ GUNTAMATIC_SENSORS: list[SensorEntityDescription] = [
"dhw_boost",
],
),
SensorEntityDescription(
key="status",
translation_key="status",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="boiler_temperature",
translation_key="boiler_temperature",
@@ -56,6 +75,33 @@ GUNTAMATIC_SENSORS: list[SensorEntityDescription] = [
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
SensorEntityDescription(
key="buffer_top_0_temperature",
translation_key="buffer_stage_top_temperature",
translation_placeholders={"tank": "1"},
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="buffer_top_1_temperature",
translation_key="buffer_stage_top_temperature",
translation_placeholders={"tank": "2"},
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="buffer_top_2_temperature",
translation_key="buffer_stage_top_temperature",
translation_placeholders={"tank": "3"},
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="buffer_center_temperature",
translation_key="buffer_center_temperature",
@@ -70,40 +116,241 @@ GUNTAMATIC_SENSORS: list[SensorEntityDescription] = [
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
SensorEntityDescription(
key="buffer_bottom_0_temperature",
translation_key="buffer_stage_bottom_temperature",
translation_placeholders={"tank": "1"},
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="buffer_bottom_1_temperature",
translation_key="buffer_stage_bottom_temperature",
translation_placeholders={"tank": "2"},
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="buffer_bottom_2_temperature",
translation_key="buffer_stage_bottom_temperature",
translation_placeholders={"tank": "3"},
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="domestic_hot_water_0_temperature",
translation_key="domestic_hot_water_0_temperature",
translation_key="domestic_hot_water_temperature",
translation_placeholders={"circuit": "1"},
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
SensorEntityDescription(
key="room_0_temperature",
translation_key="room_0_temperature",
key="domestic_hot_water_1_temperature",
translation_key="domestic_hot_water_temperature",
translation_placeholders={"circuit": "2"},
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
SensorEntityDescription(
key="room_1_temperature",
translation_key="room_1_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
SensorEntityDescription(
key="room_2_temperature",
translation_key="room_2_temperature",
key="domestic_hot_water_2_temperature",
translation_key="domestic_hot_water_temperature",
translation_placeholders={"circuit": "3"},
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
*[
SensorEntityDescription(
key=f"room_{i}_temperature",
translation_key="room_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
)
for i in range(9)
],
SensorEntityDescription(
key="buffer_load",
translation_key="buffer_load",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
),
SensorEntityDescription(
key="boiler_shunt_pump",
translation_key="boiler_shunt_pump",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
entity_registry_enabled_default=False,
),
*[
SensorEntityDescription(
key=f"auxiliary_pump_{nr}",
translation_key="auxiliary_pump",
translation_placeholders={"pump": str(nr + 1)},
device_class=SensorDeviceClass.ENUM,
options=[
"auto",
"off",
"nonstop",
],
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
)
for nr in range(3)
],
SensorEntityDescription(
key="suction_fan",
translation_key="suction_fan",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="primary_air",
translation_key="primary_air",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
key="secondary_air",
translation_key="secondary_air",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(
# This is CO2 content in a flue. It is measured in % and goes really high.
# It does not make sense to measure this as ppm as one does for air quality.
key="co2_content",
translation_key="co2_content",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
),
*[
SensorEntityDescription(
key=f"dhw_pump_{nr}",
translation_key="dhw_pump",
translation_placeholders={"circuit": str(nr + 1)},
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
)
for nr in range(3)
],
*[
SensorEntityDescription(
key=f"extra_dhw_{nr}_temperature",
translation_key="extra_dhw_temperature",
translation_placeholders={"circuit": str(nr)},
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
entity_registry_enabled_default=False,
)
for nr in range(3)
],
*[
SensorEntityDescription(
key=f"extra_dhw_boost_{nr}",
translation_key="extra_dhw_boost",
translation_placeholders={"pump": str(nr + 1)},
device_class=SensorDeviceClass.ENUM,
options=[
"auto",
"off",
"nonstop",
],
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
)
for nr in range(3)
],
*[
SensorEntityDescription(
key=f"heating_circulation_pump_{i}",
translation_key="heating_circulation_pump",
device_class=SensorDeviceClass.ENUM,
options=[
"auto",
"off",
"nonstop",
],
entity_category=EntityCategory.DIAGNOSTIC,
)
for i in range(9)
],
*[
SensorEntityDescription(
key=f"circuit_{i}_temp",
translation_key="circuit_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
entity_category=EntityCategory.DIAGNOSTIC,
)
for i in range(9)
],
*[
SensorEntityDescription(
key=f"heating_circulation_program_{i}",
translation_key="heating_circulation_program",
device_class=SensorDeviceClass.ENUM,
options=[
"off",
"timer",
"heat",
"hibernate",
"hibernate_to",
],
entity_registry_enabled_default=False,
)
for i in range(9)
],
SensorEntityDescription(
key="interruption_1",
translation_key="interruption",
translation_placeholders={"number": "1"},
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="interruption_2",
translation_key="interruption",
translation_placeholders={"number": "2"},
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="operating_time",
translation_key="operating_time",
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.TOTAL,
native_unit_of_measurement=UnitOfTime.HOURS,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="service_days",
translation_key="service_date",
device_class=SensorDeviceClass.DATE,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
]
@@ -114,13 +361,35 @@ async def async_setup_entry(
) -> None:
"""Set up Guntamatic sensors from config entry."""
coordinator = entry.runtime_data
serial = coordinator.data["serial"][0]
async_add_entities(
GuntamaticSensor(coordinator, description)
for description in GUNTAMATIC_SENSORS
if description.key in coordinator.data
device_registry = dr.async_get(hass)
main_device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, serial)},
manufacturer="Guntamatic",
serial_number=serial,
sw_version=coordinator.data["version"][0],
)
entities: list[GuntamaticSensor] = []
for description in GUNTAMATIC_SENSORS:
if description.key not in coordinator.data:
continue
if match := HEATING_CIRCUIT_REGEX.match(description.key):
circuit = int(match.group(1))
identifiers = {(DOMAIN, f"{serial}_hc{circuit}")}
device_info: DeviceInfo | ChildDeviceInfo = ChildDeviceInfo(
identifiers=identifiers,
parent_device_id=main_device.id,
name=f"Heating circuit {circuit}",
)
else:
device_info = DeviceInfo(identifiers={(DOMAIN, serial)})
entities.append(GuntamaticSensor(coordinator, description, device_info))
async_add_entities(entities)
class GuntamaticSensor(CoordinatorEntity[GuntamaticCoordinator], SensorEntity):
"""Representation of a single Guntamatic sensor."""
@@ -131,6 +400,7 @@ class GuntamaticSensor(CoordinatorEntity[GuntamaticCoordinator], SensorEntity):
self,
coordinator: GuntamaticCoordinator,
entity_description: SensorEntityDescription,
device_info: DeviceInfo | ChildDeviceInfo,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
@@ -139,16 +409,28 @@ class GuntamaticSensor(CoordinatorEntity[GuntamaticCoordinator], SensorEntity):
serial = coordinator.data["serial"][0]
self._attr_unique_id = f"{serial.replace('.', '_')}_{entity_description.key}"
self._attr_device_info = device_info
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial)},
manufacturer="Guntamatic",
serial_number=serial,
sw_version=coordinator.data["version"][0],
@property
@override
def available(self) -> bool:
"""Return whether the entity is available."""
return (
super().available and self.entity_description.key in self.coordinator.data
)
@property
@override
def native_value(self) -> StateType:
def native_value(self) -> StateType | date:
"""Return the current value of the sensor."""
return self.coordinator.data[self.entity_description.key][0]
value = self.coordinator.data[self.entity_description.key][0]
if self.entity_description.device_class is SensorDeviceClass.DATE:
return (dt_util.now() + timedelta(days=float(value))).date()
if (
self.entity_description.device_class is SensorDeviceClass.ENUM
and value not in (self.entity_description.options or [])
):
# The library passes through unmapped values of new firmware
# languages; expose them as unknown instead of raising.
return None
return value
@@ -27,9 +27,29 @@
},
"entity": {
"sensor": {
"auxiliary_pump": {
"name": "Auxiliary pump {pump}",
"state": {
"auto": "Auto",
"nonstop": "Non-stop",
"off": "Off"
}
},
"boiler_shunt_pump": {
"name": "Boiler shunt pump"
},
"boiler_temperature": {
"name": "Boiler temperature"
},
"buffer_bottom_0_temperature": {
"name": "Buffer 0 bottom temperature"
},
"buffer_bottom_1_temperature": {
"name": "Buffer 1 bottom temperature"
},
"buffer_bottom_2_temperature": {
"name": "Buffer 2 bottom temperature"
},
"buffer_bottom_temperature": {
"name": "Buffer bottom temperature"
},
@@ -39,15 +59,98 @@
"buffer_load": {
"name": "Buffer load"
},
"buffer_stage_bottom_temperature": {
"name": "Buffer stage {tank} bottom temperature"
},
"buffer_stage_top_temperature": {
"name": "Buffer stage {tank} top temperature"
},
"buffer_top_0_temperature": {
"name": "Buffer 0 top temperature"
},
"buffer_top_1_temperature": {
"name": "Buffer 1 top temperature"
},
"buffer_top_2_temperature": {
"name": "Buffer 2 top temperature"
},
"buffer_top_temperature": {
"name": "Buffer top temperature"
},
"burner_output": {
"name": "Burner output"
},
"circuit_temperature": {
"name": "Flow temperature"
},
"co2_content": {
"name": "Flue CO2 content"
},
"dhw_pump": {
"name": "DHW pump {circuit}"
},
"domestic_hot_water_0_temperature": {
"name": "Domestic hot water circuit 0 temperature"
},
"domestic_hot_water_1_temperature": {
"name": "Domestic hot water circuit 1 temperature"
},
"domestic_hot_water_2_temperature": {
"name": "Domestic hot water circuit 2 temperature"
},
"domestic_hot_water_temperature": {
"name": "Domestic hot water circuit {circuit} temperature"
},
"extra_dhw_1_temperature": {
"name": "Extra DHW 1 temperature"
},
"extra_dhw_2_temperature": {
"name": "Extra DHW 2 temperature"
},
"extra_dhw_boost": {
"name": "Extra DHW pump {pump}",
"state": {
"auto": "Auto",
"nonstop": "Non-stop",
"off": "Off"
}
},
"extra_dhw_temperature": {
"name": "Extra DHW circuit {circuit} temperature"
},
"flue_gas_actuator_target": {
"name": "Flue gas actuator target"
},
"heating_circulation_program": {
"name": "Program",
"state": {
"heat": "Heat",
"hibernate": "Setback mode",
"hibernate_to": "Away mode",
"off": "Off",
"timer": "Timer"
}
},
"heating_circulation_pump": {
"name": "Pump",
"state": {
"auto": "Auto",
"nonstop": "Non-stop",
"off": "Off"
}
},
"interruption": {
"name": "Interruption {number}"
},
"operating_time": {
"name": "Operating time"
},
"outdoor_temperature": {
"name": "Outdoor temperature"
},
"primary_air": {
"name": "Primary air"
},
"program": {
"name": "Program",
"state": {
@@ -60,17 +163,20 @@
"timer": "Timer"
}
},
"room_0_temperature": {
"name": "Room 0 temperature"
"room_temperature": {
"name": "Room temperature"
},
"room_1_temperature": {
"name": "Room 1 temperature"
"secondary_air": {
"name": "Secondary air"
},
"room_2_temperature": {
"name": "Room 2 temperature"
"service_date": {
"name": "Service date"
},
"status": {
"name": "Status"
},
"suction_fan": {
"name": "Suction fan"
}
}
}
+27
View File
@@ -28,6 +28,33 @@ MOCK_PARSE_DATA = {
"status": ["Service Ign.", ""],
"serial": ["959103", ""],
"version": ["32a", ""],
"heating_circulation_pump_1": ["auto", ""],
"heating_circulation_program_1": ["heat", ""],
"auxiliary_pump_0": ["nonstop", ""],
"auxiliary_pump_1": ["auto", ""],
"room_1_temperature": ["21.50", "°C"],
"circuit_1_temp": ["55.00", "°C"],
"operating_time": ["1188", "h"],
"service_days": ["2012", "d"],
"interruption_1": ["", ""],
"interruption_2": ["", ""],
"extra_dhw_0_temperature": ["53.00", "°C"],
"extra_dhw_1_temperature": ["52.10", "°C"],
"extra_dhw_2_temperature": ["51.80", "°C"],
"extra_dhw_boost_0": ["auto", ""],
"extra_dhw_boost_1": ["off", ""],
"buffer_top_temperature": ["65.40", "°C"],
"buffer_top_0_temperature": ["64.10", "°C"],
"buffer_center_temperature": ["58.20", "°C"],
"buffer_bottom_temperature": ["45.30", "°C"],
"buffer_bottom_0_temperature": ["44.80", "°C"],
"boiler_shunt_pump": ["45", "%"],
"suction_fan": ["78", "%"],
"primary_air": ["60", "%"],
"secondary_air": ["35", "%"],
"co2_content": ["12", "%"],
"dhw_pump_0": ["30", "%"],
"dhw_pump_1": ["25", "%"],
}
File diff suppressed because it is too large Load Diff
+188 -4
View File
@@ -1,19 +1,29 @@
"""Tests for the Guntamatic sensor platform."""
from datetime import datetime, timedelta
from unittest.mock import MagicMock, patch
from freezegun.api import FrozenDateTimeFactory
from guntamatic.heater import NoSerialException
from guntamatic.heater import (
TRANSLATE_HC_PROGRAM,
TRANSLATE_PUMP_MODE,
NoSerialException,
)
import pytest
import requests
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.guntamatic.const import SCAN_INTERVAL
from homeassistant.const import STATE_UNAVAILABLE, Platform
from homeassistant.components.guntamatic.const import DOMAIN, SCAN_INTERVAL
from homeassistant.components.guntamatic.sensor import GUNTAMATIC_SENSORS
from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.translation import async_get_translations
from homeassistant.util import dt as dt_util
from . import setup_integration
from .conftest import MOCK_PARSE_DATA
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
@@ -24,13 +34,35 @@ async def test_all_entities(
snapshot: SnapshotAssertion,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test all entities."""
# The service date sensor computes now() + a day count; freeze the clock
# so the snapshot does not depend on the day the tests run on.
freezer.move_to("2030-01-01 12:00:00+00:00")
with patch(
"homeassistant.components.guntamatic._PLATFORMS",
[Platform.SENSOR],
):
await setup_integration(hass, mock_config_entry)
disabled_entries = [
entity_entry
for entity_entry in er.async_entries_for_config_entry(
entity_registry, mock_config_entry.entry_id
)
if entity_entry.disabled_by is not None
]
assert disabled_entries
assert all(
entity_entry.disabled_by == er.RegistryEntryDisabler.INTEGRATION
for entity_entry in disabled_entries
)
for entity_entry in disabled_entries:
entity_registry.async_update_entity(entity_entry.entity_id, disabled_by=None)
await hass.config_entries.async_reload(mock_config_entry.entry_id)
await hass.async_block_till_done()
await snapshot_platform(
hass,
entity_registry,
@@ -73,3 +105,155 @@ async def test_state_unavailable(
await hass.async_block_till_done()
state = hass.states.get("sensor.mock_title_boiler_temperature")
assert state.state != STATE_UNAVAILABLE
@pytest.mark.parametrize(
("key_prefix", "translation_table"),
[
pytest.param("heating_circulation_program", TRANSLATE_HC_PROGRAM, id="program"),
pytest.param(
"heating_circulation_pump",
TRANSLATE_PUMP_MODE,
id="heating_circulation_pump",
),
pytest.param("auxiliary_pump", TRANSLATE_PUMP_MODE, id="auxiliary_pump"),
pytest.param("extra_dhw_boost", TRANSLATE_PUMP_MODE, id="extra_dhw_boost"),
],
)
def test_enum_options_match_library(
key_prefix: str,
translation_table: dict[str, str],
) -> None:
"""Test enum options mirror the canonical values emitted by the library."""
option_sets = {
tuple(description.options)
for description in GUNTAMATIC_SENSORS
if description.key.startswith(key_prefix)
and description.device_class is SensorDeviceClass.ENUM
}
assert len(option_sets) == 1, f"Inconsistent options across {key_prefix} sensors"
assert set(option_sets.pop()) == set(translation_table.values())
enum_descriptions = [
description
for description in GUNTAMATIC_SENSORS
if description.device_class is SensorDeviceClass.ENUM
]
@pytest.mark.parametrize(
"description",
enum_descriptions,
ids=lambda description: description.key,
)
async def test_enum_states_translated(
hass: HomeAssistant,
description: SensorEntityDescription,
) -> None:
"""Test every option of an enum sensor has a state translation."""
translations = await async_get_translations(hass, "en", "entity", [DOMAIN])
prefix = f"component.{DOMAIN}.entity.sensor.{description.translation_key}.state."
for option in description.options:
assert f"{prefix}{option}" in translations
async def test_enum_sensor_unmapped_value(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_heater: MagicMock,
) -> None:
"""Test an unmapped enum value is exposed as unknown instead of raising."""
return_value = MOCK_PARSE_DATA.copy()
return_value["heating_circulation_pump_1"] = ["BROKEN", ""]
mock_heater.parse_data.return_value = return_value
await setup_integration(hass, mock_config_entry)
await hass.async_block_till_done()
state = hass.states.get("sensor.heating_circuit_1_pump")
assert state is not None
assert state.state == STATE_UNKNOWN
_SERIAL = "959103"
@pytest.mark.usefixtures("mock_heater")
async def test_heating_circuit_devices(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test circuit sub-devices link to the main unit and absent slots create none."""
await setup_integration(hass, mock_config_entry)
main = device_registry.async_get_device_by_identifier(
(DOMAIN, _SERIAL), mock_config_entry.entry_id
)
circuit = device_registry.async_get_child_device_by_identifier(
(DOMAIN, f"{_SERIAL}_hc1"), mock_config_entry.entry_id
)
assert main is not None
assert circuit is not None
assert circuit.parent_device_id == main.id
assert (
device_registry.async_get_device_by_identifier(
(DOMAIN, f"{_SERIAL}_hc2"), mock_config_entry.entry_id
)
is None
)
async def test_key_disappearing_makes_entity_unavailable(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_heater: MagicMock,
) -> None:
"""Test an individual sensor becoming unavailable when its key disappears."""
await setup_integration(hass, mock_config_entry)
entity_id = "sensor.mock_title_boiler_temperature"
assert hass.states.get(entity_id).state == "14.09"
# Simulate the heater no longer reporting this key
reduced = {k: v for k, v in MOCK_PARSE_DATA.items() if k != "boiler_temperature"}
mock_heater.parse_data.return_value = reduced
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == "unavailable"
# Restore the key and verify the entity recovers
mock_heater.parse_data.return_value = MOCK_PARSE_DATA.copy()
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == "14.09"
async def test_service_date_fractional_days(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_heater: MagicMock,
freezer: FrozenDateTimeFactory,
entity_registry: er.EntityRegistry,
) -> None:
"""Fractional service_days roll over correctly when time is not midnight."""
frozen = datetime(2026, 8, 26, 15, 00, tzinfo=dt_util.get_default_time_zone())
freezer.move_to(frozen)
data = MOCK_PARSE_DATA.copy()
data["service_days"] = ["1.5", "d"]
mock_heater.parse_data.return_value = data
await setup_integration(hass, mock_config_entry)
entity_registry.async_update_entity(
"sensor.mock_title_service_date", disabled_by=None
)
await hass.config_entries.async_reload(mock_config_entry.entry_id)
await hass.async_block_till_done()
state = hass.states.get("sensor.mock_title_service_date")
assert state is not None
assert state.state == (frozen + timedelta(days=1.5)).date().isoformat()