Fix Duco unknown node type not re-evaluated after becoming known (#169579)

This commit is contained in:
Ronald van der Meer
2026-05-01 11:23:50 +02:00
committed by GitHub
parent 3f22b65e94
commit 294ee9f0cd
2 changed files with 111 additions and 3 deletions
+9 -3
View File
@@ -167,6 +167,7 @@ async def async_setup_entry(
@callback
def _async_add_new_entities() -> None:
"""Add new sensor entities and remove stale ones on coordinator updates."""
# Remove devices whose nodes have disappeared from the API.
# The firmware removes deregistered RF/wired nodes automatically.
# BSRH box sensors that are physically unplugged from the PCB are
@@ -190,14 +191,19 @@ async def async_setup_entry(
for node in coordinator.data.nodes.values():
if node.node_id in known_nodes:
continue
known_nodes.add(node.node_id)
if node.general.node_type == NodeType.UNKNOWN:
_LOGGER.warning(
"Duco node %s (%s) has an unsupported device type and will be ignored",
# Do not add the node to known_nodes so that it is re-evaluated
# on every coordinator update. This allows entities to be
# created automatically once a firmware update or library
# update adds support for the device type.
_LOGGER.debug(
"Duco node %s (%s) has an unsupported device type and will be "
"retried on subsequent coordinator updates",
node.node_id,
node.general.name,
)
continue
known_nodes.add(node.node_id)
new_entities.extend(
DucoSensorEntity(coordinator, node, description)
for description in SENSOR_DESCRIPTIONS
+102
View File
@@ -1,5 +1,6 @@
"""Tests for the Duco sensor platform."""
import logging
from unittest.mock import AsyncMock, patch
from duco.exceptions import DucoConnectionError, DucoError
@@ -255,3 +256,104 @@ async def test_unknown_node_type_logs_warning_and_creates_no_entities(
identifiers={(DOMAIN, f"{mock_config_entry.unique_id}_99")}
)
assert device is None
@pytest.mark.usefixtures("init_integration")
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],
freezer: FrozenDateTimeFactory,
) -> None:
"""Test that a node with UNKNOWN type is retried and gets entities once the type resolves."""
node_id = 99
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=62.0, iaq_rh=70, temp=21.0)
def _make_node(node_type: NodeType | str) -> Node:
"""Create a Node with the given node type for use in tests."""
return Node(
node_id=node_id,
general=NodeGeneralInfo(
node_type=node_type,
sub_type=0,
network_type="RF",
parent=1,
asso=1,
name="Future sensor",
identify=0,
),
ventilation=ventilation,
sensor=sensor,
)
# First poll: UNKNOWN type — no entities created.
mock_duco_client.async_get_nodes.return_value = [
*mock_nodes,
_make_node(NodeType.UNKNOWN),
]
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
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")]
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
state = hass.states.get("sensor.future_sensor_humidity")
assert state is not None
assert state.state == "62.0"
@pytest.mark.usefixtures("init_integration")
async def test_unknown_node_logged_at_debug(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_duco_client: AsyncMock,
mock_nodes: list[Node],
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that UNKNOWN nodes are logged at DEBUG level on every coordinator update."""
unknown_node = Node(
node_id=99,
general=NodeGeneralInfo(
node_type=NodeType.UNKNOWN,
sub_type=0,
network_type="RF",
parent=1,
asso=1,
name="Future 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=None, iaq_rh=None, temp=None),
)
mock_duco_client.async_get_nodes.return_value = [*mock_nodes, unknown_node]
with caplog.at_level(logging.WARNING, logger="homeassistant.components.duco"):
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert "has an unsupported device type" not in caplog.text
with caplog.at_level(logging.DEBUG, logger="homeassistant.components.duco"):
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert "has an unsupported device type" in caplog.text