diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index a4eb466fe87..82e79b83659 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -102,6 +102,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # update entity availability async_dispatcher_send(hass, f"{DOMAIN}_{entry.entry_id}_connection_state") + # Check for nodes that no longer exist and remove them + stored_devices = device_registry.async_entries_for_config_entry( + dev_reg, entry.entry_id + ) + known_devices = [ + dev_reg.async_get_device({get_device_id(client, node)}) + for node in client.driver.controller.nodes.values() + ] + + # Devices that are in the device registry that are not known by the controller can be removed + for device in stored_devices: + if device not in known_devices: + dev_reg.async_remove_device(device.id) + @callback def async_on_node_ready(node: ZwaveNode) -> None: """Handle node ready event.""" diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index b6cbc911b6a..0e0ebdee3c6 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -308,3 +308,15 @@ def in_wall_smart_fan_control_fixture(client, in_wall_smart_fan_control_state): node = Node(client, in_wall_smart_fan_control_state) client.driver.controller.nodes[node.node_id] = node return node + + +@pytest.fixture(name="multiple_devices") +def multiple_devices_fixture( + client, climate_radio_thermostat_ct100_plus_state, lock_schlage_be469_state +): + """Mock a client with multiple devices.""" + node = Node(client, climate_radio_thermostat_ct100_plus_state) + client.driver.controller.nodes[node.node_id] = node + node = Node(client, lock_schlage_be469_state) + client.driver.controller.nodes[node.node_id] = node + return client.driver.controller.nodes diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index b17945d05c6..86fbf27ab4f 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -7,6 +7,7 @@ from zwave_js_server.model.node import Node from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.zwave_js.const import DOMAIN +from homeassistant.components.zwave_js.entity import get_device_id from homeassistant.config_entries import ( CONN_CLASS_LOCAL_PUSH, ENTRY_STATE_LOADED, @@ -14,6 +15,7 @@ from homeassistant.config_entries import ( ENTRY_STATE_SETUP_RETRY, ) from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.helpers import device_registry, entity_registry from .common import AIR_TEMPERATURE_SENSOR @@ -290,3 +292,42 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): assert entry.state == ENTRY_STATE_NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to uninstall the Z-Wave JS add-on" in caplog.text + + +async def test_removed_device(hass, client, multiple_devices, integration): + """Test that the device registry gets updated when a device gets removed.""" + nodes = multiple_devices + + # Verify how many nodes are available + assert len(client.driver.controller.nodes) == 2 + + # Make sure there are the same number of devices + dev_reg = await device_registry.async_get_registry(hass) + device_entries = device_registry.async_entries_for_config_entry( + dev_reg, integration.entry_id + ) + assert len(device_entries) == 2 + + # Check how many entities there are + ent_reg = await entity_registry.async_get_registry(hass) + entity_entries = entity_registry.async_entries_for_config_entry( + ent_reg, integration.entry_id + ) + assert len(entity_entries) == 18 + + # Remove a node and reload the entry + old_node = nodes.pop(13) + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + # Assert that the node and all of it's entities were removed from the device and + # entity registry + device_entries = device_registry.async_entries_for_config_entry( + dev_reg, integration.entry_id + ) + assert len(device_entries) == 1 + entity_entries = entity_registry.async_entries_for_config_entry( + ent_reg, integration.entry_id + ) + assert len(entity_entries) == 9 + assert dev_reg.async_get_device({get_device_id(client, old_node)}) is None