Fix Duco VLV nodes not creating CO2 and humidity sensors (#171182)

This commit is contained in:
Ronald van der Meer
2026-05-20 16:21:32 +02:00
committed by GitHub
parent 4c31a1737d
commit 7f2f268fca
6 changed files with 661 additions and 48 deletions
+4 -4
View File
@@ -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),
),
)
+41 -1
View File
@@ -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,
@@ -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
}
}
]
@@ -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
}
}
]
@@ -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': <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.bedroom_valve_humidity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Humidity',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
'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': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.bedroom_valve_humidity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <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.bedroom_valve_humidity_air_quality_index',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.bedroom_valve_humidity_air_quality_index',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '88',
})
# ---
# name: test_sensor_entities_state[sensor.hall_valve_carbon_dioxide-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'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.hall_valve_carbon_dioxide',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Carbon dioxide',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.CO2: 'carbon_dioxide'>,
'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': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'ppm',
}),
'context': <ANY>,
'entity_id': 'sensor.hall_valve_carbon_dioxide',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <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.hall_valve_co2_air_quality_index',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.hall_valve_co2_air_quality_index',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <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.study_valve_carbon_dioxide',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Carbon dioxide',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.CO2: 'carbon_dioxide'>,
'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': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'ppm',
}),
'context': <ANY>,
'entity_id': 'sensor.study_valve_carbon_dioxide',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <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.study_valve_co2_air_quality_index',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.study_valve_co2_air_quality_index',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '95',
})
# ---
# name: test_sensor_entities_state[sensor.study_valve_humidity-entry]
EntityRegistryEntrySnapshot({
'aliases': list([
None,
]),
'area_id': None,
'capabilities': dict({
'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.study_valve_humidity',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Humidity',
'options': dict({
}),
'original_device_class': <SensorDeviceClass.HUMIDITY: 'humidity'>,
'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': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.study_valve_humidity',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <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.study_valve_humidity_air_quality_index',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.study_valve_humidity_air_quality_index',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '92',
})
# ---
+46 -43
View File
@@ -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