Home Assistant Unofficial Reference 2024.12.1
migration.py
Go to the documentation of this file.
1 """Various helpers to handle config entry and api schema migrations."""
2 
3 import logging
4 
5 from aiohue import HueBridgeV2
6 from aiohue.discovery import is_v2_bridge
7 from aiohue.v2.models.device import DeviceArchetypes
8 from aiohue.v2.models.resource import ResourceTypes
9 
10 from homeassistant import core
11 from homeassistant.components.binary_sensor import BinarySensorDeviceClass
12 from homeassistant.components.sensor import SensorDeviceClass
13 from homeassistant.config_entries import ConfigEntry
14 from homeassistant.const import CONF_API_KEY, CONF_API_VERSION, CONF_HOST, CONF_USERNAME
15 from homeassistant.helpers import (
16  aiohttp_client,
17  device_registry as dr,
18  entity_registry as er,
19 )
20 
21 from .const import DOMAIN
22 
23 LOGGER = logging.getLogger(__name__)
24 
25 
26 async def check_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> None:
27  """Check if config entry needs any migration actions."""
28  host = entry.data[CONF_HOST]
29 
30  # migrate CONF_USERNAME --> CONF_API_KEY
31  if CONF_USERNAME in entry.data:
32  LOGGER.info("Migrate %s to %s in schema", CONF_USERNAME, CONF_API_KEY)
33  data = dict(entry.data)
34  data[CONF_API_KEY] = data.pop(CONF_USERNAME)
35  hass.config_entries.async_update_entry(entry, data=data)
36 
37  if (conf_api_version := entry.data.get(CONF_API_VERSION, 1)) == 1:
38  # a bridge might have upgraded firmware since last run so
39  # we discover its capabilities at every startup
40  websession = aiohttp_client.async_get_clientsession(hass)
41  if await is_v2_bridge(host, websession):
42  supported_api_version = 2
43  else:
44  supported_api_version = 1
45  LOGGER.debug(
46  "Configured api version is %s and supported api version %s for bridge %s",
47  conf_api_version,
48  supported_api_version,
49  host,
50  )
51 
52  # the call to `is_v2_bridge` returns (silently) False even on connection error
53  # so if a migration is needed it will be done on next startup
54 
55  if conf_api_version == 1 and supported_api_version == 2:
56  # run entity/device schema migration for v2
57  await handle_v2_migration(hass, entry)
58 
59  # store api version in entry data
60  if (
61  CONF_API_VERSION not in entry.data
62  or conf_api_version != supported_api_version
63  ):
64  data = dict(entry.data)
65  data[CONF_API_VERSION] = supported_api_version
66  hass.config_entries.async_update_entry(entry, data=data)
67 
68 
69 async def handle_v2_migration(hass: core.HomeAssistant, entry: ConfigEntry) -> None:
70  """Perform migration of devices and entities to V2 Id's."""
71  host = entry.data[CONF_HOST]
72  api_key = entry.data[CONF_API_KEY]
73  dev_reg = dr.async_get(hass)
74  ent_reg = er.async_get(hass)
75  LOGGER.info("Start of migration of devices and entities to support API schema 2")
76 
77  # Create mapping of mac address to HA device id's.
78  # Identifier in dev reg should be mac-address,
79  # but in some cases it has a postfix like `-0b` or `-01`.
80  dev_ids = {}
81  for hass_dev in dr.async_entries_for_config_entry(dev_reg, entry.entry_id):
82  for domain, mac in hass_dev.identifiers:
83  if domain != DOMAIN:
84  continue
85  normalized_mac = mac.split("-")[0]
86  dev_ids[normalized_mac] = hass_dev.id
87 
88  # initialize bridge connection just for the migration
89  async with HueBridgeV2(host, api_key) as api:
90  sensor_class_mapping = {
91  SensorDeviceClass.BATTERY.value: ResourceTypes.DEVICE_POWER,
92  BinarySensorDeviceClass.MOTION.value: ResourceTypes.MOTION,
93  SensorDeviceClass.ILLUMINANCE.value: ResourceTypes.LIGHT_LEVEL,
94  SensorDeviceClass.TEMPERATURE.value: ResourceTypes.TEMPERATURE,
95  }
96 
97  # migrate entities attached to a device
98  for hue_dev in api.devices:
99  zigbee = api.devices.get_zigbee_connectivity(hue_dev.id)
100  if not zigbee or not zigbee.mac_address:
101  # not a zigbee device or invalid mac
102  continue
103 
104  # get existing device by V1 identifier (mac address)
105  if hue_dev.product_data.product_archetype == DeviceArchetypes.BRIDGE_V2:
106  hass_dev_id = dev_ids.get(api.config.bridge_id.upper())
107  else:
108  hass_dev_id = dev_ids.get(zigbee.mac_address)
109  if hass_dev_id is None:
110  # can be safely ignored, this device does not exist in current config
111  LOGGER.debug(
112  (
113  "Ignoring device %s (%s) as it does not (yet) exist in the"
114  " device registry"
115  ),
116  hue_dev.metadata.name,
117  hue_dev.id,
118  )
119  continue
120  dev_reg.async_update_device(
121  hass_dev_id, new_identifiers={(DOMAIN, hue_dev.id)}
122  )
123  LOGGER.info("Migrated device %s (%s)", hue_dev.metadata.name, hass_dev_id)
124 
125  # loop through all entities for device and find match
126  for ent in er.async_entries_for_device(ent_reg, hass_dev_id, True):
127  if ent.entity_id.startswith("light"):
128  # migrate light
129  # should always return one lightid here
130  new_unique_id = next(iter(hue_dev.lights), None)
131  else:
132  # migrate sensors
133  matched_dev_class = sensor_class_mapping.get(
134  ent.original_device_class or "unknown"
135  )
136  new_unique_id = next(
137  (
138  sensor.id
139  for sensor in api.devices.get_sensors(hue_dev.id)
140  if sensor.type == matched_dev_class
141  ),
142  None,
143  )
144 
145  if new_unique_id is None:
146  # this may happen if we're looking at orphaned or unsupported entity
147  LOGGER.warning(
148  (
149  "Skip migration of %s because it no longer exists on the"
150  " bridge"
151  ),
152  ent.entity_id,
153  )
154  continue
155 
156  try:
157  ent_reg.async_update_entity(
158  ent.entity_id, new_unique_id=new_unique_id
159  )
160  except ValueError:
161  # assume edge case where the entity was already migrated in a previous run
162  # which got aborted somehow and we do not want
163  # to crash the entire integration init
164  LOGGER.warning(
165  "Skip migration of %s because it already exists",
166  ent.entity_id,
167  )
168  else:
169  LOGGER.info(
170  "Migrated entity %s from unique id %s to %s",
171  ent.entity_id,
172  ent.unique_id,
173  new_unique_id,
174  )
175 
176  # migrate entities that are not connected to a device (groups)
177  for ent in er.async_entries_for_config_entry(ent_reg, entry.entry_id):
178  if ent.device_id is not None:
179  continue
180  if "-" in ent.unique_id:
181  # handle case where unique id is v2-id of group/zone
182  hue_group = api.groups.get(ent.unique_id)
183  else:
184  # handle case where the unique id is just the v1 id
185  v1_id = f"/groups/{ent.unique_id}"
186  hue_group = api.groups.room.get_by_v1_id(
187  v1_id
188  ) or api.groups.zone.get_by_v1_id(v1_id)
189  if hue_group is None or hue_group.grouped_light is None:
190  # this may happen if we're looking at some orphaned entity
191  LOGGER.warning(
192  "Skip migration of %s because it no longer exist on the bridge",
193  ent.entity_id,
194  )
195  continue
196  new_unique_id = hue_group.grouped_light
197  LOGGER.info(
198  "Migrating %s from unique id %s to %s ",
199  ent.entity_id,
200  ent.unique_id,
201  new_unique_id,
202  )
203  try:
204  ent_reg.async_update_entity(ent.entity_id, new_unique_id=new_unique_id)
205  except ValueError:
206  # assume edge case where the entity was already migrated in a previous run
207  # which got aborted somehow and we do not want
208  # to crash the entire integration init
209  LOGGER.warning(
210  "Skip migration of %s because it already exists",
211  ent.entity_id,
212  )
213  LOGGER.info("Migration of devices and entities to support API schema 2 finished")
None check_migration(core.HomeAssistant hass, ConfigEntry entry)
Definition: migration.py:26
None handle_v2_migration(core.HomeAssistant hass, ConfigEntry entry)
Definition: migration.py:69