Update UniFi Protect to use MAC address for unique ID (#73508)
This commit is contained in:
parent
7714183118
commit
b19b6ec6ea
19 changed files with 266 additions and 91 deletions
|
@ -128,5 +128,5 @@ async def async_remove_config_entry_device(
|
||||||
assert api is not None
|
assert api is not None
|
||||||
return api.bootstrap.nvr.mac not in unifi_macs and not any(
|
return api.bootstrap.nvr.mac not in unifi_macs and not any(
|
||||||
device.mac in unifi_macs
|
device.mac in unifi_macs
|
||||||
for device in async_get_devices(api, DEVICES_THAT_ADOPT)
|
for device in async_get_devices(api.bootstrap, DEVICES_THAT_ADOPT)
|
||||||
)
|
)
|
||||||
|
|
|
@ -110,10 +110,10 @@ class ProtectCamera(ProtectDeviceEntity, Camera):
|
||||||
super().__init__(data, camera)
|
super().__init__(data, camera)
|
||||||
|
|
||||||
if self._secure:
|
if self._secure:
|
||||||
self._attr_unique_id = f"{self.device.id}_{self.channel.id}"
|
self._attr_unique_id = f"{self.device.mac}_{self.channel.id}"
|
||||||
self._attr_name = f"{self.device.name} {self.channel.name}"
|
self._attr_name = f"{self.device.name} {self.channel.name}"
|
||||||
else:
|
else:
|
||||||
self._attr_unique_id = f"{self.device.id}_{self.channel.id}_insecure"
|
self._attr_unique_id = f"{self.device.mac}_{self.channel.id}_insecure"
|
||||||
self._attr_name = f"{self.device.name} {self.channel.name} Insecure"
|
self._attr_name = f"{self.device.name} {self.channel.name} Insecure"
|
||||||
# only the default (first) channel is enabled by default
|
# only the default (first) channel is enabled by default
|
||||||
self._attr_entity_registry_enabled_default = is_default and secure
|
self._attr_entity_registry_enabled_default = is_default and secure
|
||||||
|
|
|
@ -21,7 +21,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||||
from homeassistant.helpers.event import async_track_time_interval
|
from homeassistant.helpers.event import async_track_time_interval
|
||||||
|
|
||||||
from .const import CONF_DISABLE_RTSP, DEVICES_THAT_ADOPT, DEVICES_WITH_ENTITIES, DOMAIN
|
from .const import CONF_DISABLE_RTSP, DEVICES_THAT_ADOPT, DEVICES_WITH_ENTITIES, DOMAIN
|
||||||
from .utils import async_get_adoptable_devices_by_type, async_get_devices
|
from .utils import async_get_devices, async_get_devices_by_type
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -70,8 +70,8 @@ class ProtectData:
|
||||||
) -> Generator[ProtectAdoptableDeviceModel, None, None]:
|
) -> Generator[ProtectAdoptableDeviceModel, None, None]:
|
||||||
"""Get all devices matching types."""
|
"""Get all devices matching types."""
|
||||||
for device_type in device_types:
|
for device_type in device_types:
|
||||||
yield from async_get_adoptable_devices_by_type(
|
yield from async_get_devices_by_type(
|
||||||
self.api, device_type
|
self.api.bootstrap, device_type
|
||||||
).values()
|
).values()
|
||||||
|
|
||||||
async def async_setup(self) -> None:
|
async def async_setup(self) -> None:
|
||||||
|
@ -153,7 +153,7 @@ class ProtectData:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.async_signal_device_id_update(self.api.bootstrap.nvr.id)
|
self.async_signal_device_id_update(self.api.bootstrap.nvr.id)
|
||||||
for device in async_get_devices(self.api, DEVICES_THAT_ADOPT):
|
for device in async_get_devices(self.api.bootstrap, DEVICES_THAT_ADOPT):
|
||||||
self.async_signal_device_id_update(device.id)
|
self.async_signal_device_id_update(device.id)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
|
|
@ -26,7 +26,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription
|
||||||
from .const import ATTR_EVENT_SCORE, DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN
|
from .const import ATTR_EVENT_SCORE, DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN
|
||||||
from .data import ProtectData
|
from .data import ProtectData
|
||||||
from .models import ProtectRequiredKeysMixin
|
from .models import ProtectRequiredKeysMixin
|
||||||
from .utils import async_get_adoptable_devices_by_type, get_nested_attr
|
from .utils import async_device_by_id, get_nested_attr
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -117,11 +117,11 @@ class ProtectDeviceEntity(Entity):
|
||||||
self.device = device
|
self.device = device
|
||||||
|
|
||||||
if description is None:
|
if description is None:
|
||||||
self._attr_unique_id = f"{self.device.id}"
|
self._attr_unique_id = f"{self.device.mac}"
|
||||||
self._attr_name = f"{self.device.name}"
|
self._attr_name = f"{self.device.name}"
|
||||||
else:
|
else:
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._attr_unique_id = f"{self.device.id}_{description.key}"
|
self._attr_unique_id = f"{self.device.mac}_{description.key}"
|
||||||
name = description.name or ""
|
name = description.name or ""
|
||||||
self._attr_name = f"{self.device.name} {name.title()}"
|
self._attr_name = f"{self.device.name} {name.title()}"
|
||||||
|
|
||||||
|
@ -153,10 +153,11 @@ class ProtectDeviceEntity(Entity):
|
||||||
"""Update Entity object from Protect device."""
|
"""Update Entity object from Protect device."""
|
||||||
if self.data.last_update_success:
|
if self.data.last_update_success:
|
||||||
assert self.device.model
|
assert self.device.model
|
||||||
devices = async_get_adoptable_devices_by_type(
|
device = async_device_by_id(
|
||||||
self.data.api, self.device.model
|
self.data.api.bootstrap, self.device.id, device_type=self.device.model
|
||||||
)
|
)
|
||||||
self.device = devices[self.device.id]
|
assert device is not None
|
||||||
|
self.device = device
|
||||||
|
|
||||||
is_connected = (
|
is_connected = (
|
||||||
self.data.last_update_success and self.device.state == StateType.CONNECTED
|
self.data.last_update_success and self.device.state == StateType.CONNECTED
|
||||||
|
|
|
@ -3,14 +3,18 @@ from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from aiohttp.client_exceptions import ServerDisconnectedError
|
||||||
from pyunifiprotect import ProtectApiClient
|
from pyunifiprotect import ProtectApiClient
|
||||||
|
from pyunifiprotect.data import NVR, Bootstrap, ProtectAdoptableDeviceModel
|
||||||
|
from pyunifiprotect.exceptions import ClientError
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
from .const import DEVICES_THAT_ADOPT
|
from .utils import async_device_by_id
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -24,6 +28,21 @@ async def async_migrate_data(
|
||||||
await async_migrate_buttons(hass, entry, protect)
|
await async_migrate_buttons(hass, entry, protect)
|
||||||
_LOGGER.debug("Completed Migrate: async_migrate_buttons")
|
_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(
|
async def async_migrate_buttons(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, protect: ProtectApiClient
|
hass: HomeAssistant, entry: ConfigEntry, protect: ProtectApiClient
|
||||||
|
@ -47,16 +66,10 @@ async def async_migrate_buttons(
|
||||||
_LOGGER.debug("No button entities need migration")
|
_LOGGER.debug("No button entities need migration")
|
||||||
return
|
return
|
||||||
|
|
||||||
bootstrap = await protect.get_bootstrap()
|
bootstrap = await async_get_bootstrap(protect)
|
||||||
count = 0
|
count = 0
|
||||||
for button in to_migrate:
|
for button in to_migrate:
|
||||||
device = None
|
device = async_device_by_id(bootstrap, button.unique_id)
|
||||||
for model in DEVICES_THAT_ADOPT:
|
|
||||||
attr = f"{model.value}s"
|
|
||||||
device = getattr(bootstrap, attr).get(button.unique_id)
|
|
||||||
if device is not None:
|
|
||||||
break
|
|
||||||
|
|
||||||
if device is None:
|
if device is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -81,3 +94,68 @@ async def async_migrate_buttons(
|
||||||
|
|
||||||
if count < len(to_migrate):
|
if count < len(to_migrate):
|
||||||
_LOGGER.warning("Failed to migate %s reboot buttons", len(to_migrate) - count)
|
_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 = async_device_by_id(bootstrap, 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:
|
||||||
|
print(err)
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Could not migrate entity %s (old unique_id: %s, new unique_id: %s)",
|
||||||
|
entity.entity_id,
|
||||||
|
entity.unique_id,
|
||||||
|
new_unique_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
if count < len(to_migrate):
|
||||||
|
_LOGGER.warning("Failed to migrate %s entities", len(to_migrate) - count)
|
||||||
|
|
|
@ -3,10 +3,11 @@ from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
from typing import Any
|
from typing import Any, cast
|
||||||
|
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
from pyunifiprotect.api import ProtectApiClient
|
from pyunifiprotect.api import ProtectApiClient
|
||||||
|
from pyunifiprotect.data import Chime
|
||||||
from pyunifiprotect.exceptions import BadRequest
|
from pyunifiprotect.exceptions import BadRequest
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
@ -122,8 +123,8 @@ async def set_default_doorbell_text(hass: HomeAssistant, call: ServiceCall) -> N
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_unique_id_to_ufp_device_id(unique_id: str) -> str:
|
def _async_unique_id_to_mac(unique_id: str) -> str:
|
||||||
"""Extract the UFP device id from the registry entry unique id."""
|
"""Extract the MAC address from the registry entry unique id."""
|
||||||
return unique_id.split("_")[0]
|
return unique_id.split("_")[0]
|
||||||
|
|
||||||
|
|
||||||
|
@ -136,10 +137,12 @@ async def set_chime_paired_doorbells(hass: HomeAssistant, call: ServiceCall) ->
|
||||||
chime_button = entity_registry.async_get(entity_id)
|
chime_button = entity_registry.async_get(entity_id)
|
||||||
assert chime_button is not None
|
assert chime_button is not None
|
||||||
assert chime_button.device_id is not None
|
assert chime_button.device_id is not None
|
||||||
chime_ufp_device_id = _async_unique_id_to_ufp_device_id(chime_button.unique_id)
|
chime_mac = _async_unique_id_to_mac(chime_button.unique_id)
|
||||||
|
|
||||||
instance = _async_get_ufp_instance(hass, chime_button.device_id)
|
instance = _async_get_ufp_instance(hass, chime_button.device_id)
|
||||||
chime = instance.bootstrap.chimes[chime_ufp_device_id]
|
chime = instance.bootstrap.get_device_from_mac(chime_mac)
|
||||||
|
chime = cast(Chime, chime)
|
||||||
|
assert chime is not None
|
||||||
|
|
||||||
call.data = ReadOnlyDict(call.data.get("doorbells") or {})
|
call.data = ReadOnlyDict(call.data.get("doorbells") or {})
|
||||||
doorbell_refs = async_extract_referenced_entity_ids(hass, call)
|
doorbell_refs = async_extract_referenced_entity_ids(hass, call)
|
||||||
|
@ -154,10 +157,9 @@ async def set_chime_paired_doorbells(hass: HomeAssistant, call: ServiceCall) ->
|
||||||
!= BinarySensorDeviceClass.OCCUPANCY
|
!= BinarySensorDeviceClass.OCCUPANCY
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
doorbell_ufp_device_id = _async_unique_id_to_ufp_device_id(
|
doorbell_mac = _async_unique_id_to_mac(doorbell_sensor.unique_id)
|
||||||
doorbell_sensor.unique_id
|
camera = instance.bootstrap.get_device_from_mac(doorbell_mac)
|
||||||
)
|
assert camera is not None
|
||||||
camera = instance.bootstrap.cameras[doorbell_ufp_device_id]
|
|
||||||
doorbell_ids.add(camera.id)
|
doorbell_ids.add(camera.id)
|
||||||
chime.camera_ids = sorted(doorbell_ids)
|
chime.camera_ids = sorted(doorbell_ids)
|
||||||
await chime.save_device()
|
await chime.save_device()
|
||||||
|
|
|
@ -7,12 +7,15 @@ from enum import Enum
|
||||||
import socket
|
import socket
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pyunifiprotect import ProtectApiClient
|
from pyunifiprotect.data import (
|
||||||
from pyunifiprotect.data.base import ProtectAdoptableDeviceModel, ProtectDeviceModel
|
Bootstrap,
|
||||||
|
ProtectAdoptableDeviceModel,
|
||||||
|
ProtectDeviceModel,
|
||||||
|
)
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
from .const import ModelType
|
from .const import DEVICES_THAT_ADOPT, ModelType
|
||||||
|
|
||||||
|
|
||||||
def get_nested_attr(obj: Any, attr: str) -> Any:
|
def get_nested_attr(obj: Any, attr: str) -> Any:
|
||||||
|
@ -59,33 +62,45 @@ async def _async_resolve(hass: HomeAssistant, host: str) -> str | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
def async_get_devices_by_type(
|
def async_get_devices_by_type(
|
||||||
api: ProtectApiClient, device_type: ModelType
|
bootstrap: Bootstrap, device_type: ModelType
|
||||||
) -> dict[str, ProtectDeviceModel]:
|
|
||||||
"""Get devices by type."""
|
|
||||||
devices: dict[str, ProtectDeviceModel] = getattr(
|
|
||||||
api.bootstrap, f"{device_type.value}s"
|
|
||||||
)
|
|
||||||
return devices
|
|
||||||
|
|
||||||
|
|
||||||
def async_get_adoptable_devices_by_type(
|
|
||||||
api: ProtectApiClient, device_type: ModelType
|
|
||||||
) -> dict[str, ProtectAdoptableDeviceModel]:
|
) -> dict[str, ProtectAdoptableDeviceModel]:
|
||||||
"""Get adoptable devices by type."""
|
"""Get devices by type."""
|
||||||
|
|
||||||
devices: dict[str, ProtectAdoptableDeviceModel] = getattr(
|
devices: dict[str, ProtectAdoptableDeviceModel] = getattr(
|
||||||
api.bootstrap, f"{device_type.value}s"
|
bootstrap, f"{device_type.value}s"
|
||||||
)
|
)
|
||||||
return devices
|
return devices
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_device_by_id(
|
||||||
|
bootstrap: Bootstrap,
|
||||||
|
device_id: str,
|
||||||
|
device_type: ModelType | None = None,
|
||||||
|
) -> ProtectAdoptableDeviceModel | None:
|
||||||
|
"""Get devices by type."""
|
||||||
|
|
||||||
|
device_types = DEVICES_THAT_ADOPT
|
||||||
|
if device_type is not None:
|
||||||
|
device_types = {device_type}
|
||||||
|
|
||||||
|
device = None
|
||||||
|
for model in device_types:
|
||||||
|
device = async_get_devices_by_type(bootstrap, model).get(device_id)
|
||||||
|
if device is not None:
|
||||||
|
break
|
||||||
|
return device
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_get_devices(
|
def async_get_devices(
|
||||||
api: ProtectApiClient, model_type: Iterable[ModelType]
|
bootstrap: Bootstrap, model_type: Iterable[ModelType]
|
||||||
) -> Generator[ProtectDeviceModel, None, None]:
|
) -> Generator[ProtectDeviceModel, None, None]:
|
||||||
"""Return all device by type."""
|
"""Return all device by type."""
|
||||||
return (
|
return (
|
||||||
device
|
device
|
||||||
for device_type in model_type
|
for device_type in model_type
|
||||||
for device in async_get_devices_by_type(api, device_type).values()
|
for device in async_get_devices_by_type(bootstrap, device_type).values()
|
||||||
)
|
)
|
||||||
|
|
|
@ -23,6 +23,7 @@ from pyunifiprotect.data import (
|
||||||
Viewer,
|
Viewer,
|
||||||
WSSubscriptionMessage,
|
WSSubscriptionMessage,
|
||||||
)
|
)
|
||||||
|
from pyunifiprotect.test_util.anonymize import random_hex
|
||||||
|
|
||||||
from homeassistant.components.unifiprotect.const import DOMAIN
|
from homeassistant.components.unifiprotect.const import DOMAIN
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
|
@ -80,6 +81,26 @@ class MockBootstrap:
|
||||||
"chimes": [c.unifi_dict() for c in self.chimes.values()],
|
"chimes": [c.unifi_dict() for c in self.chimes.values()],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_device_from_mac(self, mac: str) -> ProtectAdoptableDeviceModel | None:
|
||||||
|
"""Return device for MAC address."""
|
||||||
|
|
||||||
|
mac = mac.lower().replace(":", "").replace("-", "").replace("_", "")
|
||||||
|
|
||||||
|
all_devices = (
|
||||||
|
self.cameras.values(),
|
||||||
|
self.lights.values(),
|
||||||
|
self.sensors.values(),
|
||||||
|
self.viewers.values(),
|
||||||
|
self.liveviews.values(),
|
||||||
|
self.doorlocks.values(),
|
||||||
|
self.chimes.values(),
|
||||||
|
)
|
||||||
|
for devices in all_devices:
|
||||||
|
for device in devices:
|
||||||
|
if device.mac.lower() == mac:
|
||||||
|
return device
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MockEntityFixture:
|
class MockEntityFixture:
|
||||||
|
@ -301,7 +322,19 @@ def ids_from_device_description(
|
||||||
description.name.lower().replace(":", "").replace(" ", "_").replace("-", "_")
|
description.name.lower().replace(":", "").replace(" ", "_").replace("-", "_")
|
||||||
)
|
)
|
||||||
|
|
||||||
unique_id = f"{device.id}_{description.key}"
|
unique_id = f"{device.mac}_{description.key}"
|
||||||
entity_id = f"{platform.value}.{entity_name}_{description_entity_name}"
|
entity_id = f"{platform.value}.{entity_name}_{description_entity_name}"
|
||||||
|
|
||||||
return unique_id, entity_id
|
return unique_id, entity_id
|
||||||
|
|
||||||
|
|
||||||
|
def generate_random_ids() -> tuple[str, str]:
|
||||||
|
"""Generate random IDs for device."""
|
||||||
|
|
||||||
|
return random_hex(24).upper(), random_hex(12).upper()
|
||||||
|
|
||||||
|
|
||||||
|
def regenerate_device_ids(device: ProtectAdoptableDeviceModel) -> None:
|
||||||
|
"""Regenerate the IDs on UFP device."""
|
||||||
|
|
||||||
|
device.id, device.mac = generate_random_ids()
|
||||||
|
|
|
@ -366,7 +366,6 @@ async def test_binary_sensor_setup_sensor_none(
|
||||||
|
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
assert state
|
assert state
|
||||||
print(entity_id)
|
|
||||||
assert state.state == expected[index]
|
assert state.state == expected[index]
|
||||||
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ async def test_reboot_button(
|
||||||
|
|
||||||
mock_entry.api.reboot_device = AsyncMock()
|
mock_entry.api.reboot_device = AsyncMock()
|
||||||
|
|
||||||
unique_id = f"{chime.id}_reboot"
|
unique_id = f"{chime.mac}_reboot"
|
||||||
entity_id = "button.test_chime_reboot_device"
|
entity_id = "button.test_chime_reboot_device"
|
||||||
|
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
|
@ -75,7 +75,7 @@ async def test_chime_button(
|
||||||
|
|
||||||
mock_entry.api.play_speaker = AsyncMock()
|
mock_entry.api.play_speaker = AsyncMock()
|
||||||
|
|
||||||
unique_id = f"{chime.id}_play"
|
unique_id = f"{chime.mac}_play"
|
||||||
entity_id = "button.test_chime_play_chime"
|
entity_id = "button.test_chime_play_chime"
|
||||||
|
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
|
|
|
@ -38,6 +38,7 @@ from .conftest import (
|
||||||
MockEntityFixture,
|
MockEntityFixture,
|
||||||
assert_entity_counts,
|
assert_entity_counts,
|
||||||
enable_entity,
|
enable_entity,
|
||||||
|
regenerate_device_ids,
|
||||||
time_changed,
|
time_changed,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -124,7 +125,7 @@ def validate_default_camera_entity(
|
||||||
channel = camera_obj.channels[channel_id]
|
channel = camera_obj.channels[channel_id]
|
||||||
|
|
||||||
entity_name = f"{camera_obj.name} {channel.name}"
|
entity_name = f"{camera_obj.name} {channel.name}"
|
||||||
unique_id = f"{camera_obj.id}_{channel.id}"
|
unique_id = f"{camera_obj.mac}_{channel.id}"
|
||||||
entity_id = f"camera.{entity_name.replace(' ', '_').lower()}"
|
entity_id = f"camera.{entity_name.replace(' ', '_').lower()}"
|
||||||
|
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
|
@ -146,7 +147,7 @@ def validate_rtsps_camera_entity(
|
||||||
channel = camera_obj.channels[channel_id]
|
channel = camera_obj.channels[channel_id]
|
||||||
|
|
||||||
entity_name = f"{camera_obj.name} {channel.name}"
|
entity_name = f"{camera_obj.name} {channel.name}"
|
||||||
unique_id = f"{camera_obj.id}_{channel.id}"
|
unique_id = f"{camera_obj.mac}_{channel.id}"
|
||||||
entity_id = f"camera.{entity_name.replace(' ', '_').lower()}"
|
entity_id = f"camera.{entity_name.replace(' ', '_').lower()}"
|
||||||
|
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
|
@ -168,7 +169,7 @@ def validate_rtsp_camera_entity(
|
||||||
channel = camera_obj.channels[channel_id]
|
channel = camera_obj.channels[channel_id]
|
||||||
|
|
||||||
entity_name = f"{camera_obj.name} {channel.name} Insecure"
|
entity_name = f"{camera_obj.name} {channel.name} Insecure"
|
||||||
unique_id = f"{camera_obj.id}_{channel.id}_insecure"
|
unique_id = f"{camera_obj.mac}_{channel.id}_insecure"
|
||||||
entity_id = f"camera.{entity_name.replace(' ', '_').lower()}"
|
entity_id = f"camera.{entity_name.replace(' ', '_').lower()}"
|
||||||
|
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
|
@ -251,12 +252,12 @@ async def test_basic_setup(
|
||||||
camera_high_only.channels[1]._api = mock_entry.api
|
camera_high_only.channels[1]._api = mock_entry.api
|
||||||
camera_high_only.channels[2]._api = mock_entry.api
|
camera_high_only.channels[2]._api = mock_entry.api
|
||||||
camera_high_only.name = "Test Camera 1"
|
camera_high_only.name = "Test Camera 1"
|
||||||
camera_high_only.id = "test_high"
|
|
||||||
camera_high_only.channels[0].is_rtsp_enabled = True
|
camera_high_only.channels[0].is_rtsp_enabled = True
|
||||||
camera_high_only.channels[0].name = "High"
|
camera_high_only.channels[0].name = "High"
|
||||||
camera_high_only.channels[0].rtsp_alias = "test_high_alias"
|
camera_high_only.channels[0].rtsp_alias = "test_high_alias"
|
||||||
camera_high_only.channels[1].is_rtsp_enabled = False
|
camera_high_only.channels[1].is_rtsp_enabled = False
|
||||||
camera_high_only.channels[2].is_rtsp_enabled = False
|
camera_high_only.channels[2].is_rtsp_enabled = False
|
||||||
|
regenerate_device_ids(camera_high_only)
|
||||||
|
|
||||||
camera_medium_only = mock_camera.copy(deep=True)
|
camera_medium_only = mock_camera.copy(deep=True)
|
||||||
camera_medium_only._api = mock_entry.api
|
camera_medium_only._api = mock_entry.api
|
||||||
|
@ -264,12 +265,12 @@ async def test_basic_setup(
|
||||||
camera_medium_only.channels[1]._api = mock_entry.api
|
camera_medium_only.channels[1]._api = mock_entry.api
|
||||||
camera_medium_only.channels[2]._api = mock_entry.api
|
camera_medium_only.channels[2]._api = mock_entry.api
|
||||||
camera_medium_only.name = "Test Camera 2"
|
camera_medium_only.name = "Test Camera 2"
|
||||||
camera_medium_only.id = "test_medium"
|
|
||||||
camera_medium_only.channels[0].is_rtsp_enabled = False
|
camera_medium_only.channels[0].is_rtsp_enabled = False
|
||||||
camera_medium_only.channels[1].is_rtsp_enabled = True
|
camera_medium_only.channels[1].is_rtsp_enabled = True
|
||||||
camera_medium_only.channels[1].name = "Medium"
|
camera_medium_only.channels[1].name = "Medium"
|
||||||
camera_medium_only.channels[1].rtsp_alias = "test_medium_alias"
|
camera_medium_only.channels[1].rtsp_alias = "test_medium_alias"
|
||||||
camera_medium_only.channels[2].is_rtsp_enabled = False
|
camera_medium_only.channels[2].is_rtsp_enabled = False
|
||||||
|
regenerate_device_ids(camera_medium_only)
|
||||||
|
|
||||||
camera_all_channels = mock_camera.copy(deep=True)
|
camera_all_channels = mock_camera.copy(deep=True)
|
||||||
camera_all_channels._api = mock_entry.api
|
camera_all_channels._api = mock_entry.api
|
||||||
|
@ -277,7 +278,6 @@ async def test_basic_setup(
|
||||||
camera_all_channels.channels[1]._api = mock_entry.api
|
camera_all_channels.channels[1]._api = mock_entry.api
|
||||||
camera_all_channels.channels[2]._api = mock_entry.api
|
camera_all_channels.channels[2]._api = mock_entry.api
|
||||||
camera_all_channels.name = "Test Camera 3"
|
camera_all_channels.name = "Test Camera 3"
|
||||||
camera_all_channels.id = "test_all"
|
|
||||||
camera_all_channels.channels[0].is_rtsp_enabled = True
|
camera_all_channels.channels[0].is_rtsp_enabled = True
|
||||||
camera_all_channels.channels[0].name = "High"
|
camera_all_channels.channels[0].name = "High"
|
||||||
camera_all_channels.channels[0].rtsp_alias = "test_high_alias"
|
camera_all_channels.channels[0].rtsp_alias = "test_high_alias"
|
||||||
|
@ -287,6 +287,7 @@ async def test_basic_setup(
|
||||||
camera_all_channels.channels[2].is_rtsp_enabled = True
|
camera_all_channels.channels[2].is_rtsp_enabled = True
|
||||||
camera_all_channels.channels[2].name = "Low"
|
camera_all_channels.channels[2].name = "Low"
|
||||||
camera_all_channels.channels[2].rtsp_alias = "test_low_alias"
|
camera_all_channels.channels[2].rtsp_alias = "test_low_alias"
|
||||||
|
regenerate_device_ids(camera_all_channels)
|
||||||
|
|
||||||
camera_no_channels = mock_camera.copy(deep=True)
|
camera_no_channels = mock_camera.copy(deep=True)
|
||||||
camera_no_channels._api = mock_entry.api
|
camera_no_channels._api = mock_entry.api
|
||||||
|
@ -294,11 +295,11 @@ async def test_basic_setup(
|
||||||
camera_no_channels.channels[1]._api = mock_entry.api
|
camera_no_channels.channels[1]._api = mock_entry.api
|
||||||
camera_no_channels.channels[2]._api = mock_entry.api
|
camera_no_channels.channels[2]._api = mock_entry.api
|
||||||
camera_no_channels.name = "Test Camera 4"
|
camera_no_channels.name = "Test Camera 4"
|
||||||
camera_no_channels.id = "test_none"
|
|
||||||
camera_no_channels.channels[0].is_rtsp_enabled = False
|
camera_no_channels.channels[0].is_rtsp_enabled = False
|
||||||
camera_no_channels.channels[0].name = "High"
|
camera_no_channels.channels[0].name = "High"
|
||||||
camera_no_channels.channels[1].is_rtsp_enabled = False
|
camera_no_channels.channels[1].is_rtsp_enabled = False
|
||||||
camera_no_channels.channels[2].is_rtsp_enabled = False
|
camera_no_channels.channels[2].is_rtsp_enabled = False
|
||||||
|
regenerate_device_ids(camera_no_channels)
|
||||||
|
|
||||||
camera_package = mock_camera.copy(deep=True)
|
camera_package = mock_camera.copy(deep=True)
|
||||||
camera_package._api = mock_entry.api
|
camera_package._api = mock_entry.api
|
||||||
|
@ -306,12 +307,12 @@ async def test_basic_setup(
|
||||||
camera_package.channels[1]._api = mock_entry.api
|
camera_package.channels[1]._api = mock_entry.api
|
||||||
camera_package.channels[2]._api = mock_entry.api
|
camera_package.channels[2]._api = mock_entry.api
|
||||||
camera_package.name = "Test Camera 5"
|
camera_package.name = "Test Camera 5"
|
||||||
camera_package.id = "test_package"
|
|
||||||
camera_package.channels[0].is_rtsp_enabled = True
|
camera_package.channels[0].is_rtsp_enabled = True
|
||||||
camera_package.channels[0].name = "High"
|
camera_package.channels[0].name = "High"
|
||||||
camera_package.channels[0].rtsp_alias = "test_high_alias"
|
camera_package.channels[0].rtsp_alias = "test_high_alias"
|
||||||
camera_package.channels[1].is_rtsp_enabled = False
|
camera_package.channels[1].is_rtsp_enabled = False
|
||||||
camera_package.channels[2].is_rtsp_enabled = False
|
camera_package.channels[2].is_rtsp_enabled = False
|
||||||
|
regenerate_device_ids(camera_package)
|
||||||
package_channel = camera_package.channels[0].copy(deep=True)
|
package_channel = camera_package.channels[0].copy(deep=True)
|
||||||
package_channel.is_rtsp_enabled = False
|
package_channel.is_rtsp_enabled = False
|
||||||
package_channel.name = "Package Camera"
|
package_channel.name = "Package Camera"
|
||||||
|
|
|
@ -4,7 +4,7 @@ from pyunifiprotect.data import NVR, Light
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from .conftest import MockEntityFixture
|
from .conftest import MockEntityFixture, regenerate_device_ids
|
||||||
|
|
||||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ async def test_diagnostics(
|
||||||
light1 = mock_light.copy()
|
light1 = mock_light.copy()
|
||||||
light1._api = mock_entry.api
|
light1._api = mock_entry.api
|
||||||
light1.name = "Test Light 1"
|
light1.name = "Test Light 1"
|
||||||
light1.id = "lightid1"
|
regenerate_device_ids(light1)
|
||||||
|
|
||||||
mock_entry.api.bootstrap.lights = {
|
mock_entry.api.bootstrap.lights = {
|
||||||
light1.id: light1,
|
light1.id: light1,
|
||||||
|
|
|
@ -16,7 +16,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from . import _patch_discovery
|
from . import _patch_discovery
|
||||||
from .conftest import MockBootstrap, MockEntityFixture
|
from .conftest import MockBootstrap, MockEntityFixture, regenerate_device_ids
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
@ -212,8 +212,7 @@ async def test_device_remove_devices(
|
||||||
light1 = mock_light.copy()
|
light1 = mock_light.copy()
|
||||||
light1._api = mock_entry.api
|
light1._api = mock_entry.api
|
||||||
light1.name = "Test Light 1"
|
light1.name = "Test Light 1"
|
||||||
light1.id = "lightid1"
|
regenerate_device_ids(light1)
|
||||||
light1.mac = "AABBCCDDEEFF"
|
|
||||||
|
|
||||||
mock_entry.api.bootstrap.lights = {
|
mock_entry.api.bootstrap.lights = {
|
||||||
light1.id: light1,
|
light1.id: light1,
|
||||||
|
|
|
@ -57,7 +57,7 @@ async def test_light_setup(
|
||||||
):
|
):
|
||||||
"""Test light entity setup."""
|
"""Test light entity setup."""
|
||||||
|
|
||||||
unique_id = light[0].id
|
unique_id = light[0].mac
|
||||||
entity_id = light[1]
|
entity_id = light[1]
|
||||||
|
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
|
|
|
@ -60,7 +60,7 @@ async def test_lock_setup(
|
||||||
):
|
):
|
||||||
"""Test lock entity setup."""
|
"""Test lock entity setup."""
|
||||||
|
|
||||||
unique_id = f"{doorlock[0].id}_lock"
|
unique_id = f"{doorlock[0].mac}_lock"
|
||||||
entity_id = doorlock[1]
|
entity_id = doorlock[1]
|
||||||
|
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
|
|
|
@ -66,7 +66,7 @@ async def test_media_player_setup(
|
||||||
):
|
):
|
||||||
"""Test media_player entity setup."""
|
"""Test media_player entity setup."""
|
||||||
|
|
||||||
unique_id = f"{camera[0].id}_speaker"
|
unique_id = f"{camera[0].mac}_speaker"
|
||||||
entity_id = camera[1]
|
entity_id = camera[1]
|
||||||
|
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
|
|
|
@ -12,7 +12,7 @@ from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
from .conftest import MockEntityFixture
|
from .conftest import MockEntityFixture, generate_random_ids, regenerate_device_ids
|
||||||
|
|
||||||
|
|
||||||
async def test_migrate_reboot_button(
|
async def test_migrate_reboot_button(
|
||||||
|
@ -23,12 +23,13 @@ async def test_migrate_reboot_button(
|
||||||
light1 = mock_light.copy()
|
light1 = mock_light.copy()
|
||||||
light1._api = mock_entry.api
|
light1._api = mock_entry.api
|
||||||
light1.name = "Test Light 1"
|
light1.name = "Test Light 1"
|
||||||
light1.id = "lightid1"
|
regenerate_device_ids(light1)
|
||||||
|
|
||||||
light2 = mock_light.copy()
|
light2 = mock_light.copy()
|
||||||
light2._api = mock_entry.api
|
light2._api = mock_entry.api
|
||||||
light2.name = "Test Light 2"
|
light2.name = "Test Light 2"
|
||||||
light2.id = "lightid2"
|
regenerate_device_ids(light2)
|
||||||
|
|
||||||
mock_entry.api.bootstrap.lights = {
|
mock_entry.api.bootstrap.lights = {
|
||||||
light1.id: light1,
|
light1.id: light1,
|
||||||
light2.id: light2,
|
light2.id: light2,
|
||||||
|
@ -42,7 +43,7 @@ async def test_migrate_reboot_button(
|
||||||
registry.async_get_or_create(
|
registry.async_get_or_create(
|
||||||
Platform.BUTTON,
|
Platform.BUTTON,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
f"{light2.id}_reboot",
|
f"{light2.mac}_reboot",
|
||||||
config_entry=mock_entry.entry,
|
config_entry=mock_entry.entry,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -59,20 +60,21 @@ async def test_migrate_reboot_button(
|
||||||
):
|
):
|
||||||
if entity.domain == Platform.BUTTON.value:
|
if entity.domain == Platform.BUTTON.value:
|
||||||
buttons.append(entity)
|
buttons.append(entity)
|
||||||
print(entity.entity_id)
|
|
||||||
assert len(buttons) == 2
|
assert len(buttons) == 2
|
||||||
|
|
||||||
assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device") is None
|
assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device") is None
|
||||||
assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device_2") is None
|
assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device_2") is None
|
||||||
light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_lightid1")
|
light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_{light1.id.lower()}")
|
||||||
assert light is not None
|
assert light is not None
|
||||||
assert light.unique_id == f"{light1.id}_reboot"
|
assert light.unique_id == f"{light1.mac}_reboot"
|
||||||
|
|
||||||
assert registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device") is None
|
assert registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device") is None
|
||||||
assert registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device_2") is None
|
assert registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device_2") is None
|
||||||
light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_lightid2_reboot")
|
light = registry.async_get(
|
||||||
|
f"{Platform.BUTTON}.unifiprotect_{light2.mac.lower()}_reboot"
|
||||||
|
)
|
||||||
assert light is not None
|
assert light is not None
|
||||||
assert light.unique_id == f"{light2.id}_reboot"
|
assert light.unique_id == f"{light2.mac}_reboot"
|
||||||
|
|
||||||
|
|
||||||
async def test_migrate_reboot_button_no_device(
|
async def test_migrate_reboot_button_no_device(
|
||||||
|
@ -83,7 +85,9 @@ async def test_migrate_reboot_button_no_device(
|
||||||
light1 = mock_light.copy()
|
light1 = mock_light.copy()
|
||||||
light1._api = mock_entry.api
|
light1._api = mock_entry.api
|
||||||
light1.name = "Test Light 1"
|
light1.name = "Test Light 1"
|
||||||
light1.id = "lightid1"
|
regenerate_device_ids(light1)
|
||||||
|
|
||||||
|
light2_id, _ = generate_random_ids()
|
||||||
|
|
||||||
mock_entry.api.bootstrap.lights = {
|
mock_entry.api.bootstrap.lights = {
|
||||||
light1.id: light1,
|
light1.id: light1,
|
||||||
|
@ -92,7 +96,7 @@ async def test_migrate_reboot_button_no_device(
|
||||||
|
|
||||||
registry = er.async_get(hass)
|
registry = er.async_get(hass)
|
||||||
registry.async_get_or_create(
|
registry.async_get_or_create(
|
||||||
Platform.BUTTON, DOMAIN, "lightid2", config_entry=mock_entry.entry
|
Platform.BUTTON, DOMAIN, light2_id, config_entry=mock_entry.entry
|
||||||
)
|
)
|
||||||
|
|
||||||
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||||
|
@ -110,9 +114,9 @@ async def test_migrate_reboot_button_no_device(
|
||||||
buttons.append(entity)
|
buttons.append(entity)
|
||||||
assert len(buttons) == 2
|
assert len(buttons) == 2
|
||||||
|
|
||||||
light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_lightid2")
|
light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_{light2_id.lower()}")
|
||||||
assert light is not None
|
assert light is not None
|
||||||
assert light.unique_id == "lightid2"
|
assert light.unique_id == light2_id
|
||||||
|
|
||||||
|
|
||||||
async def test_migrate_reboot_button_fail(
|
async def test_migrate_reboot_button_fail(
|
||||||
|
@ -123,7 +127,7 @@ async def test_migrate_reboot_button_fail(
|
||||||
light1 = mock_light.copy()
|
light1 = mock_light.copy()
|
||||||
light1._api = mock_entry.api
|
light1._api = mock_entry.api
|
||||||
light1.name = "Test Light 1"
|
light1.name = "Test Light 1"
|
||||||
light1.id = "lightid1"
|
regenerate_device_ids(light1)
|
||||||
|
|
||||||
mock_entry.api.bootstrap.lights = {
|
mock_entry.api.bootstrap.lights = {
|
||||||
light1.id: light1,
|
light1.id: light1,
|
||||||
|
@ -155,4 +159,47 @@ async def test_migrate_reboot_button_fail(
|
||||||
|
|
||||||
light = registry.async_get(f"{Platform.BUTTON}.test_light_1")
|
light = registry.async_get(f"{Platform.BUTTON}.test_light_1")
|
||||||
assert light is not None
|
assert light is not None
|
||||||
assert light.unique_id == f"{light1.id}"
|
assert light.unique_id == f"{light1.mac}"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_migrate_device_mac_button_fail(
|
||||||
|
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light
|
||||||
|
):
|
||||||
|
"""Test migrating unique ID to MAC format."""
|
||||||
|
|
||||||
|
light1 = mock_light.copy()
|
||||||
|
light1._api = mock_entry.api
|
||||||
|
light1.name = "Test Light 1"
|
||||||
|
regenerate_device_ids(light1)
|
||||||
|
|
||||||
|
mock_entry.api.bootstrap.lights = {
|
||||||
|
light1.id: light1,
|
||||||
|
}
|
||||||
|
mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap)
|
||||||
|
|
||||||
|
registry = er.async_get(hass)
|
||||||
|
registry.async_get_or_create(
|
||||||
|
Platform.BUTTON,
|
||||||
|
DOMAIN,
|
||||||
|
f"{light1.id}_reboot",
|
||||||
|
config_entry=mock_entry.entry,
|
||||||
|
suggested_object_id=light1.name,
|
||||||
|
)
|
||||||
|
registry.async_get_or_create(
|
||||||
|
Platform.BUTTON,
|
||||||
|
DOMAIN,
|
||||||
|
f"{light1.mac}_reboot",
|
||||||
|
config_entry=mock_entry.entry,
|
||||||
|
suggested_object_id=light1.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert mock_entry.entry.state == ConfigEntryState.LOADED
|
||||||
|
assert mock_entry.api.update.called
|
||||||
|
assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac
|
||||||
|
|
||||||
|
light = registry.async_get(f"{Platform.BUTTON}.test_light_1")
|
||||||
|
assert light is not None
|
||||||
|
assert light.unique_id == f"{light1.id}_reboot"
|
||||||
|
|
|
@ -21,7 +21,7 @@ from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
|
|
||||||
from .conftest import MockEntityFixture
|
from .conftest import MockEntityFixture, regenerate_device_ids
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="device")
|
@pytest.fixture(name="device")
|
||||||
|
@ -165,22 +165,22 @@ async def test_set_chime_paired_doorbells(
|
||||||
}
|
}
|
||||||
|
|
||||||
camera1 = mock_camera.copy()
|
camera1 = mock_camera.copy()
|
||||||
camera1.id = "cameraid1"
|
|
||||||
camera1.name = "Test Camera 1"
|
camera1.name = "Test Camera 1"
|
||||||
camera1._api = mock_entry.api
|
camera1._api = mock_entry.api
|
||||||
camera1.channels[0]._api = mock_entry.api
|
camera1.channels[0]._api = mock_entry.api
|
||||||
camera1.channels[1]._api = mock_entry.api
|
camera1.channels[1]._api = mock_entry.api
|
||||||
camera1.channels[2]._api = mock_entry.api
|
camera1.channels[2]._api = mock_entry.api
|
||||||
camera1.feature_flags.has_chime = True
|
camera1.feature_flags.has_chime = True
|
||||||
|
regenerate_device_ids(camera1)
|
||||||
|
|
||||||
camera2 = mock_camera.copy()
|
camera2 = mock_camera.copy()
|
||||||
camera2.id = "cameraid2"
|
|
||||||
camera2.name = "Test Camera 2"
|
camera2.name = "Test Camera 2"
|
||||||
camera2._api = mock_entry.api
|
camera2._api = mock_entry.api
|
||||||
camera2.channels[0]._api = mock_entry.api
|
camera2.channels[0]._api = mock_entry.api
|
||||||
camera2.channels[1]._api = mock_entry.api
|
camera2.channels[1]._api = mock_entry.api
|
||||||
camera2.channels[2]._api = mock_entry.api
|
camera2.channels[2]._api = mock_entry.api
|
||||||
camera2.feature_flags.has_chime = True
|
camera2.feature_flags.has_chime = True
|
||||||
|
regenerate_device_ids(camera2)
|
||||||
|
|
||||||
mock_entry.api.bootstrap.cameras = {
|
mock_entry.api.bootstrap.cameras = {
|
||||||
camera1.id: camera1,
|
camera1.id: camera1,
|
||||||
|
@ -210,5 +210,5 @@ async def test_set_chime_paired_doorbells(
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_entry.api.update_device.assert_called_once_with(
|
mock_entry.api.update_device.assert_called_once_with(
|
||||||
ModelType.CHIME, mock_chime.id, {"cameraIds": [camera1.id, camera2.id]}
|
ModelType.CHIME, mock_chime.id, {"cameraIds": sorted([camera1.id, camera2.id])}
|
||||||
)
|
)
|
||||||
|
|
|
@ -238,7 +238,7 @@ async def test_switch_setup_light(
|
||||||
|
|
||||||
description = LIGHT_SWITCHES[0]
|
description = LIGHT_SWITCHES[0]
|
||||||
|
|
||||||
unique_id = f"{light.id}_{description.key}"
|
unique_id = f"{light.mac}_{description.key}"
|
||||||
entity_id = f"switch.test_light_{description.name.lower().replace(' ', '_')}"
|
entity_id = f"switch.test_light_{description.name.lower().replace(' ', '_')}"
|
||||||
|
|
||||||
entity = entity_registry.async_get(entity_id)
|
entity = entity_registry.async_get(entity_id)
|
||||||
|
@ -282,7 +282,7 @@ async def test_switch_setup_camera_all(
|
||||||
description_entity_name = (
|
description_entity_name = (
|
||||||
description.name.lower().replace(":", "").replace(" ", "_")
|
description.name.lower().replace(":", "").replace(" ", "_")
|
||||||
)
|
)
|
||||||
unique_id = f"{camera.id}_{description.key}"
|
unique_id = f"{camera.mac}_{description.key}"
|
||||||
entity_id = f"switch.test_camera_{description_entity_name}"
|
entity_id = f"switch.test_camera_{description_entity_name}"
|
||||||
|
|
||||||
entity = entity_registry.async_get(entity_id)
|
entity = entity_registry.async_get(entity_id)
|
||||||
|
@ -329,7 +329,7 @@ async def test_switch_setup_camera_none(
|
||||||
description_entity_name = (
|
description_entity_name = (
|
||||||
description.name.lower().replace(":", "").replace(" ", "_")
|
description.name.lower().replace(":", "").replace(" ", "_")
|
||||||
)
|
)
|
||||||
unique_id = f"{camera_none.id}_{description.key}"
|
unique_id = f"{camera_none.mac}_{description.key}"
|
||||||
entity_id = f"switch.test_camera_{description_entity_name}"
|
entity_id = f"switch.test_camera_{description_entity_name}"
|
||||||
|
|
||||||
entity = entity_registry.async_get(entity_id)
|
entity = entity_registry.async_get(entity_id)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue