Fix Z-Wave duplicate provisioned device (#150008)

This commit is contained in:
Martin Hjelmare
2025-08-05 08:50:42 +02:00
committed by GitHub
parent 68faa897ad
commit 4c5cf028d7
2 changed files with 87 additions and 33 deletions

View File

@@ -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: if node.is_controller_node:
# Create a controller status sensor for each device # Create a controller status sensor for each device
@@ -637,8 +637,8 @@ class ControllerEvents:
f"{DOMAIN}.identify_controller.{dev_id[1]}", f"{DOMAIN}.identify_controller.{dev_id[1]}",
) )
async def async_check_preprovisioned_device(self, node: ZwaveNode) -> None: async def async_check_pre_provisioned_device(self, node: ZwaveNode) -> None:
"""Check if the node was preprovisioned and update the device registry.""" """Check if the node was pre-provisioned and update the device registry."""
provisioning_entry = ( provisioning_entry = (
await self.driver_events.driver.controller.async_get_provisioning_entry( await self.driver_events.driver.controller.async_get_provisioning_entry(
node.node_id node.node_id
@@ -648,29 +648,37 @@ class ControllerEvents:
provisioning_entry provisioning_entry
and provisioning_entry.additional_properties and provisioning_entry.additional_properties
and "device_id" in provisioning_entry.additional_properties and "device_id" in provisioning_entry.additional_properties
): and (
preprovisioned_device = self.dev_reg.async_get( pre_provisioned_device := self.dev_reg.async_get(
provisioning_entry.additional_properties["device_id"] 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: if self.dev_reg.async_get_device(identifiers=new_identifiers):
dsk = provisioning_entry.dsk # If a device entry is registered with the node ID based identifiers,
dsk_identifier = (DOMAIN, f"provision_{dsk}") # just remove the device entry with the DSK identifier.
self.dev_reg.async_update_device(
# If the pre-provisioned device has the DSK identifier, remove it pre_provisioned_device.id,
if dsk_identifier in preprovisioned_device.identifiers: remove_config_entry_id=self.config_entry.entry_id,
driver = self.driver_events.driver )
device_id = get_device_id(driver, node) else:
device_id_ext = get_device_id_ext(driver, node) # Add the node ID based identifiers to the device entry
new_identifiers = preprovisioned_device.identifiers.copy() # with the DSK identifier and remove the DSK identifier.
new_identifiers.remove(dsk_identifier) self.dev_reg.async_update_device(
new_identifiers.add(device_id) pre_provisioned_device.id,
if device_id_ext: new_identifiers=new_identifiers,
new_identifiers.add(device_id_ext) )
self.dev_reg.async_update_device(
preprovisioned_device.id,
new_identifiers=new_identifiers,
)
async def async_register_node_in_dev_reg(self, node: ZwaveNode) -> dr.DeviceEntry: async def async_register_node_in_dev_reg(self, node: ZwaveNode) -> dr.DeviceEntry:
"""Register node in dev reg.""" """Register node in dev reg."""

View File

@@ -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, hass: HomeAssistant,
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,
multisensor_6_state, multisensor_6_state: NodeDataType,
client, client: MagicMock,
integration, integration: MockConfigEntry,
) -> None: ) -> None:
"""Test node added event with a preprovisioned device.""" """Test check pre-provisioned device that should update the device."""
dsk = "test" dsk = "test"
node = Node(client, deepcopy(multisensor_6_state)) 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, config_entry_id=integration.entry_id,
identifiers={(DOMAIN, f"provision_{dsk}")}, identifiers={(DOMAIN, f"provision_{dsk}")},
) )
@@ -515,7 +515,7 @@ async def test_on_node_added_preprovisioned(
{ {
"dsk": dsk, "dsk": dsk,
"securityClasses": [SecurityClass.S2_UNAUTHENTICATED], "securityClasses": [SecurityClass.S2_UNAUTHENTICATED],
"device_id": device.id, "device_id": pre_provisioned_device.id,
} }
) )
with patch( with patch(
@@ -526,14 +526,60 @@ async def test_on_node_added_preprovisioned(
client.driver.controller.emit("node added", event) client.driver.controller.emit("node added", event)
await hass.async_block_till_done() 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
assert device.identifiers == { assert device.identifiers == {
get_device_id(client.driver, node), get_device_id(client.driver, node),
get_device_id_ext(client.driver, node), get_device_id_ext(client.driver, node),
} }
assert device.sw_version == node.firmware_version 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 assert len(device_registry.devices) == 2