Fix Duco box device removal on partial node refreshes (#173186)

This commit is contained in:
Ronald van der Meer
2026-06-08 09:48:27 +02:00
committed by GitHub
parent c3d6ad029f
commit f58e0e5234
3 changed files with 60 additions and 3 deletions
+1
View File
@@ -7,3 +7,4 @@ from homeassistant.const import Platform
DOMAIN = "duco"
PLATFORMS = [Platform.FAN, Platform.SENSOR]
SCAN_INTERVAL = timedelta(seconds=10)
BOX_NODE_ID = 1
+8 -2
View File
@@ -24,7 +24,7 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from .const import DOMAIN
from .const import BOX_NODE_ID, DOMAIN
from .coordinator import DucoConfigEntry, DucoCoordinator
from .entity import DucoEntity
@@ -158,7 +158,13 @@ async def async_setup_entry(
# The firmware removes deregistered RF/wired nodes automatically.
# BSRH box sensors that are physically unplugged from the PCB are
# not deregistered by the firmware and will never appear here as stale.
stale_node_ids = known_nodes - coordinator.data.nodes.keys()
# The BOX node can transiently disappear from the API response, so keep
# node 1 to avoid removing the main controller device.
stale_node_ids = {
node_id
for node_id in known_nodes - coordinator.data.nodes.keys()
if node_id != BOX_NODE_ID
}
if stale_node_ids:
device_reg = dr.async_get(hass)
mac = entry.unique_id
+51 -1
View File
@@ -17,7 +17,7 @@ from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.duco.const import DOMAIN, SCAN_INTERVAL
from homeassistant.components.duco.const import BOX_NODE_ID, DOMAIN, SCAN_INTERVAL
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
@@ -212,6 +212,56 @@ async def test_deregistered_node_removes_device(
assert device is None
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_box_node_not_removed_on_transient_incomplete_node_list(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_duco_client: AsyncMock,
mock_sensor_nodes: list[Node],
freezer: FrozenDateTimeFactory,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test BOX-linked entities survive a transient node list without node 1."""
await setup_platform_integration(
hass, mock_config_entry, [Platform.FAN, Platform.SENSOR]
)
box_device = device_registry.async_get_device(
identifiers={(DOMAIN, f"{mock_config_entry.unique_id}_{BOX_NODE_ID}")}
)
assert box_device is not None
assert hass.states.get("fan.living") is not None
mock_duco_client.async_get_nodes.return_value = [
node for node in mock_sensor_nodes if node.node_id != BOX_NODE_ID
]
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert (
device_registry.async_get_device(
identifiers={(DOMAIN, f"{mock_config_entry.unique_id}_{BOX_NODE_ID}")}
)
is not None
)
state = hass.states.get("fan.living")
assert state is not None
assert state.state == STATE_UNAVAILABLE
mock_duco_client.async_get_nodes.return_value = mock_sensor_nodes
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
state = hass.states.get("fan.living")
assert state is not None
assert state.state != STATE_UNAVAILABLE
@pytest.mark.usefixtures("init_integration")
async def test_unknown_node_type_logs_warning_and_creates_no_entities(
hass: HomeAssistant,