From 7f2f268fca339cd989edbc9d53479bdf0dd1fd9b Mon Sep 17 00:00:00 2001 From: Ronald van der Meer Date: Wed, 20 May 2026 16:21:32 +0200 Subject: [PATCH] Fix Duco VLV nodes not creating CO2 and humidity sensors (#171182) --- homeassistant/components/duco/sensor.py | 8 +- tests/components/duco/conftest.py | 42 +- .../duco/fixtures/dynamic_sensor_nodes.json | 54 +++ .../duco/fixtures/sensor_nodes.json | 80 ++++ .../duco/snapshots/test_sensor.ambr | 436 ++++++++++++++++++ tests/components/duco/test_sensor.py | 89 ++-- 6 files changed, 661 insertions(+), 48 deletions(-) create mode 100644 tests/components/duco/fixtures/dynamic_sensor_nodes.json create mode 100644 tests/components/duco/fixtures/sensor_nodes.json diff --git a/homeassistant/components/duco/sensor.py b/homeassistant/components/duco/sensor.py index d7d2556481d0..fb82acea0670 100644 --- a/homeassistant/components/duco/sensor.py +++ b/homeassistant/components/duco/sensor.py @@ -95,7 +95,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, value_fn=lambda node: node.sensor.co2 if node.sensor else None, - node_types=(NodeType.UCCO2,), + node_types=(NodeType.UCCO2, NodeType.VLVCO2, NodeType.VLVCO2RH), ), DucoSensorEntityDescription( key="iaq_co2", @@ -104,7 +104,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, value_fn=lambda node: node.sensor.iaq_co2 if node.sensor else None, - node_types=(NodeType.UCCO2,), + node_types=(NodeType.UCCO2, NodeType.VLVCO2, NodeType.VLVCO2RH), ), DucoSensorEntityDescription( key="humidity", @@ -112,7 +112,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, value_fn=lambda node: node.sensor.rh if node.sensor else None, - node_types=(NodeType.BSRH, NodeType.UCRH), + node_types=(NodeType.BSRH, NodeType.UCRH, NodeType.VLVRH, NodeType.VLVCO2RH), ), DucoSensorEntityDescription( key="iaq_rh", @@ -121,7 +121,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, value_fn=lambda node: node.sensor.iaq_rh if node.sensor else None, - node_types=(NodeType.BSRH, NodeType.UCRH), + node_types=(NodeType.BSRH, NodeType.UCRH, NodeType.VLVRH, NodeType.VLVCO2RH), ), ) diff --git a/tests/components/duco/conftest.py b/tests/components/duco/conftest.py index 760096fda5fe..541cf39a5877 100644 --- a/tests/components/duco/conftest.py +++ b/tests/components/duco/conftest.py @@ -1,6 +1,7 @@ """Fixtures for Duco tests.""" from collections.abc import Generator +from typing import Any from unittest.mock import AsyncMock, patch from duco_connectivity import ( @@ -12,6 +13,7 @@ from duco_connectivity import ( LanInfo, Node, NodeGeneralInfo, + NodeMotorStateInfo, NodeSensorInfo, NodeVentilationInfo, ) @@ -21,7 +23,7 @@ from homeassistant.components.duco.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_json_array_fixture TEST_HOST = "192.168.1.100" TEST_MAC = "aa:bb:cc:dd:ee:ff" @@ -29,6 +31,30 @@ TEST_MAC = "aa:bb:cc:dd:ee:ff" USER_INPUT = {CONF_HOST: TEST_HOST} +def _node_from_dict(data: dict[str, Any]) -> Node: + """Convert a node fixture payload into a Duco node model.""" + ventilation = data.get("ventilation") + sensor = data.get("sensor") + motor_state = data.get("motor_state") + + return Node( + node_id=data["node_id"], + general=NodeGeneralInfo(**data["general"]), + ventilation=NodeVentilationInfo(**ventilation) + if ventilation is not None + else None, + sensor=NodeSensorInfo(**sensor) if sensor is not None else None, + motor_state=NodeMotorStateInfo(**motor_state) + if motor_state is not None + else None, + ) + + +def load_nodes_fixture(filename: str) -> list[Node]: + """Load nodes from a JSON fixture file.""" + return [_node_from_dict(node) for node in load_json_array_fixture(filename, DOMAIN)] + + @pytest.fixture def mock_config_entry() -> MockConfigEntry: """Return the default mocked config entry.""" @@ -199,6 +225,20 @@ def mock_nodes() -> list[Node]: ] +@pytest.fixture +def mock_sensor_nodes(mock_nodes: list[Node]) -> list[Node]: + """Return sensor test nodes including VLV examples.""" + return [*mock_nodes, *load_nodes_fixture("sensor_nodes.json")] + + +@pytest.fixture +def dynamic_sensor_nodes() -> dict[int, Node]: + """Return dynamic sensor test nodes keyed by node ID.""" + return { + node.node_id: node for node in load_nodes_fixture("dynamic_sensor_nodes.json") + } + + @pytest.fixture def mock_duco_client( mock_api_info: ApiInfo, diff --git a/tests/components/duco/fixtures/dynamic_sensor_nodes.json b/tests/components/duco/fixtures/dynamic_sensor_nodes.json new file mode 100644 index 000000000000..bdad2a06363c --- /dev/null +++ b/tests/components/duco/fixtures/dynamic_sensor_nodes.json @@ -0,0 +1,54 @@ +[ + { + "node_id": 200, + "general": { + "node_type": "BSRH", + "sub_type": 0, + "network_type": "RF", + "parent": 1, + "asso": 1, + "name": "New RH sensor", + "identify": 0 + }, + "ventilation": { + "state": "AUTO", + "time_state_remain": 0, + "time_state_end": 0, + "mode": "-", + "flow_lvl_tgt": null + }, + "sensor": { + "co2": null, + "iaq_co2": null, + "rh": 55.0, + "iaq_rh": 70, + "temp": 21.0 + } + }, + { + "node_id": 201, + "general": { + "node_type": "VLVCO2", + "sub_type": 0, + "network_type": "RF", + "parent": 1, + "asso": 1, + "name": "New valve", + "identify": 0 + }, + "ventilation": { + "state": "AUTO", + "time_state_remain": 0, + "time_state_end": 0, + "mode": "-", + "flow_lvl_tgt": null + }, + "sensor": { + "co2": 575, + "iaq_co2": 82, + "rh": null, + "iaq_rh": null, + "temp": 20.5 + } + } +] diff --git a/tests/components/duco/fixtures/sensor_nodes.json b/tests/components/duco/fixtures/sensor_nodes.json new file mode 100644 index 000000000000..b203b3a42685 --- /dev/null +++ b/tests/components/duco/fixtures/sensor_nodes.json @@ -0,0 +1,80 @@ +[ + { + "node_id": 60, + "general": { + "node_type": "VLVRH", + "sub_type": 0, + "network_type": "WI", + "parent": 1, + "asso": 1, + "name": "Bedroom valve", + "identify": 0 + }, + "ventilation": { + "state": "AUTO", + "time_state_remain": 0, + "time_state_end": 0, + "mode": "-", + "flow_lvl_tgt": null + }, + "sensor": { + "co2": null, + "iaq_co2": null, + "rh": 48.0, + "iaq_rh": 88, + "temp": 21.3 + } + }, + { + "node_id": 61, + "general": { + "node_type": "VLVCO2", + "sub_type": 0, + "network_type": "WI", + "parent": 1, + "asso": 1, + "name": "Hall valve", + "identify": 0 + }, + "ventilation": { + "state": "AUTO", + "time_state_remain": 0, + "time_state_end": 0, + "mode": "-", + "flow_lvl_tgt": null + }, + "sensor": { + "co2": 512, + "iaq_co2": 76, + "rh": null, + "iaq_rh": null, + "temp": 20.1 + } + }, + { + "node_id": 62, + "general": { + "node_type": "VLVCO2RH", + "sub_type": 0, + "network_type": "WI", + "parent": 1, + "asso": 1, + "name": "Study valve", + "identify": 0 + }, + "ventilation": { + "state": "AUTO", + "time_state_remain": 0, + "time_state_end": 0, + "mode": "-", + "flow_lvl_tgt": null + }, + "sensor": { + "co2": 645, + "iaq_co2": 95, + "rh": 54.0, + "iaq_rh": 92, + "temp": 20.7 + } + } +] diff --git a/tests/components/duco/snapshots/test_sensor.ambr b/tests/components/duco/snapshots/test_sensor.ambr index 89822eb8b159..9ef44a2c59d9 100644 --- a/tests/components/duco/snapshots/test_sensor.ambr +++ b/tests/components/duco/snapshots/test_sensor.ambr @@ -108,6 +108,224 @@ 'state': '85', }) # --- +# name: test_sensor_entities_state[sensor.bedroom_valve_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bedroom_valve_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Humidity', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'duco', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aa:bb:cc:dd:ee:ff_60_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_entities_state[sensor.bedroom_valve_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Bedroom valve Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.bedroom_valve_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '48.0', + }) +# --- +# name: test_sensor_entities_state[sensor.bedroom_valve_humidity_air_quality_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.bedroom_valve_humidity_air_quality_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Humidity air quality index', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Humidity air quality index', + 'platform': 'duco', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'iaq_rh', + 'unique_id': 'aa:bb:cc:dd:ee:ff_60_iaq_rh', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_entities_state[sensor.bedroom_valve_humidity_air_quality_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bedroom valve Humidity air quality index', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.bedroom_valve_humidity_air_quality_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '88', + }) +# --- +# name: test_sensor_entities_state[sensor.hall_valve_carbon_dioxide-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hall_valve_carbon_dioxide', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Carbon dioxide', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Carbon dioxide', + 'platform': 'duco', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aa:bb:cc:dd:ee:ff_61_co2', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_sensor_entities_state[sensor.hall_valve_carbon_dioxide-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_dioxide', + 'friendly_name': 'Hall valve Carbon dioxide', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.hall_valve_carbon_dioxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '512', + }) +# --- +# name: test_sensor_entities_state[sensor.hall_valve_co2_air_quality_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.hall_valve_co2_air_quality_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'CO2 air quality index', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CO2 air quality index', + 'platform': 'duco', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'iaq_co2', + 'unique_id': 'aa:bb:cc:dd:ee:ff_61_iaq_co2', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_entities_state[sensor.hall_valve_co2_air_quality_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Hall valve CO2 air quality index', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.hall_valve_co2_air_quality_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '76', + }) +# --- # name: test_sensor_entities_state[sensor.kitchen_rh_humidity-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -581,3 +799,221 @@ 'state': '80', }) # --- +# name: test_sensor_entities_state[sensor.study_valve_carbon_dioxide-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.study_valve_carbon_dioxide', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Carbon dioxide', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Carbon dioxide', + 'platform': 'duco', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aa:bb:cc:dd:ee:ff_62_co2', + 'unit_of_measurement': 'ppm', + }) +# --- +# name: test_sensor_entities_state[sensor.study_valve_carbon_dioxide-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'carbon_dioxide', + 'friendly_name': 'Study valve Carbon dioxide', + 'state_class': , + 'unit_of_measurement': 'ppm', + }), + 'context': , + 'entity_id': 'sensor.study_valve_carbon_dioxide', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '645', + }) +# --- +# name: test_sensor_entities_state[sensor.study_valve_co2_air_quality_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.study_valve_co2_air_quality_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'CO2 air quality index', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'CO2 air quality index', + 'platform': 'duco', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'iaq_co2', + 'unique_id': 'aa:bb:cc:dd:ee:ff_62_iaq_co2', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_entities_state[sensor.study_valve_co2_air_quality_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Study valve CO2 air quality index', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.study_valve_co2_air_quality_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '95', + }) +# --- +# name: test_sensor_entities_state[sensor.study_valve_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.study_valve_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Humidity', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'duco', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'aa:bb:cc:dd:ee:ff_62_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_entities_state[sensor.study_valve_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'Study valve Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.study_valve_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '54.0', + }) +# --- +# name: test_sensor_entities_state[sensor.study_valve_humidity_air_quality_index-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.study_valve_humidity_air_quality_index', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Humidity air quality index', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Humidity air quality index', + 'platform': 'duco', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'iaq_rh', + 'unique_id': 'aa:bb:cc:dd:ee:ff_62_iaq_rh', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor_entities_state[sensor.study_valve_humidity_air_quality_index-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Study valve Humidity air quality index', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.study_valve_humidity_air_quality_index', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '92', + }) +# --- diff --git a/tests/components/duco/test_sensor.py b/tests/components/duco/test_sensor.py index 6dae63a8935f..33cd8535acb2 100644 --- a/tests/components/duco/test_sensor.py +++ b/tests/components/duco/test_sensor.py @@ -30,8 +30,10 @@ async def init_integration( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_duco_client: AsyncMock, + mock_sensor_nodes: list[Node], ) -> MockConfigEntry: """Set up only the sensor platform for testing.""" + mock_duco_client.async_get_nodes.return_value = mock_sensor_nodes mock_config_entry.add_to_hass(hass) with patch("homeassistant.components.duco.PLATFORMS", [Platform.SENSOR]): await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -58,8 +60,12 @@ async def test_iaq_sensor_entities_disabled_by_default( """Test that IAQ sensor entities are disabled by default.""" for entity_id in ( "sensor.bathroom_rh_humidity_air_quality_index", + "sensor.bedroom_valve_humidity_air_quality_index", + "sensor.hall_valve_co2_air_quality_index", "sensor.kitchen_rh_humidity_air_quality_index", "sensor.office_co2_co2_air_quality_index", + "sensor.study_valve_co2_air_quality_index", + "sensor.study_valve_humidity_air_quality_index", ): entry = entity_registry.async_get(entity_id) assert entry is not None @@ -136,58 +142,52 @@ async def test_lan_info_duco_error_marks_unavailable( assert state.state == STATE_UNAVAILABLE +@pytest.mark.parametrize( + ("node_id", "expected_entity_id", "expected_state"), + [ + ( + 200, + "sensor.new_rh_sensor_humidity", + "55.0", + ), + ( + 201, + "sensor.new_valve_carbon_dioxide", + "575", + ), + ], +) @pytest.mark.usefixtures("init_integration") async def test_new_node_added_dynamically( hass: HomeAssistant, mock_duco_client: AsyncMock, - mock_nodes: list[Node], + mock_sensor_nodes: list[Node], + dynamic_sensor_nodes: dict[int, Node], freezer: FrozenDateTimeFactory, + node_id: int, + expected_entity_id: str, + expected_state: str, ) -> None: """Test a new node appearing in coordinator data creates entities automatically.""" - assert hass.states.get("sensor.new_rh_sensor_humidity") is None + assert hass.states.get(expected_entity_id) is None - new_node = Node( - node_id=200, - general=NodeGeneralInfo( - node_type="BSRH", - sub_type=0, - network_type="RF", - parent=1, - asso=1, - name="New RH sensor", - identify=0, - ), - ventilation=NodeVentilationInfo( - state="AUTO", - time_state_remain=0, - time_state_end=0, - mode="-", - flow_lvl_tgt=None, - ), - sensor=NodeSensorInfo( - co2=None, - iaq_co2=None, - rh=55.0, - iaq_rh=70, - temp=21.0, - ), - ) - mock_duco_client.async_get_nodes.return_value = [*mock_nodes, new_node] + new_node = dynamic_sensor_nodes[node_id] + mock_duco_client.async_get_nodes.return_value = [*mock_sensor_nodes, new_node] freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - state = hass.states.get("sensor.new_rh_sensor_humidity") + state = hass.states.get(expected_entity_id) assert state is not None - assert state.state == "55.0" + assert state.state == expected_state @pytest.mark.usefixtures("init_integration") async def test_deregistered_node_removes_device( hass: HomeAssistant, mock_duco_client: AsyncMock, - mock_nodes: list[Node], + mock_sensor_nodes: list[Node], mock_config_entry: MockConfigEntry, freezer: FrozenDateTimeFactory, ) -> None: @@ -202,7 +202,7 @@ async def test_deregistered_node_removes_device( # Simulate the firmware removing the deregistered node from the API response. mock_duco_client.async_get_nodes.return_value = [ - node for node in mock_nodes if node.node_id != 2 + node for node in mock_sensor_nodes if node.node_id != 2 ] freezer.tick(SCAN_INTERVAL) @@ -221,7 +221,7 @@ async def test_unknown_node_type_logs_warning_and_creates_no_entities( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_duco_client: AsyncMock, - mock_nodes: list[Node], + mock_sensor_nodes: list[Node], freezer: FrozenDateTimeFactory, caplog: pytest.LogCaptureFixture, device_registry: dr.DeviceRegistry, @@ -242,7 +242,7 @@ async def test_unknown_node_type_logs_warning_and_creates_no_entities( sensor=None, ) - mock_duco_client.async_get_nodes.return_value = [*mock_nodes, unknown_node] + mock_duco_client.async_get_nodes.return_value = [*mock_sensor_nodes, unknown_node] freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -262,7 +262,7 @@ async def test_previously_unknown_node_gets_entities_after_type_becomes_known( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_duco_client: AsyncMock, - mock_nodes: list[Node], + mock_sensor_nodes: list[Node], freezer: FrozenDateTimeFactory, ) -> None: """Test UNKNOWN type node is retried and gets entities once the type resolves.""" @@ -291,7 +291,7 @@ async def test_previously_unknown_node_gets_entities_after_type_becomes_known( # First poll: UNKNOWN type — no entities created. mock_duco_client.async_get_nodes.return_value = [ - *mock_nodes, + *mock_sensor_nodes, _make_node(NodeType.UNKNOWN), ] freezer.tick(SCAN_INTERVAL) @@ -301,7 +301,10 @@ async def test_previously_unknown_node_gets_entities_after_type_becomes_known( assert hass.states.get("sensor.future_sensor_humidity") is None # Second poll: type now resolved — entities must be created. - mock_duco_client.async_get_nodes.return_value = [*mock_nodes, _make_node("BSRH")] + mock_duco_client.async_get_nodes.return_value = [ + *mock_sensor_nodes, + _make_node("BSRH"), + ] freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -316,7 +319,7 @@ async def test_unknown_node_logged_at_debug( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_duco_client: AsyncMock, - mock_nodes: list[Node], + mock_sensor_nodes: list[Node], freezer: FrozenDateTimeFactory, caplog: pytest.LogCaptureFixture, ) -> None: @@ -341,7 +344,7 @@ async def test_unknown_node_logged_at_debug( ), sensor=NodeSensorInfo(co2=None, iaq_co2=None, rh=None, iaq_rh=None, temp=None), ) - mock_duco_client.async_get_nodes.return_value = [*mock_nodes, unknown_node] + mock_duco_client.async_get_nodes.return_value = [*mock_sensor_nodes, unknown_node] with caplog.at_level(logging.WARNING, logger="homeassistant.components.duco"): freezer.tick(SCAN_INTERVAL) @@ -362,11 +365,11 @@ async def test_unknown_node_logged_at_debug( async def test_ventilation_state_unknown_returns_state_unknown( hass: HomeAssistant, mock_duco_client: AsyncMock, - mock_nodes: list[Node], + mock_sensor_nodes: list[Node], freezer: FrozenDateTimeFactory, ) -> None: """Test that VentilationState.UNKNOWN makes the sensor report unknown.""" - box_node = next(n for n in mock_nodes if n.general.node_type == NodeType.BOX) + box_node = next(n for n in mock_sensor_nodes if n.general.node_type == NodeType.BOX) updated_nodes = [ Node( node_id=box_node.node_id, @@ -382,7 +385,7 @@ async def test_ventilation_state_unknown_returns_state_unknown( ) if n is box_node else n - for n in mock_nodes + for n in mock_sensor_nodes ] mock_duco_client.async_get_nodes.return_value = updated_nodes