Home Assistant Unofficial Reference 2024.12.1
migrate.py
Go to the documentation of this file.
1 """Functions used to migrate unique IDs for Z-Wave JS entities."""
2 
3 from __future__ import annotations
4 
5 from dataclasses import dataclass
6 import logging
7 
8 from zwave_js_server.model.driver import Driver
9 from zwave_js_server.model.node import Node
10 from zwave_js_server.model.value import Value as ZwaveValue
11 
12 from homeassistant.const import STATE_UNAVAILABLE, Platform
13 from homeassistant.core import HomeAssistant, callback
14 from homeassistant.helpers import device_registry as dr, entity_registry as er
15 
16 from .const import DOMAIN
17 from .discovery import ZwaveDiscoveryInfo
18 from .helpers import get_unique_id, get_valueless_base_unique_id
19 
20 _LOGGER = logging.getLogger(__name__)
21 
22 
23 @dataclass
24 class ValueID:
25  """Class to represent a Value ID."""
26 
27  command_class: str
28  endpoint: str
29  property_: str
30  property_key: str | None = None
31 
32  @staticmethod
33  def from_unique_id(unique_id: str) -> ValueID:
34  """Get a ValueID from a unique ID.
35 
36  This also works for Notification CC Binary Sensors which have their
37  own unique ID format.
38  """
39  return ValueID.from_string_id(unique_id.split(".")[1])
40 
41  @staticmethod
42  def from_string_id(value_id_str: str) -> ValueID:
43  """Get a ValueID from a string representation of the value ID."""
44  parts = value_id_str.split("-")
45  property_key = parts[4] if len(parts) > 4 else None
46  return ValueID(parts[1], parts[2], parts[3], property_key=property_key)
47 
48  def is_same_value_different_endpoints(self, other: ValueID) -> bool:
49  """Return whether two value IDs are the same excluding endpoint."""
50  return (
51  self.command_classcommand_class == other.command_class
52  and self.property_property_ == other.property_
53  and self.property_keyproperty_key == other.property_key
54  and self.endpoint != other.endpoint
55  )
56 
57 
58 @callback
60  hass: HomeAssistant,
61  ent_reg: er.EntityRegistry,
62  registered_unique_ids: set[str],
63  platform: Platform,
64  device: dr.DeviceEntry,
65  unique_id: str,
66 ) -> None:
67  """Migrate existing entity if current one can't be found and an old one exists."""
68  # If we can find an existing entity with this unique ID, there's nothing to migrate
69  if ent_reg.async_get_entity_id(platform, DOMAIN, unique_id):
70  return
71 
72  value_id = ValueID.from_unique_id(unique_id)
73 
74  # Look for existing entities in the registry that could be the same value but on
75  # a different endpoint
76  existing_entity_entries: list[er.RegistryEntry] = []
77  for entry in er.async_entries_for_device(ent_reg, device.id):
78  # If entity is not in the domain for this discovery info or entity has already
79  # been processed, skip it
80  if entry.domain != platform or entry.unique_id in registered_unique_ids:
81  continue
82 
83  try:
84  old_ent_value_id = ValueID.from_unique_id(entry.unique_id)
85  # Skip non value ID based unique ID's (e.g. node status sensor)
86  except IndexError:
87  continue
88 
89  if value_id.is_same_value_different_endpoints(old_ent_value_id):
90  existing_entity_entries.append(entry)
91  # We can return early if we get more than one result
92  if len(existing_entity_entries) > 1:
93  return
94 
95  # If we couldn't find any results, return early
96  if not existing_entity_entries:
97  return
98 
99  entry = existing_entity_entries[0]
100  state = hass.states.get(entry.entity_id)
101 
102  if not state or state.state == STATE_UNAVAILABLE:
103  async_migrate_unique_id(ent_reg, platform, entry.unique_id, unique_id)
104 
105 
106 @callback
108  ent_reg: er.EntityRegistry,
109  platform: Platform,
110  old_unique_id: str,
111  new_unique_id: str,
112 ) -> None:
113  """Check if entity with old unique ID exists, and if so migrate it to new ID."""
114  if not (entity_id := ent_reg.async_get_entity_id(platform, DOMAIN, old_unique_id)):
115  return
116 
117  _LOGGER.debug(
118  "Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
119  entity_id,
120  old_unique_id,
121  new_unique_id,
122  )
123  try:
124  ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)
125  except ValueError:
126  _LOGGER.debug(
127  (
128  "Entity %s can't be migrated because the unique ID is taken; "
129  "Cleaning it up since it is likely no longer valid"
130  ),
131  entity_id,
132  )
133  ent_reg.async_remove(entity_id)
134 
135 
136 @callback
138  hass: HomeAssistant,
139  ent_reg: er.EntityRegistry,
140  registered_unique_ids: set[str],
141  device: dr.DeviceEntry,
142  driver: Driver,
143  disc_info: ZwaveDiscoveryInfo,
144 ) -> None:
145  """Migrate unique ID for entity/entities tied to discovered value."""
146 
147  new_unique_id = get_unique_id(driver, disc_info.primary_value.value_id)
148 
149  # On reinterviews, there is no point in going through this logic again for already
150  # discovered values
151  if new_unique_id in registered_unique_ids:
152  return
153 
154  # Migration logic was added in 2021.3 to handle a breaking change to the value_id
155  # format. Some time in the future, the logic to migrate unique IDs can be removed.
156 
157  # 2021.2.*, 2021.3.0b0, and 2021.3.0 formats
158  old_unique_ids = [
159  get_unique_id(driver, value_id)
160  for value_id in get_old_value_ids(disc_info.primary_value)
161  ]
162 
163  if (
164  disc_info.platform == Platform.BINARY_SENSOR
165  and disc_info.platform_hint == "notification"
166  ):
167  for state_key in disc_info.primary_value.metadata.states:
168  # ignore idle key (0)
169  if state_key == "0":
170  continue
171 
172  new_bin_sensor_unique_id = f"{new_unique_id}.{state_key}"
173 
174  # On reinterviews, there is no point in going through this logic again
175  # for already discovered values
176  if new_bin_sensor_unique_id in registered_unique_ids:
177  continue
178 
179  # Unique ID migration
180  for old_unique_id in old_unique_ids:
182  ent_reg,
183  disc_info.platform,
184  f"{old_unique_id}.{state_key}",
185  new_bin_sensor_unique_id,
186  )
187 
188  # Migrate entities in case upstream changes cause endpoint change
190  hass,
191  ent_reg,
192  registered_unique_ids,
193  disc_info.platform,
194  device,
195  new_bin_sensor_unique_id,
196  )
197  registered_unique_ids.add(new_bin_sensor_unique_id)
198 
199  # Once we've iterated through all state keys, we are done
200  return
201 
202  # Unique ID migration
203  for old_unique_id in old_unique_ids:
205  ent_reg, disc_info.platform, old_unique_id, new_unique_id
206  )
207 
208  # Migrate entities in case upstream changes cause endpoint change
210  hass, ent_reg, registered_unique_ids, disc_info.platform, device, new_unique_id
211  )
212  registered_unique_ids.add(new_unique_id)
213 
214 
215 @callback
217  hass: HomeAssistant, driver: Driver, node: Node, key_map: dict[str, str]
218 ) -> None:
219  """Migrate statistics sensors to new unique IDs.
220 
221  - Migrate camel case keys in unique IDs to snake keys.
222  """
223  ent_reg = er.async_get(hass)
224  base_unique_id = f"{get_valueless_base_unique_id(driver, node)}.statistics"
225  for new_key, old_key in key_map.items():
226  if new_key == old_key:
227  continue
228  old_unique_id = f"{base_unique_id}_{old_key}"
229  new_unique_id = f"{base_unique_id}_{new_key}"
230  async_migrate_unique_id(ent_reg, Platform.SENSOR, old_unique_id, new_unique_id)
231 
232 
233 @callback
234 def get_old_value_ids(value: ZwaveValue) -> list[str]:
235  """Get old value IDs so we can migrate entity unique ID."""
236  value_ids = []
237 
238  # Pre 2021.3.0 value ID
239  command_class = value.command_class
240  endpoint = value.endpoint or "00"
241  property_ = value.property_
242  property_key_name = value.property_key_name or "00"
243  value_ids.append(
244  f"{value.node.node_id}.{value.node.node_id}-{command_class}-{endpoint}-"
245  f"{property_}-{property_key_name}"
246  )
247 
248  endpoint = "00" if value.endpoint is None else value.endpoint
249  property_key = "00" if value.property_key is None else value.property_key
250  property_key_name = value.property_key_name or "00"
251 
252  value_id = (
253  f"{value.node.node_id}-{command_class}-{endpoint}-"
254  f"{property_}-{property_key}-{property_key_name}"
255  )
256  # 2021.3.0b0 and 2021.3.0 value IDs
257  value_ids.extend([f"{value.node.node_id}.{value_id}", value_id])
258 
259  return value_ids
bool is_same_value_different_endpoints(self, ValueID other)
Definition: migrate.py:48
ValueID from_string_id(str value_id_str)
Definition: migrate.py:42
None async_migrate_discovered_value(HomeAssistant hass, er.EntityRegistry ent_reg, set[str] registered_unique_ids, dr.DeviceEntry device, Driver driver, ZwaveDiscoveryInfo disc_info)
Definition: migrate.py:144
None async_migrate_unique_id(er.EntityRegistry ent_reg, Platform platform, str old_unique_id, str new_unique_id)
Definition: migrate.py:112
None async_migrate_statistics_sensors(HomeAssistant hass, Driver driver, Node node, dict[str, str] key_map)
Definition: migrate.py:218
None async_migrate_old_entity(HomeAssistant hass, er.EntityRegistry ent_reg, set[str] registered_unique_ids, Platform platform, dr.DeviceEntry device, str unique_id)
Definition: migrate.py:66
list[str] get_old_value_ids(ZwaveValue value)
Definition: migrate.py:234