From 4c5cf028d7861e7a9082ffcc6f67b979a4f0cbba Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 5 Aug 2025 08:50:42 +0200 Subject: [PATCH] Fix Z-Wave duplicate provisioned device (#150008) --- homeassistant/components/zwave_js/__init__.py | 56 +++++++++------- tests/components/zwave_js/test_init.py | 64 ++++++++++++++++--- 2 files changed, 87 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 923cd776f92..af42f024e6a 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -509,7 +509,7 @@ class ControllerEvents: ) ) - await self.async_check_preprovisioned_device(node) + await self.async_check_pre_provisioned_device(node) if node.is_controller_node: # Create a controller status sensor for each device @@ -637,8 +637,8 @@ class ControllerEvents: f"{DOMAIN}.identify_controller.{dev_id[1]}", ) - async def async_check_preprovisioned_device(self, node: ZwaveNode) -> None: - """Check if the node was preprovisioned and update the device registry.""" + async def async_check_pre_provisioned_device(self, node: ZwaveNode) -> None: + """Check if the node was pre-provisioned and update the device registry.""" provisioning_entry = ( await self.driver_events.driver.controller.async_get_provisioning_entry( node.node_id @@ -648,29 +648,37 @@ class ControllerEvents: provisioning_entry and provisioning_entry.additional_properties and "device_id" in provisioning_entry.additional_properties - ): - preprovisioned_device = self.dev_reg.async_get( - provisioning_entry.additional_properties["device_id"] + and ( + pre_provisioned_device := self.dev_reg.async_get( + provisioning_entry.additional_properties["device_id"] + ) ) + and (dsk_identifier := (DOMAIN, f"provision_{provisioning_entry.dsk}")) + in pre_provisioned_device.identifiers + ): + driver = self.driver_events.driver + device_id = get_device_id(driver, node) + device_id_ext = get_device_id_ext(driver, node) + new_identifiers = pre_provisioned_device.identifiers.copy() + new_identifiers.remove(dsk_identifier) + new_identifiers.add(device_id) + if device_id_ext: + new_identifiers.add(device_id_ext) - if preprovisioned_device: - dsk = provisioning_entry.dsk - dsk_identifier = (DOMAIN, f"provision_{dsk}") - - # If the pre-provisioned device has the DSK identifier, remove it - if dsk_identifier in preprovisioned_device.identifiers: - driver = self.driver_events.driver - device_id = get_device_id(driver, node) - device_id_ext = get_device_id_ext(driver, node) - new_identifiers = preprovisioned_device.identifiers.copy() - new_identifiers.remove(dsk_identifier) - new_identifiers.add(device_id) - if device_id_ext: - new_identifiers.add(device_id_ext) - self.dev_reg.async_update_device( - preprovisioned_device.id, - new_identifiers=new_identifiers, - ) + if self.dev_reg.async_get_device(identifiers=new_identifiers): + # If a device entry is registered with the node ID based identifiers, + # just remove the device entry with the DSK identifier. + self.dev_reg.async_update_device( + pre_provisioned_device.id, + remove_config_entry_id=self.config_entry.entry_id, + ) + else: + # Add the node ID based identifiers to the device entry + # with the DSK identifier and remove the DSK identifier. + self.dev_reg.async_update_device( + pre_provisioned_device.id, + new_identifiers=new_identifiers, + ) async def async_register_node_in_dev_reg(self, node: ZwaveNode) -> dr.DeviceEntry: """Register node in dev reg.""" diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 3c39868ff93..1aaa9013d87 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -497,17 +497,17 @@ async def test_on_node_added_ready( ) -async def test_on_node_added_preprovisioned( +async def test_check_pre_provisioned_device_update_device( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - multisensor_6_state, - client, - integration, + multisensor_6_state: NodeDataType, + client: MagicMock, + integration: MockConfigEntry, ) -> None: - """Test node added event with a preprovisioned device.""" + """Test check pre-provisioned device that should update the device.""" dsk = "test" node = Node(client, deepcopy(multisensor_6_state)) - device = device_registry.async_get_or_create( + pre_provisioned_device = device_registry.async_get_or_create( config_entry_id=integration.entry_id, identifiers={(DOMAIN, f"provision_{dsk}")}, ) @@ -515,7 +515,7 @@ async def test_on_node_added_preprovisioned( { "dsk": dsk, "securityClasses": [SecurityClass.S2_UNAUTHENTICATED], - "device_id": device.id, + "device_id": pre_provisioned_device.id, } ) with patch( @@ -526,14 +526,60 @@ async def test_on_node_added_preprovisioned( client.driver.controller.emit("node added", event) await hass.async_block_till_done() - device = device_registry.async_get(device.id) + device = device_registry.async_get(pre_provisioned_device.id) assert device assert device.identifiers == { get_device_id(client.driver, node), get_device_id_ext(client.driver, node), } assert device.sw_version == node.firmware_version - # There should only be the controller and the preprovisioned device + # There should only be the controller and the pre-provisioned device + assert len(device_registry.devices) == 2 + + +async def test_check_pre_provisioned_device_remove_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + multisensor_6_state: NodeDataType, + client: MagicMock, + integration: MockConfigEntry, +) -> None: + """Test check pre-provisioned device that should remove the device.""" + dsk = "test" + driver = client.driver + node = Node(client, deepcopy(multisensor_6_state)) + pre_provisioned_device = device_registry.async_get_or_create( + config_entry_id=integration.entry_id, + identifiers={(DOMAIN, f"provision_{dsk}")}, + ) + extended_identifier = get_device_id_ext(driver, node) + assert extended_identifier + existing_device = device_registry.async_get_or_create( + config_entry_id=integration.entry_id, + identifiers={ + get_device_id(driver, node), + extended_identifier, + }, + ) + provisioning_entry = ProvisioningEntry.from_dict( + { + "dsk": dsk, + "securityClasses": [SecurityClass.S2_UNAUTHENTICATED], + "device_id": pre_provisioned_device.id, + } + ) + with patch( + f"{CONTROLLER_PATCH_PREFIX}.async_get_provisioning_entry", + side_effect=lambda id: provisioning_entry if id == node.node_id else None, + ): + event = {"node": node} + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + assert not device_registry.async_get(pre_provisioned_device.id) + assert device_registry.async_get(existing_device.id) + + # There should only be the controller and the existing device assert len(device_registry.devices) == 2