Migrate existing zwave_js entities if endpoint has changed (#48963)

* Migrate existing zwave_js entities if endpoint has changed

* better function name

* cleanup code

* return as early as we can

* use defaultdict instead of setdefault

* PR comments

* re-add missing logic

* set defaultdict outside of for loop

* additional cleanup

* parametrize tests

* fix reinterview logic

* test that we skip migration when multiple entities are found

* Update tests/components/zwave_js/test_init.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Raman Gupta 2021-04-13 10:37:55 -04:00 committed by GitHub
parent de569982a4
commit fe6d6895aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 355 additions and 183 deletions

View file

@ -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:

View file

@ -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

View file

@ -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(