"""UniFi Protect data migrations.""" from __future__ import annotations import logging from aiohttp.client_exceptions import ServerDisconnectedError from pyunifiprotect import ProtectApiClient from pyunifiprotect.data import NVR, Bootstrap, ProtectAdoptableDeviceModel from pyunifiprotect.exceptions import ClientError from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry as er _LOGGER = logging.getLogger(__name__) async def async_migrate_data( hass: HomeAssistant, entry: ConfigEntry, protect: ProtectApiClient ) -> None: """Run all valid UniFi Protect data migrations.""" _LOGGER.debug("Start Migrate: async_migrate_buttons") await async_migrate_buttons(hass, entry, protect) _LOGGER.debug("Completed Migrate: async_migrate_buttons") _LOGGER.debug("Start Migrate: async_migrate_device_ids") await async_migrate_device_ids(hass, entry, protect) _LOGGER.debug("Completed Migrate: async_migrate_device_ids") async def async_get_bootstrap(protect: ProtectApiClient) -> Bootstrap: """Get UniFi Protect bootstrap or raise appropriate HA error.""" try: bootstrap = await protect.get_bootstrap() except (TimeoutError, ClientError, ServerDisconnectedError) as err: raise ConfigEntryNotReady from err return bootstrap async def async_migrate_buttons( hass: HomeAssistant, entry: ConfigEntry, protect: ProtectApiClient ) -> None: """ Migrate existing Reboot button unique IDs from {device_id} to {deivce_id}_reboot. This allows for additional types of buttons that are outside of just a reboot button. Added in 2022.6.0. """ registry = er.async_get(hass) to_migrate = [] for entity in er.async_entries_for_config_entry(registry, entry.entry_id): if entity.domain == Platform.BUTTON and "_" not in entity.unique_id: _LOGGER.debug("Button %s needs migration", entity.entity_id) to_migrate.append(entity) if len(to_migrate) == 0: _LOGGER.debug("No button entities need migration") return bootstrap = await async_get_bootstrap(protect) count = 0 for button in to_migrate: device = bootstrap.get_device_from_id(button.unique_id) if device is None: continue new_unique_id = f"{device.id}_reboot" _LOGGER.debug( "Migrating entity %s (old unique_id: %s, new unique_id: %s)", button.entity_id, button.unique_id, new_unique_id, ) try: registry.async_update_entity(button.entity_id, new_unique_id=new_unique_id) except ValueError: _LOGGER.warning( "Could not migrate entity %s (old unique_id: %s, new unique_id: %s)", button.entity_id, button.unique_id, new_unique_id, ) else: count += 1 if count < len(to_migrate): _LOGGER.warning("Failed to migate %s reboot buttons", len(to_migrate) - count) async def async_migrate_device_ids( hass: HomeAssistant, entry: ConfigEntry, protect: ProtectApiClient ) -> None: """ Migrate unique IDs from {device_id}_{name} format to {mac}_{name} format. This makes devices persist better with in HA. Anything a device is unadopted/readopted or the Protect instance has to rebuild the disk array, the device IDs of Protect devices can change. This causes a ton of orphaned entities and loss of historical data. MAC addresses are the one persistent identifier a device has that does not change. Added in 2022.7.0. """ registry = er.async_get(hass) to_migrate = [] for entity in er.async_entries_for_config_entry(registry, entry.entry_id): parts = entity.unique_id.split("_") # device ID = 24 characters, MAC = 12 if len(parts[0]) == 24: _LOGGER.debug("Entity %s needs migration", entity.entity_id) to_migrate.append(entity) if len(to_migrate) == 0: _LOGGER.debug("No entities need migration to MAC address ID") return bootstrap = await async_get_bootstrap(protect) count = 0 for entity in to_migrate: parts = entity.unique_id.split("_") if parts[0] == bootstrap.nvr.id: device: NVR | ProtectAdoptableDeviceModel | None = bootstrap.nvr else: device = bootstrap.get_device_from_id(parts[0]) if device is None: continue new_unique_id = device.mac if len(parts) > 1: new_unique_id = f"{device.mac}_{'_'.join(parts[1:])}" _LOGGER.debug( "Migrating entity %s (old unique_id: %s, new unique_id: %s)", entity.entity_id, entity.unique_id, new_unique_id, ) try: registry.async_update_entity(entity.entity_id, new_unique_id=new_unique_id) except ValueError as err: _LOGGER.warning( ( "Could not migrate entity %s (old unique_id: %s, new unique_id:" " %s): %s" ), entity.entity_id, entity.unique_id, new_unique_id, err, ) else: count += 1 if count < len(to_migrate): _LOGGER.warning("Failed to migrate %s entities", len(to_migrate) - count)