mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Add Duco ventilation temperature sensors (#176621)
This commit is contained in:
@@ -10,8 +10,15 @@ from duco_connectivity.exceptions import (
|
||||
DucoConnectionError,
|
||||
DucoError,
|
||||
DucoResponseError,
|
||||
DucoUnsupportedCapabilityError,
|
||||
)
|
||||
from duco_connectivity.models import (
|
||||
BoardInfo,
|
||||
Node,
|
||||
NodeListActionItemList,
|
||||
NodeName,
|
||||
VentilationTemperatureInfo,
|
||||
)
|
||||
from duco_connectivity.models import BoardInfo, Node, NodeListActionItemList, NodeName
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -26,7 +33,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
type DucoConfigEntry = ConfigEntry[DucoCoordinator]
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class DucoData:
|
||||
"""Data returned by the Duco coordinator."""
|
||||
|
||||
@@ -34,6 +41,7 @@ class DucoData:
|
||||
node_actions: NodeListActionItemList
|
||||
rssi_wifi: int | None
|
||||
time_filter_remain: int | None
|
||||
ventilation_temperatures: VentilationTemperatureInfo | None
|
||||
|
||||
|
||||
class DucoCoordinator(DataUpdateCoordinator[DucoData]):
|
||||
@@ -42,6 +50,7 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
|
||||
config_entry: DucoConfigEntry
|
||||
board_info: BoardInfo
|
||||
_supports_time_filter_remain: bool
|
||||
_supports_ventilation_temperatures: bool
|
||||
_configured_node_names: dict[int, str]
|
||||
|
||||
def __init__(
|
||||
@@ -61,6 +70,7 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
|
||||
self.client = client
|
||||
self._configured_node_names = {}
|
||||
self._supports_time_filter_remain = True
|
||||
self._supports_ventilation_temperatures = True
|
||||
|
||||
async def _async_load_node_names(self) -> None:
|
||||
"""Load configured Duco node names during setup."""
|
||||
@@ -175,9 +185,26 @@ class DucoCoordinator(DataUpdateCoordinator[DucoData]):
|
||||
time_filter_remain = await self.client.async_get_time_filter_remaining()
|
||||
self._supports_time_filter_remain = time_filter_remain is not None
|
||||
|
||||
ventilation_temperatures = (
|
||||
self.data.ventilation_temperatures if self.data else None
|
||||
)
|
||||
if self._supports_ventilation_temperatures:
|
||||
try:
|
||||
ventilation_temperatures = (
|
||||
await self.client.async_get_ventilation_temperature_info()
|
||||
)
|
||||
except DucoUnsupportedCapabilityError:
|
||||
ventilation_temperatures = None
|
||||
self._supports_ventilation_temperatures = False
|
||||
except DucoError as err:
|
||||
_LOGGER.debug(
|
||||
"Could not fetch Duco ventilation temperatures", exc_info=err
|
||||
)
|
||||
|
||||
return DucoData(
|
||||
nodes={node.node_id: node for node in nodes},
|
||||
node_actions=node_actions,
|
||||
rssi_wifi=rssi_wifi,
|
||||
time_filter_remain=time_filter_remain,
|
||||
ventilation_temperatures=ventilation_temperatures,
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.const import (
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
EntityCategory,
|
||||
UnitOfRatio,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -156,6 +157,70 @@ BOX_SENSOR_DESCRIPTIONS: tuple[DucoBoxSensorEntityDescription, ...] = (
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda coordinator: coordinator.data.rssi_wifi,
|
||||
),
|
||||
DucoBoxSensorEntityDescription(
|
||||
key="outdoor_air_temperature",
|
||||
translation_key="outdoor_air_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
supported_fn=lambda coordinator: (
|
||||
coordinator.data.ventilation_temperatures is not None
|
||||
and coordinator.data.ventilation_temperatures.temp_oda is not None
|
||||
),
|
||||
value_fn=lambda coordinator: (
|
||||
coordinator.data.ventilation_temperatures.temp_oda
|
||||
if coordinator.data.ventilation_temperatures
|
||||
else None
|
||||
),
|
||||
),
|
||||
DucoBoxSensorEntityDescription(
|
||||
key="supply_air_temperature",
|
||||
translation_key="supply_air_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
supported_fn=lambda coordinator: (
|
||||
coordinator.data.ventilation_temperatures is not None
|
||||
and coordinator.data.ventilation_temperatures.temp_sup is not None
|
||||
),
|
||||
value_fn=lambda coordinator: (
|
||||
coordinator.data.ventilation_temperatures.temp_sup
|
||||
if coordinator.data.ventilation_temperatures
|
||||
else None
|
||||
),
|
||||
),
|
||||
DucoBoxSensorEntityDescription(
|
||||
key="extract_air_temperature",
|
||||
translation_key="extract_air_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
supported_fn=lambda coordinator: (
|
||||
coordinator.data.ventilation_temperatures is not None
|
||||
and coordinator.data.ventilation_temperatures.temp_eta is not None
|
||||
),
|
||||
value_fn=lambda coordinator: (
|
||||
coordinator.data.ventilation_temperatures.temp_eta
|
||||
if coordinator.data.ventilation_temperatures
|
||||
else None
|
||||
),
|
||||
),
|
||||
DucoBoxSensorEntityDescription(
|
||||
key="exhaust_air_temperature",
|
||||
translation_key="exhaust_air_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
supported_fn=lambda coordinator: (
|
||||
coordinator.data.ventilation_temperatures is not None
|
||||
and coordinator.data.ventilation_temperatures.temp_eha is not None
|
||||
),
|
||||
value_fn=lambda coordinator: (
|
||||
coordinator.data.ventilation_temperatures.temp_eha
|
||||
if coordinator.data.ventilation_temperatures
|
||||
else None
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -73,6 +73,12 @@
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"exhaust_air_temperature": {
|
||||
"name": "Exhaust air temperature"
|
||||
},
|
||||
"extract_air_temperature": {
|
||||
"name": "Extract air temperature"
|
||||
},
|
||||
"filter_remaining": {
|
||||
"name": "Filter remaining"
|
||||
},
|
||||
@@ -82,6 +88,12 @@
|
||||
"iaq_rh": {
|
||||
"name": "Humidity air quality index"
|
||||
},
|
||||
"outdoor_air_temperature": {
|
||||
"name": "Outdoor air temperature"
|
||||
},
|
||||
"supply_air_temperature": {
|
||||
"name": "Supply air temperature"
|
||||
},
|
||||
"target_flow_level": {
|
||||
"name": "Target flow level"
|
||||
},
|
||||
|
||||
@@ -23,6 +23,7 @@ from duco_connectivity import (
|
||||
NodeMotorStateInfo,
|
||||
NodeSensorInfo,
|
||||
NodeVentilationInfo,
|
||||
VentilationTemperatureInfo,
|
||||
)
|
||||
import pytest
|
||||
|
||||
@@ -178,6 +179,17 @@ def mock_lan_info() -> LanInfo:
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ventilation_temperature_info() -> VentilationTemperatureInfo:
|
||||
"""Return mock ventilation temperatures in Celsius."""
|
||||
return VentilationTemperatureInfo(
|
||||
temp_oda=5.5,
|
||||
temp_sup=18.2,
|
||||
temp_eta=21.4,
|
||||
temp_eha=8.1,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_nodes() -> list[Node]:
|
||||
"""Return a list of nodes covering all supported types."""
|
||||
@@ -235,6 +247,7 @@ def mock_duco_client(
|
||||
mock_lan_info: LanInfo,
|
||||
mock_nodes: list[Node],
|
||||
mock_node_actions: NodeListActionItemList,
|
||||
mock_ventilation_temperature_info: VentilationTemperatureInfo,
|
||||
) -> Generator[AsyncMock]:
|
||||
"""Return a mocked DucoClient used by both the integration and config flow."""
|
||||
with (
|
||||
@@ -255,6 +268,9 @@ def mock_duco_client(
|
||||
client.async_get_node_configs.return_value = node_configs_from_nodes(mock_nodes)
|
||||
client.async_get_node_actions.return_value = mock_node_actions
|
||||
client.async_get_time_filter_remaining.return_value = 180
|
||||
client.async_get_ventilation_temperature_info.return_value = (
|
||||
mock_ventilation_temperature_info
|
||||
)
|
||||
client.async_get_diagnostics.return_value = [
|
||||
DiagComponent(component="Ventilation", status="Ok")
|
||||
]
|
||||
|
||||
@@ -835,6 +835,122 @@
|
||||
'state': '90',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.living_exhaust_air_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.living_exhaust_air_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Exhaust air temperature',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Exhaust air temperature',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'exhaust_air_temperature',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_1_exhaust_air_temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.living_exhaust_air_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'temperature',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Living Exhaust air temperature',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.living_exhaust_air_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '8.1',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.living_extract_air_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.living_extract_air_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Extract air temperature',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Extract air temperature',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'extract_air_temperature',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_1_extract_air_temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.living_extract_air_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'temperature',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Living Extract air temperature',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.living_extract_air_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '21.4',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.living_filter_remaining-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
@@ -890,6 +1006,64 @@
|
||||
'state': '180',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.living_outdoor_air_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.living_outdoor_air_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Outdoor air temperature',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Outdoor air temperature',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'outdoor_air_temperature',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_1_outdoor_air_temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.living_outdoor_air_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'temperature',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Living Outdoor air temperature',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.living_outdoor_air_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '5.5',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.living_signal_strength-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
@@ -996,6 +1170,64 @@
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.living_supply_air_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.living_supply_air_temperature',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Supply air temperature',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Supply air temperature',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'supply_air_temperature',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_1_supply_air_temperature',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.living_supply_air_temperature-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'temperature',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Living Supply air temperature',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.living_supply_air_temperature',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '18.2',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.living_target_flow_level-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -15,10 +15,12 @@ from duco_connectivity import (
|
||||
LanInfo,
|
||||
Node,
|
||||
NodeListActionItemList,
|
||||
VentilationTemperatureInfo,
|
||||
)
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.duco.const import SCAN_INTERVAL
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -158,6 +160,42 @@ async def test_setup_entry_ignores_lan_info_failures(
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception",
|
||||
[
|
||||
pytest.param(DucoError("API error"), id="duco_error"),
|
||||
pytest.param(DucoConnectionError("Connection refused"), id="connection_error"),
|
||||
],
|
||||
)
|
||||
async def test_setup_entry_recovers_from_optional_temperature_capability_failure(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test an optional temperature capability is retried after a setup failure."""
|
||||
mock_duco_client.async_get_ventilation_temperature_info.side_effect = [
|
||||
exception,
|
||||
VentilationTemperatureInfo(temp_oda=5.5),
|
||||
]
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
assert hass.states.get("sensor.living_outdoor_air_temperature") is None
|
||||
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get("sensor.living_outdoor_air_temperature")
|
||||
assert state is not None
|
||||
assert state.state == "5.5"
|
||||
|
||||
|
||||
async def test_setup_entry_ignores_node_name_config_failures(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
@@ -280,6 +318,9 @@ async def test_setup_entry_creates_http_client(
|
||||
mock_client_class.return_value.async_get_node_actions.return_value = (
|
||||
mock_node_actions
|
||||
)
|
||||
(
|
||||
mock_client_class.return_value.async_get_ventilation_temperature_info.return_value
|
||||
) = VentilationTemperatureInfo()
|
||||
mock_client_class.return_value.async_get_diagnostics.return_value = [
|
||||
DiagComponent(component="Ventilation", status="Ok")
|
||||
]
|
||||
|
||||
@@ -7,12 +7,14 @@ from unittest.mock import AsyncMock
|
||||
from duco_connectivity import (
|
||||
DucoConnectionError,
|
||||
DucoError,
|
||||
DucoUnsupportedCapabilityError,
|
||||
Node,
|
||||
NodeGeneralInfo,
|
||||
NodeSensorInfo,
|
||||
NodeType,
|
||||
NodeVentilationInfo,
|
||||
VentilationState,
|
||||
VentilationTemperatureInfo,
|
||||
)
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
@@ -28,6 +30,12 @@ from . import setup_platform_integration
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
FILTER_REMAINING_ENTITY_ID = "sensor.living_filter_remaining"
|
||||
VENTILATION_TEMPERATURE_ENTITY_IDS = (
|
||||
"sensor.living_outdoor_air_temperature",
|
||||
"sensor.living_supply_air_temperature",
|
||||
"sensor.living_extract_air_temperature",
|
||||
"sensor.living_exhaust_air_temperature",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -223,6 +231,55 @@ async def test_time_filter_remaining_missing_skips_sensor_creation(
|
||||
assert hass.states.get(FILTER_REMAINING_ENTITY_ID) is None
|
||||
|
||||
|
||||
async def test_ventilation_temperatures_missing_skip_sensor_creation(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test unsupported ventilation temperatures never expose temperature states."""
|
||||
mock_duco_client.async_get_ventilation_temperature_info.side_effect = [
|
||||
DucoUnsupportedCapabilityError(400, "/info", '{"Code":3,"Result":"FAILED"}'),
|
||||
VentilationTemperatureInfo(temp_oda=5.5),
|
||||
]
|
||||
|
||||
await setup_platform_integration(hass, mock_config_entry, [Platform.SENSOR])
|
||||
|
||||
for entity_id in VENTILATION_TEMPERATURE_ENTITY_IDS:
|
||||
assert hass.states.get(entity_id) is None
|
||||
|
||||
freezer.tick(SCAN_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
for entity_id in VENTILATION_TEMPERATURE_ENTITY_IDS:
|
||||
assert hass.states.get(entity_id) is None
|
||||
|
||||
|
||||
async def test_partial_ventilation_temperatures_only_expose_available_sensor_values(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test only populated ventilation temperature fields are exposed as states."""
|
||||
mock_duco_client.async_get_ventilation_temperature_info.return_value = (
|
||||
VentilationTemperatureInfo(temp_oda=5.5, temp_eta=21.4)
|
||||
)
|
||||
|
||||
await setup_platform_integration(hass, mock_config_entry, [Platform.SENSOR])
|
||||
|
||||
state = hass.states.get("sensor.living_outdoor_air_temperature")
|
||||
assert state is not None
|
||||
assert state.state == "5.5"
|
||||
|
||||
state = hass.states.get("sensor.living_extract_air_temperature")
|
||||
assert state is not None
|
||||
assert state.state == "21.4"
|
||||
|
||||
assert hass.states.get("sensor.living_supply_air_temperature") is None
|
||||
assert hass.states.get("sensor.living_exhaust_air_temperature") is None
|
||||
|
||||
|
||||
async def test_time_filter_remaining_transient_failure_recovers_sensor_creation(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
|
||||
Reference in New Issue
Block a user