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 from __future__ import annotations
import asyncio import asyncio
from collections import defaultdict
from typing import Callable from typing import Callable
from async_timeout import timeout from async_timeout import timeout
@ -87,7 +88,7 @@ def register_node_in_dev_reg(
dev_reg: device_registry.DeviceRegistry, dev_reg: device_registry.DeviceRegistry,
client: ZwaveClient, client: ZwaveClient,
node: ZwaveNode, node: ZwaveNode,
) -> None: ) -> device_registry.DeviceEntry:
"""Register node in dev reg.""" """Register node in dev reg."""
params = { params = {
"config_entry_id": entry.entry_id, "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) 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: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Z-Wave JS from a config entry.""" """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_UNSUBSCRIBE] = unsubscribe_callbacks
entry_hass_data[DATA_PLATFORM_SETUP] = {} 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: async def async_on_node_ready(node: ZwaveNode) -> None:
"""Handle node ready event.""" """Handle node ready event."""
LOGGER.debug("Processing node %s", node) 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] platform_setup_tasks = entry_hass_data[DATA_PLATFORM_SETUP]
# register (or update) node in device registry # 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 # run discovery on all node values and create/update entities
for disc_info in async_discover_values(node): 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 # 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 # the value_id format. Some time in the future, this call (as well as the
# helper functions) can be removed. # helper functions) can be removed.
async_migrate_discovered_value(ent_reg, client, disc_info) async_migrate_discovered_value(
if disc_info.platform not in platform_setup_tasks: hass,
platform_setup_tasks[disc_info.platform] = hass.async_create_task( ent_reg,
hass.config_entries.async_forward_entry_setup( registered_unique_ids[device.id][platform],
entry, disc_info.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) LOGGER.debug("Discovered entity: %s", disc_info)
async_dispatcher_send( 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 # 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}) device = dev_reg.async_get_device({dev_id})
# note: removal of entity registry entry is handled by core # note: removal of entity registry entry is handled by core
dev_reg.async_remove_device(device.id) # type: ignore dev_reg.async_remove_device(device.id) # type: ignore
registered_unique_ids.pop(device.id, None) # type: ignore
@callback @callback
def async_on_value_notification(notification: ValueNotification) -> None: 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.""" """Functions used to migrate unique IDs for Z-Wave JS entities."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
import logging import logging
from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.model.value import Value as ZwaveValue from zwave_js_server.model.value import Value as ZwaveValue
from homeassistant.core import callback from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.helpers.entity_registry import EntityRegistry 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 .const import DOMAIN
from .discovery import ZwaveDiscoveryInfo from .discovery import ZwaveDiscoveryInfo
@ -16,8 +23,88 @@ from .helpers import get_unique_id
_LOGGER = logging.getLogger(__name__) _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 @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 ent_reg: EntityRegistry, platform: str, old_unique_id: str, new_unique_id: str
) -> None: ) -> None:
"""Check if entity with old unique ID exists, and if so migrate it to new ID.""" """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, new_unique_id,
) )
try: try:
ent_reg.async_update_entity( ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
entity_id,
new_unique_id=new_unique_id,
)
except ValueError: except ValueError:
_LOGGER.debug( _LOGGER.debug(
( (
@ -46,43 +130,87 @@ def async_migrate_entity(
@callback @callback
def async_migrate_discovered_value( 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: ) -> None:
"""Migrate unique ID for entity/entities tied to discovered value.""" """Migrate unique ID for entity/entities tied to discovered value."""
new_unique_id = get_unique_id( new_unique_id = get_unique_id(
client.driver.controller.home_id, client.driver.controller.home_id,
disc_info.primary_value.value_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 # 2021.2.*, 2021.3.0b0, and 2021.3.0 formats
for value_id in get_old_value_ids(disc_info.primary_value): old_unique_ids = [
old_unique_id = get_unique_id( get_unique_id(
client.driver.controller.home_id, client.driver.controller.home_id,
value_id, value_id,
) )
# Most entities have the same ID format, but notification binary sensors for value_id in get_old_value_ids(disc_info.primary_value)
# 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
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, ent_reg,
disc_info.platform, disc_info.platform,
f"{old_unique_id}.{state_key}", 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 # Migrate entities in case upstream changes cause endpoint change
# next item async_migrate_old_entity(
continue 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 @callback

View File

@ -162,19 +162,27 @@ async def test_unique_id_migration_dupes(
entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR) entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR)
new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature" new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature"
assert entity_entry.unique_id == new_unique_id assert entity_entry.unique_id == new_unique_id
assert ent_reg.async_get_entity_id("sensor", DOMAIN, old_unique_id_1) is None
assert ent_reg.async_get(f"{AIR_TEMPERATURE_SENSOR}_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): @pytest.mark.parametrize(
"""Test unique ID is migrated from old format to new (version 1).""" "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) ent_reg = er.async_get(hass)
# Migrate version 1 # Migrate version 1
entity_name = AIR_TEMPERATURE_SENSOR.split(".")[1] entity_name = AIR_TEMPERATURE_SENSOR.split(".")[1]
# Create entity RegistryEntry using old unique ID format # 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( entity_entry = ent_reg.async_get_or_create(
"sensor", "sensor",
DOMAIN, 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) entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR)
new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature" new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature"
assert entity_entry.unique_id == new_unique_id 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): @pytest.mark.parametrize(
"""Test unique ID is migrated from old format to new (version 2).""" "id",
ent_reg = er.async_get(hass) [
# Migrate version 2 ("32.32-50-00-value-W_Consumed"),
ILLUMINANCE_SENSOR = "sensor.multisensor_6_illuminance" ("32.32-50-0-value-66049-W_Consumed"),
entity_name = ILLUMINANCE_SENSOR.split(".")[1] ("32-50-0-value-66049-W_Consumed"),
],
# Create entity RegistryEntry using old unique ID format )
old_unique_id = f"{client.driver.controller.home_id}.52.52-49-0-Illuminance-00-00" async def test_unique_id_migration_property_key(
entity_entry = ent_reg.async_get_or_create( hass, hank_binary_switch_state, client, integration, id
"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
): ):
"""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) ent_reg = er.async_get(hass)
SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed"
entity_name = SENSOR_NAME.split(".")[1] entity_name = SENSOR_NAME.split(".")[1]
# Create entity RegistryEntry using old unique ID format # Create entity RegistryEntry using old unique ID format
old_unique_id = f"{client.driver.controller.home_id}.32.32-50-00-value-W_Consumed" old_unique_id = f"{client.driver.controller.home_id}.{id}"
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"
entity_entry = ent_reg.async_get_or_create( entity_entry = ent_reg.async_get_or_create(
"sensor", "sensor",
DOMAIN, DOMAIN,
@ -370,6 +249,7 @@ async def test_unique_id_migration_property_key_v3(
entity_entry = ent_reg.async_get(SENSOR_NAME) entity_entry = ent_reg.async_get(SENSOR_NAME)
new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049" new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049"
assert entity_entry.unique_id == new_unique_id 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( 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) 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" 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 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( async def test_on_node_added_not_ready(