From 424526db7eeec63a31cd162bf5983568236aee88 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 24 Feb 2021 09:41:10 -0500 Subject: [PATCH] Migrate zwave_js entities to use new unique ID format (#46979) * migrate zwave_js entities to use new unique ID format * remove extra param from helper * add comment to remove migration logic in the future * update comment * use instance attribute instead of calling functino on every state update --- homeassistant/components/zwave_js/__init__.py | 27 ++++++++++++++-- homeassistant/components/zwave_js/entity.py | 7 +++-- homeassistant/components/zwave_js/helpers.py | 17 ++++++++++ tests/components/zwave_js/test_init.py | 31 +++++++++++++++++++ 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 062b28cf6a9..cc58e31066a 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -43,7 +43,7 @@ from .const import ( ZWAVE_JS_EVENT, ) from .discovery import async_discover_values -from .helpers import get_device_id +from .helpers import get_device_id, get_old_value_id, get_unique_id from .services import ZWaveServices LOGGER = logging.getLogger(__package__) @@ -83,6 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Z-Wave JS from a config entry.""" client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) dev_reg = await device_registry.async_get_registry(hass) + ent_reg = entity_registry.async_get(hass) @callback def async_on_node_ready(node: ZwaveNode) -> None: @@ -95,6 +96,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # run discovery on all node values and create/update entities for disc_info in async_discover_values(node): LOGGER.debug("Discovered entity: %s", disc_info) + + # This migration logic was added in 2021.3 to handle breaking change to + # value_id format. Some time in the future, this code block + # (and get_old_value_id helper) can be removed. + old_value_id = get_old_value_id(disc_info.primary_value) + old_unique_id = get_unique_id( + client.driver.controller.home_id, old_value_id + ) + if entity_id := ent_reg.async_get_entity_id( + disc_info.platform, DOMAIN, old_unique_id + ): + LOGGER.debug( + "Entity %s is using old unique ID, migrating to new one", entity_id + ) + ent_reg.async_update_entity( + entity_id, + new_unique_id=get_unique_id( + client.driver.controller.home_id, + disc_info.primary_value.value_id, + ), + ) + async_dispatcher_send( hass, f"{DOMAIN}_{entry.entry_id}_add_{disc_info.platform}", disc_info ) @@ -193,7 +216,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_UNSUBSCRIBE: unsubscribe_callbacks, } - services = ZWaveServices(hass, entity_registry.async_get(hass)) + services = ZWaveServices(hass, ent_reg) services.async_register() # Set up websocket API diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index cb898e861e9..685fe50c9b6 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -13,7 +13,7 @@ from homeassistant.helpers.entity import Entity from .const import DOMAIN from .discovery import ZwaveDiscoveryInfo -from .helpers import get_device_id +from .helpers import get_device_id, get_unique_id LOGGER = logging.getLogger(__name__) @@ -31,6 +31,9 @@ class ZWaveBaseEntity(Entity): self.client = client self.info = info self._name = self.generate_name() + self._unique_id = get_unique_id( + self.client.driver.controller.home_id, self.info.value_id + ) # entities requiring additional values, can add extra ids to this list self.watched_value_ids = {self.info.primary_value.value_id} @@ -128,7 +131,7 @@ class ZWaveBaseEntity(Entity): @property def unique_id(self) -> str: """Return the unique_id of the entity.""" - return f"{self.client.driver.controller.home_id}.{self.info.value_id}" + return self._unique_id @property def available(self) -> bool: diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index cc00c39b747..9582b7ee054 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -3,6 +3,7 @@ from typing import List, Tuple, cast from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.node import Node as ZwaveNode +from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -12,6 +13,22 @@ from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg from .const import DATA_CLIENT, DOMAIN +@callback +def get_old_value_id(value: ZwaveValue) -> str: + """Get old value ID so we can migrate entity unique ID.""" + command_class = value.command_class + endpoint = value.endpoint or "00" + property_ = value.property_ + property_key_name = value.property_key_name or "00" + return f"{value.node.node_id}-{command_class}-{endpoint}-{property_}-{property_key_name}" + + +@callback +def get_unique_id(home_id: str, value_id: str) -> str: + """Get unique ID from home ID and value ID.""" + return f"{home_id}.{value_id}" + + @callback def get_device_id(client: ZwaveClient, node: ZwaveNode) -> Tuple[str, str]: """Get device registry identifier for Z-Wave node.""" diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 6a255becf2d..3634454544f 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -124,6 +124,37 @@ async def test_on_node_added_ready( ) +async def test_unique_id_migration(hass, multisensor_6_state, client, integration): + """Test unique ID is migrated from old format to new.""" + ent_reg = entity_registry.async_get(hass) + 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-49-00-Air temperature-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 == AIR_TEMPERATURE_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(AIR_TEMPERATURE_SENSOR) + new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature-00-00" + assert entity_entry.unique_id == new_unique_id + + async def test_on_node_added_not_ready( hass, multisensor_6_state, client, integration, device_registry ):