161 lines
5.4 KiB
Python
161 lines
5.4 KiB
Python
"""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)
|