From ca912906f5bbf7705bcea7f1fd31f60c9f7e9240 Mon Sep 17 00:00:00 2001 From: Markus Adrario Date: Wed, 15 Oct 2025 19:36:27 +0200 Subject: [PATCH] Automatically removing stale devices in Homee (#152680) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: AbĂ­lio Costa --- homeassistant/components/homee/__init__.py | 35 ++++++++++++ .../components/homee/quality_scale.yaml | 2 +- tests/components/homee/test_binary_sensor.py | 2 +- tests/components/homee/test_init.py | 57 +++++++++++++++++++ 4 files changed, 94 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homee/__init__.py b/homeassistant/components/homee/__init__.py index d748d1dd809..6e90ecd97d8 100644 --- a/homeassistant/components/homee/__init__.py +++ b/homeassistant/components/homee/__init__.py @@ -3,6 +3,7 @@ import logging from pyHomee import Homee, HomeeAuthFailedException, HomeeConnectionFailedException +from pyHomee.model import HomeeNode from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform @@ -88,6 +89,40 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeeConfigEntry) -> boo sw_version=homee.settings.version, ) + # Remove devices that are no longer present in homee. + devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id) + for device in devices: + # Check if the device is still present in homee + device_identifiers = {identifier[1] for identifier in device.identifiers} + # homee itself uses just the uid, nodes use uid-nodeid + is_homee_hub = homee.settings.uid in device_identifiers + is_node_present = any( + f"{homee.settings.uid}-{node.id}" in device_identifiers + for node in homee.nodes + ) + if not is_node_present and not is_homee_hub: + _LOGGER.info("Removing device %s", device.name) + device_registry.async_update_device( + device_id=device.id, + remove_config_entry_id=entry.entry_id, + ) + + # Remove device at runtime when node is removed in homee + async def _remove_node_callback(node: HomeeNode, add: bool) -> None: + """Call when a node is removed.""" + if not add: + device = device_registry.async_get_device( + identifiers={(DOMAIN, f"{entry.runtime_data.settings.uid}-{node.id}")} + ) + if device: + _LOGGER.info("Removing device %s", device.name) + device_registry.async_update_device( + device_id=device.id, + remove_config_entry_id=entry.entry_id, + ) + + homee.add_nodes_listener(_remove_node_callback) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/homee/quality_scale.yaml b/homeassistant/components/homee/quality_scale.yaml index f27876b1725..bf86abb7938 100644 --- a/homeassistant/components/homee/quality_scale.yaml +++ b/homeassistant/components/homee/quality_scale.yaml @@ -63,7 +63,7 @@ rules: icon-translations: done reconfiguration-flow: done repair-issues: todo - stale-devices: todo + stale-devices: done # Platinum async-dependency: todo diff --git a/tests/components/homee/test_binary_sensor.py b/tests/components/homee/test_binary_sensor.py index ef3cf8ecee3..9cfca384676 100644 --- a/tests/components/homee/test_binary_sensor.py +++ b/tests/components/homee/test_binary_sensor.py @@ -46,7 +46,7 @@ async def test_add_device( added_node = build_mock_node("add_device.json") mock_homee.nodes.append(added_node) mock_homee.get_node_by_id.return_value = mock_homee.nodes[1] - await mock_homee.add_nodes_listener.call_args_list[0][0][0](added_node, True) + await mock_homee.add_nodes_listener.call_args_list[1][0][0](added_node, True) await hass.async_block_till_done() await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/homee/test_init.py b/tests/components/homee/test_init.py index c24cb39295d..8dde59e967f 100644 --- a/tests/components/homee/test_init.py +++ b/tests/components/homee/test_init.py @@ -143,3 +143,60 @@ async def test_unload_entry( await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_remove_stale_device_on_startup( + hass: HomeAssistant, + mock_homee: MagicMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test removal of stale device on startup.""" + mock_homee.nodes = [ + build_mock_node("homee.json"), + build_mock_node("light_single.json"), # id 2 + build_mock_node("add_device.json"), # id 3 + ] + mock_homee.get_node_by_id = lambda node_id: mock_homee.nodes[node_id - 1] + await setup_integration(hass, mock_config_entry) + + device = device_registry.async_get_device(identifiers={(DOMAIN, f"{HOMEE_ID}-3")}) + assert device is not None + + mock_homee.nodes.pop() # Remove node with id 3 + # Reload integration + await hass.config_entries.async_reload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # Stale device should be removed + device = device_registry.async_get_device(identifiers={(DOMAIN, f"{HOMEE_ID}-3")}) + assert device is None + + +async def test_remove_node_callback( + hass: HomeAssistant, + mock_homee: MagicMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test removal of device when node is removed in homee.""" + mock_homee.nodes = [ + build_mock_node("homee.json"), + build_mock_node("light_single.json"), # id 2 + build_mock_node("add_device.json"), # id 3 + ] + mock_homee.get_node_by_id = lambda node_id: mock_homee.nodes[node_id - 1] + await setup_integration(hass, mock_config_entry) + + device = device_registry.async_get_device(identifiers={(DOMAIN, f"{HOMEE_ID}-3")}) + assert device is not None + + # Simulate removal of node with id 3 in homee + await mock_homee.add_nodes_listener.call_args_list[0][0][0]( + mock_homee.nodes[2], add=False + ) + await hass.async_block_till_done() + + # Device should be removed + device = device_registry.async_get_device(identifiers={(DOMAIN, f"{HOMEE_ID}-3")}) + assert device is None