mirror of
https://github.com/home-assistant/core.git
synced 2026-09-24 15:31:52 -05:00
Add all remaining guntamatic sensors (#179770)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user