diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 45aef87bf80..b6f781d4a34 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections import defaultdict from typing import Callable from async_timeout import timeout @@ -87,7 +88,7 @@ def register_node_in_dev_reg( dev_reg: device_registry.DeviceRegistry, client: ZwaveClient, node: ZwaveNode, -) -> None: +) -> device_registry.DeviceEntry: """Register node in dev reg.""" params = { "config_entry_id": entry.entry_id, @@ -103,6 +104,10 @@ def register_node_in_dev_reg( async_dispatcher_send(hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device) + # We can assert here because we will always get a device + assert device + return device + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Z-Wave JS from a config entry.""" @@ -120,6 +125,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry_hass_data[DATA_UNSUBSCRIBE] = unsubscribe_callbacks entry_hass_data[DATA_PLATFORM_SETUP] = {} + registered_unique_ids: dict[str, dict[str, set[str]]] = defaultdict(dict) + async def async_on_node_ready(node: ZwaveNode) -> None: """Handle node ready event.""" LOGGER.debug("Processing node %s", node) @@ -127,26 +134,37 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: platform_setup_tasks = entry_hass_data[DATA_PLATFORM_SETUP] # register (or update) node in device registry - register_node_in_dev_reg(hass, entry, dev_reg, client, node) + device = register_node_in_dev_reg(hass, entry, dev_reg, client, node) + # We only want to create the defaultdict once, even on reinterviews + if device.id not in registered_unique_ids: + registered_unique_ids[device.id] = defaultdict(set) # run discovery on all node values and create/update entities for disc_info in async_discover_values(node): + platform = disc_info.platform + # This migration logic was added in 2021.3 to handle a breaking change to # the value_id format. Some time in the future, this call (as well as the # helper functions) can be removed. - async_migrate_discovered_value(ent_reg, client, disc_info) - if disc_info.platform not in platform_setup_tasks: - platform_setup_tasks[disc_info.platform] = hass.async_create_task( - hass.config_entries.async_forward_entry_setup( - entry, disc_info.platform - ) + async_migrate_discovered_value( + hass, + ent_reg, + registered_unique_ids[device.id][platform], + device, + client, + disc_info, + ) + + if platform not in platform_setup_tasks: + platform_setup_tasks[platform] = hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) ) - await platform_setup_tasks[disc_info.platform] + await platform_setup_tasks[platform] LOGGER.debug("Discovered entity: %s", disc_info) async_dispatcher_send( - hass, f"{DOMAIN}_{entry.entry_id}_add_{disc_info.platform}", disc_info + hass, f"{DOMAIN}_{entry.entry_id}_add_{platform}", disc_info ) # add listener for stateless node value notification events @@ -189,6 +207,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device = dev_reg.async_get_device({dev_id}) # note: removal of entity registry entry is handled by core dev_reg.async_remove_device(device.id) # type: ignore + registered_unique_ids.pop(device.id, None) # type: ignore @callback def async_on_value_notification(notification: ValueNotification) -> None: diff --git a/homeassistant/components/zwave_js/migrate.py b/homeassistant/components/zwave_js/migrate.py index 997d34c8445..ea4b978cab5 100644 --- a/homeassistant/components/zwave_js/migrate.py +++ b/homeassistant/components/zwave_js/migrate.py @@ -1,13 +1,20 @@ """Functions used to migrate unique IDs for Z-Wave JS entities.""" from __future__ import annotations +from dataclasses import dataclass import logging from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.value import Value as ZwaveValue -from homeassistant.core import callback -from homeassistant.helpers.entity_registry import EntityRegistry +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.entity_registry import ( + EntityRegistry, + RegistryEntry, + async_entries_for_device, +) from .const import DOMAIN from .discovery import ZwaveDiscoveryInfo @@ -16,8 +23,88 @@ from .helpers import get_unique_id _LOGGER = logging.getLogger(__name__) +@dataclass +class ValueID: + """Class to represent a Value ID.""" + + command_class: str + endpoint: str + property_: str + property_key: str | None = None + + @staticmethod + def from_unique_id(unique_id: str) -> ValueID: + """ + Get a ValueID from a unique ID. + + This also works for Notification CC Binary Sensors which have their own unique ID + format. + """ + return ValueID.from_string_id(unique_id.split(".")[1]) + + @staticmethod + def from_string_id(value_id_str: str) -> ValueID: + """Get a ValueID from a string representation of the value ID.""" + parts = value_id_str.split("-") + property_key = parts[4] if len(parts) > 4 else None + return ValueID(parts[1], parts[2], parts[3], property_key=property_key) + + def is_same_value_different_endpoints(self, other: ValueID) -> bool: + """Return whether two value IDs are the same excluding endpoint.""" + return ( + self.command_class == other.command_class + and self.property_ == other.property_ + and self.property_key == other.property_key + and self.endpoint != other.endpoint + ) + + @callback -def async_migrate_entity( +def async_migrate_old_entity( + hass: HomeAssistant, + ent_reg: EntityRegistry, + registered_unique_ids: set[str], + platform: str, + device: DeviceEntry, + unique_id: str, +) -> None: + """Migrate existing entity if current one can't be found and an old one exists.""" + # If we can find an existing entity with this unique ID, there's nothing to migrate + if ent_reg.async_get_entity_id(platform, DOMAIN, unique_id): + return + + value_id = ValueID.from_unique_id(unique_id) + + # Look for existing entities in the registry that could be the same value but on + # a different endpoint + existing_entity_entries: list[RegistryEntry] = [] + for entry in async_entries_for_device(ent_reg, device.id): + # If entity is not in the domain for this discovery info or entity has already + # been processed, skip it + if entry.domain != platform or entry.unique_id in registered_unique_ids: + continue + + old_ent_value_id = ValueID.from_unique_id(entry.unique_id) + + if value_id.is_same_value_different_endpoints(old_ent_value_id): + existing_entity_entries.append(entry) + # We can return early if we get more than one result + if len(existing_entity_entries) > 1: + return + + # If we couldn't find any results, return early + if not existing_entity_entries: + return + + entry = existing_entity_entries[0] + state = hass.states.get(entry.entity_id) + + if not state or state.state == STATE_UNAVAILABLE: + async_migrate_unique_id(ent_reg, platform, entry.unique_id, unique_id) + + +@callback +def async_migrate_unique_id( ent_reg: EntityRegistry, platform: str, old_unique_id: str, new_unique_id: str ) -> None: """Check if entity with old unique ID exists, and if so migrate it to new ID.""" @@ -29,10 +116,7 @@ def async_migrate_entity( new_unique_id, ) try: - ent_reg.async_update_entity( - entity_id, - new_unique_id=new_unique_id, - ) + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) except ValueError: _LOGGER.debug( ( @@ -46,43 +130,87 @@ def async_migrate_entity( @callback def async_migrate_discovered_value( - ent_reg: EntityRegistry, client: ZwaveClient, disc_info: ZwaveDiscoveryInfo + hass: HomeAssistant, + ent_reg: EntityRegistry, + registered_unique_ids: set[str], + device: DeviceEntry, + client: ZwaveClient, + disc_info: ZwaveDiscoveryInfo, ) -> None: """Migrate unique ID for entity/entities tied to discovered value.""" + new_unique_id = get_unique_id( client.driver.controller.home_id, disc_info.primary_value.value_id, ) + # On reinterviews, there is no point in going through this logic again for already + # discovered values + if new_unique_id in registered_unique_ids: + return + + # Migration logic was added in 2021.3 to handle a breaking change to the value_id + # format. Some time in the future, the logic to migrate unique IDs can be removed. + # 2021.2.*, 2021.3.0b0, and 2021.3.0 formats - for value_id in get_old_value_ids(disc_info.primary_value): - old_unique_id = get_unique_id( + old_unique_ids = [ + get_unique_id( client.driver.controller.home_id, value_id, ) - # Most entities have the same ID format, but notification binary sensors - # have a state key in their ID so we need to handle them differently - if ( - disc_info.platform == "binary_sensor" - and disc_info.platform_hint == "notification" - ): - for state_key in disc_info.primary_value.metadata.states: - # ignore idle key (0) - if state_key == "0": - continue + for value_id in get_old_value_ids(disc_info.primary_value) + ] - async_migrate_entity( + if ( + disc_info.platform == "binary_sensor" + and disc_info.platform_hint == "notification" + ): + for state_key in disc_info.primary_value.metadata.states: + # ignore idle key (0) + if state_key == "0": + continue + + new_bin_sensor_unique_id = f"{new_unique_id}.{state_key}" + + # On reinterviews, there is no point in going through this logic again + # for already discovered values + if new_bin_sensor_unique_id in registered_unique_ids: + continue + + # Unique ID migration + for old_unique_id in old_unique_ids: + async_migrate_unique_id( ent_reg, disc_info.platform, f"{old_unique_id}.{state_key}", - f"{new_unique_id}.{state_key}", + new_bin_sensor_unique_id, ) - # Once we've iterated through all state keys, we can move on to the - # next item - continue + # Migrate entities in case upstream changes cause endpoint change + async_migrate_old_entity( + hass, + ent_reg, + registered_unique_ids, + disc_info.platform, + device, + new_bin_sensor_unique_id, + ) + registered_unique_ids.add(new_bin_sensor_unique_id) - async_migrate_entity(ent_reg, disc_info.platform, old_unique_id, new_unique_id) + # Once we've iterated through all state keys, we are done + return + + # Unique ID migration + for old_unique_id in old_unique_ids: + async_migrate_unique_id( + ent_reg, disc_info.platform, old_unique_id, new_unique_id + ) + + # Migrate entities in case upstream changes cause endpoint change + async_migrate_old_entity( + hass, ent_reg, registered_unique_ids, disc_info.platform, device, new_unique_id + ) + registered_unique_ids.add(new_unique_id) @callback diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 3e7f79b9cec..32fcdbcc84a 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -162,19 +162,27 @@ async def test_unique_id_migration_dupes( entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR) new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature" assert entity_entry.unique_id == new_unique_id - - assert ent_reg.async_get(f"{AIR_TEMPERATURE_SENSOR}_1") is None + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_1) is None + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_2) is None -async def test_unique_id_migration_v1(hass, multisensor_6_state, client, integration): - """Test unique ID is migrated from old format to new (version 1).""" +@pytest.mark.parametrize( + "id", + [ + ("52.52-49-00-Air temperature-00"), + ("52.52-49-0-Air temperature-00-00"), + ("52-49-0-Air temperature-00-00"), + ], +) +async def test_unique_id_migration(hass, multisensor_6_state, client, integration, id): + """Test unique ID is migrated from old format to new.""" ent_reg = er.async_get(hass) # Migrate version 1 entity_name = AIR_TEMPERATURE_SENSOR.split(".")[1] # Create entity RegistryEntry using old unique ID format - old_unique_id = f"{client.driver.controller.home_id}.52.52-49-00-Air temperature-00" + old_unique_id = f"{client.driver.controller.home_id}.{id}" entity_entry = ent_reg.async_get_or_create( "sensor", DOMAIN, @@ -197,157 +205,28 @@ async def test_unique_id_migration_v1(hass, multisensor_6_state, client, integra entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR) new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature" assert entity_entry.unique_id == new_unique_id + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id) is None -async def test_unique_id_migration_v2(hass, multisensor_6_state, client, integration): - """Test unique ID is migrated from old format to new (version 2).""" - ent_reg = er.async_get(hass) - # Migrate version 2 - ILLUMINANCE_SENSOR = "sensor.multisensor_6_illuminance" - entity_name = ILLUMINANCE_SENSOR.split(".")[1] - - # Create entity RegistryEntry using old unique ID format - old_unique_id = f"{client.driver.controller.home_id}.52.52-49-0-Illuminance-00-00" - entity_entry = ent_reg.async_get_or_create( - "sensor", - DOMAIN, - old_unique_id, - suggested_object_id=entity_name, - config_entry=integration, - original_name=entity_name, - ) - assert entity_entry.entity_id == ILLUMINANCE_SENSOR - assert entity_entry.unique_id == old_unique_id - - # Add a ready node, unique ID should be migrated - node = Node(client, multisensor_6_state) - event = {"node": node} - - client.driver.controller.emit("node added", event) - await hass.async_block_till_done() - - # Check that new RegistryEntry is using new unique ID format - entity_entry = ent_reg.async_get(ILLUMINANCE_SENSOR) - new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Illuminance" - assert entity_entry.unique_id == new_unique_id - - -async def test_unique_id_migration_v3(hass, multisensor_6_state, client, integration): - """Test unique ID is migrated from old format to new (version 3).""" - ent_reg = er.async_get(hass) - # Migrate version 2 - ILLUMINANCE_SENSOR = "sensor.multisensor_6_illuminance" - entity_name = ILLUMINANCE_SENSOR.split(".")[1] - - # Create entity RegistryEntry using old unique ID format - old_unique_id = f"{client.driver.controller.home_id}.52-49-0-Illuminance-00-00" - entity_entry = ent_reg.async_get_or_create( - "sensor", - DOMAIN, - old_unique_id, - suggested_object_id=entity_name, - config_entry=integration, - original_name=entity_name, - ) - assert entity_entry.entity_id == ILLUMINANCE_SENSOR - assert entity_entry.unique_id == old_unique_id - - # Add a ready node, unique ID should be migrated - node = Node(client, multisensor_6_state) - event = {"node": node} - - client.driver.controller.emit("node added", event) - await hass.async_block_till_done() - - # Check that new RegistryEntry is using new unique ID format - entity_entry = ent_reg.async_get(ILLUMINANCE_SENSOR) - new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Illuminance" - assert entity_entry.unique_id == new_unique_id - - -async def test_unique_id_migration_property_key_v1( - hass, hank_binary_switch_state, client, integration +@pytest.mark.parametrize( + "id", + [ + ("32.32-50-00-value-W_Consumed"), + ("32.32-50-0-value-66049-W_Consumed"), + ("32-50-0-value-66049-W_Consumed"), + ], +) +async def test_unique_id_migration_property_key( + hass, hank_binary_switch_state, client, integration, id ): - """Test unique ID with property key is migrated from old format to new (version 1).""" + """Test unique ID with property key is migrated from old format to new.""" ent_reg = er.async_get(hass) SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" entity_name = SENSOR_NAME.split(".")[1] # Create entity RegistryEntry using old unique ID format - old_unique_id = f"{client.driver.controller.home_id}.32.32-50-00-value-W_Consumed" - entity_entry = ent_reg.async_get_or_create( - "sensor", - DOMAIN, - old_unique_id, - suggested_object_id=entity_name, - config_entry=integration, - original_name=entity_name, - ) - assert entity_entry.entity_id == SENSOR_NAME - assert entity_entry.unique_id == old_unique_id - - # Add a ready node, unique ID should be migrated - node = Node(client, hank_binary_switch_state) - event = {"node": node} - - client.driver.controller.emit("node added", event) - await hass.async_block_till_done() - - # Check that new RegistryEntry is using new unique ID format - entity_entry = ent_reg.async_get(SENSOR_NAME) - new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049" - assert entity_entry.unique_id == new_unique_id - - -async def test_unique_id_migration_property_key_v2( - hass, hank_binary_switch_state, client, integration -): - """Test unique ID with property key is migrated from old format to new (version 2).""" - ent_reg = er.async_get(hass) - - SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" - entity_name = SENSOR_NAME.split(".")[1] - - # Create entity RegistryEntry using old unique ID format - old_unique_id = ( - f"{client.driver.controller.home_id}.32.32-50-0-value-66049-W_Consumed" - ) - entity_entry = ent_reg.async_get_or_create( - "sensor", - DOMAIN, - old_unique_id, - suggested_object_id=entity_name, - config_entry=integration, - original_name=entity_name, - ) - assert entity_entry.entity_id == SENSOR_NAME - assert entity_entry.unique_id == old_unique_id - - # Add a ready node, unique ID should be migrated - node = Node(client, hank_binary_switch_state) - event = {"node": node} - - client.driver.controller.emit("node added", event) - await hass.async_block_till_done() - - # Check that new RegistryEntry is using new unique ID format - entity_entry = ent_reg.async_get(SENSOR_NAME) - new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049" - assert entity_entry.unique_id == new_unique_id - - -async def test_unique_id_migration_property_key_v3( - hass, hank_binary_switch_state, client, integration -): - """Test unique ID with property key is migrated from old format to new (version 3).""" - ent_reg = er.async_get(hass) - - SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" - entity_name = SENSOR_NAME.split(".")[1] - - # Create entity RegistryEntry using old unique ID format - old_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049-W_Consumed" + old_unique_id = f"{client.driver.controller.home_id}.{id}" entity_entry = ent_reg.async_get_or_create( "sensor", DOMAIN, @@ -370,6 +249,7 @@ async def test_unique_id_migration_property_key_v3( entity_entry = ent_reg.async_get(SENSOR_NAME) new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049" assert entity_entry.unique_id == new_unique_id + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id) is None async def test_unique_id_migration_notification_binary_sensor( @@ -404,6 +284,151 @@ async def test_unique_id_migration_notification_binary_sensor( entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_BINARY_SENSOR) new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status.8" assert entity_entry.unique_id == new_unique_id + assert ent_reg.async_get_entity_id("binary_sensor", DOMAIN, old_unique_id) is None + + +async def test_old_entity_migration( + hass, hank_binary_switch_state, client, integration +): + """Test old entity on a different endpoint is migrated to a new one.""" + node = Node(client, hank_binary_switch_state) + + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_or_create( + config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)} + ) + + SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" + entity_name = SENSOR_NAME.split(".")[1] + + # Create entity RegistryEntry using fake endpoint + old_unique_id = f"{client.driver.controller.home_id}.32-50-1-value-66049" + entity_entry = ent_reg.async_get_or_create( + "sensor", + DOMAIN, + old_unique_id, + suggested_object_id=entity_name, + config_entry=integration, + original_name=entity_name, + device_id=device.id, + ) + assert entity_entry.entity_id == SENSOR_NAME + assert entity_entry.unique_id == old_unique_id + + # Do this twice to make sure re-interview doesn't do anything weird + for i in range(0, 2): + # Add a ready node, unique ID should be migrated + event = {"node": node} + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + # Check that new RegistryEntry is using new unique ID format + entity_entry = ent_reg.async_get(SENSOR_NAME) + new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049" + assert entity_entry.unique_id == new_unique_id + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id) is None + + +async def test_skip_old_entity_migration_for_multiple( + hass, hank_binary_switch_state, client, integration +): + """Test that multiple entities of the same value but on a different endpoint get skipped.""" + node = Node(client, hank_binary_switch_state) + + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_or_create( + config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)} + ) + + SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" + entity_name = SENSOR_NAME.split(".")[1] + + # Create two entity entrrys using different endpoints + old_unique_id_1 = f"{client.driver.controller.home_id}.32-50-1-value-66049" + entity_entry = ent_reg.async_get_or_create( + "sensor", + DOMAIN, + old_unique_id_1, + suggested_object_id=f"{entity_name}_1", + config_entry=integration, + original_name=f"{entity_name}_1", + device_id=device.id, + ) + assert entity_entry.entity_id == f"{SENSOR_NAME}_1" + assert entity_entry.unique_id == old_unique_id_1 + + # Create two entity entrrys using different endpoints + old_unique_id_2 = f"{client.driver.controller.home_id}.32-50-2-value-66049" + entity_entry = ent_reg.async_get_or_create( + "sensor", + DOMAIN, + old_unique_id_2, + suggested_object_id=f"{entity_name}_2", + config_entry=integration, + original_name=f"{entity_name}_2", + device_id=device.id, + ) + assert entity_entry.entity_id == f"{SENSOR_NAME}_2" + assert entity_entry.unique_id == old_unique_id_2 + # Add a ready node, unique ID should be migrated + event = {"node": node} + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + # Check that new RegistryEntry is created using new unique ID format + entity_entry = ent_reg.async_get(SENSOR_NAME) + new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049" + assert entity_entry.unique_id == new_unique_id + + # Check that the old entities stuck around because we skipped the migration step + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_1) + assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_2) + + +async def test_old_entity_migration_notification_binary_sensor( + hass, multisensor_6_state, client, integration +): + """Test old entity on a different endpoint is migrated to a new one for a notification binary sensor.""" + node = Node(client, multisensor_6_state) + + ent_reg = er.async_get(hass) + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_or_create( + config_entry_id=integration.entry_id, identifiers={get_device_id(client, node)} + ) + + entity_name = NOTIFICATION_MOTION_BINARY_SENSOR.split(".")[1] + + # Create entity RegistryEntry using old unique ID format + old_unique_id = f"{client.driver.controller.home_id}.52-113-1-Home Security-Motion sensor status.8" + entity_entry = ent_reg.async_get_or_create( + "binary_sensor", + DOMAIN, + old_unique_id, + suggested_object_id=entity_name, + config_entry=integration, + original_name=entity_name, + device_id=device.id, + ) + assert entity_entry.entity_id == NOTIFICATION_MOTION_BINARY_SENSOR + assert entity_entry.unique_id == old_unique_id + + # Do this twice to make sure re-interview doesn't do anything weird + for _ in range(0, 2): + # Add a ready node, unique ID should be migrated + event = {"node": node} + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + # Check that new RegistryEntry is using new unique ID format + entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_BINARY_SENSOR) + new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status.8" + assert entity_entry.unique_id == new_unique_id + assert ( + ent_reg.async_get_entity_id("binary_sensor", DOMAIN, old_unique_id) is None + ) async def test_on_node_added_not_ready(