UniFi Protect cleanup and enable unadopted devices (#73860)
This commit is contained in:
parent
5c5fd746fd
commit
01a9367281
24 changed files with 258 additions and 114 deletions
|
@ -61,6 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
subscribed_models=DEVICES_FOR_SUBSCRIBE,
|
||||
override_connection_host=entry.options.get(CONF_OVERRIDE_CHOST, False),
|
||||
ignore_stats=not entry.options.get(CONF_ALL_UPDATES, False),
|
||||
ignore_unadopted=False,
|
||||
)
|
||||
_LOGGER.debug("Connect to UniFi Protect")
|
||||
data_service = ProtectData(hass, protect, SCAN_INTERVAL, entry)
|
||||
|
@ -127,7 +128,9 @@ async def async_remove_config_entry_device(
|
|||
}
|
||||
api = async_ufp_instance_for_config_entry_ids(hass, {config_entry.entry_id})
|
||||
assert api is not None
|
||||
return api.bootstrap.nvr.mac not in unifi_macs and not any(
|
||||
device.mac in unifi_macs
|
||||
for device in async_get_devices(api.bootstrap, DEVICES_THAT_ADOPT)
|
||||
)
|
||||
if api.bootstrap.nvr.mac in unifi_macs:
|
||||
return False
|
||||
for device in async_get_devices(api.bootstrap, DEVICES_THAT_ADOPT):
|
||||
if device.is_adopted_by_us and device.mac in unifi_macs:
|
||||
return False
|
||||
return True
|
||||
|
|
|
@ -35,7 +35,6 @@ from .entity import (
|
|||
async_all_device_entities,
|
||||
)
|
||||
from .models import PermRequired, ProtectRequiredKeysMixin
|
||||
from .utils import async_get_is_highfps
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_KEY_DOOR = "door"
|
||||
|
@ -103,7 +102,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
|||
icon="mdi:video-high-definition",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
ufp_required_field="feature_flags.has_highfps",
|
||||
ufp_value_fn=async_get_is_highfps,
|
||||
ufp_value="is_high_fps_enabled",
|
||||
ufp_perm=PermRequired.NO_WRITE,
|
||||
),
|
||||
ProtectBinaryEntityDescription(
|
||||
|
@ -386,12 +385,15 @@ def _async_motion_entities(
|
|||
) -> list[ProtectDeviceEntity]:
|
||||
entities: list[ProtectDeviceEntity] = []
|
||||
for device in data.api.bootstrap.cameras.values():
|
||||
if not device.is_adopted_by_us:
|
||||
continue
|
||||
|
||||
for description in MOTION_SENSORS:
|
||||
entities.append(ProtectEventBinarySensor(data, device, description))
|
||||
_LOGGER.debug(
|
||||
"Adding binary sensor entity %s for %s",
|
||||
description.name,
|
||||
device.name,
|
||||
device.display_name,
|
||||
)
|
||||
|
||||
return entities
|
||||
|
@ -468,9 +470,9 @@ class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity):
|
|||
slot = self._disk.slot
|
||||
self._attr_available = False
|
||||
|
||||
if self.device.system_info.ustorage is None:
|
||||
return
|
||||
|
||||
# should not be possible since it would require user to
|
||||
# _downgrade_ to make ustorage disppear
|
||||
assert self.device.system_info.ustorage is not None
|
||||
for disk in self.device.system_info.ustorage.disks:
|
||||
if disk.slot == slot:
|
||||
self._disk = disk
|
||||
|
|
|
@ -103,7 +103,7 @@ class ProtectButton(ProtectDeviceEntity, ButtonEntity):
|
|||
) -> None:
|
||||
"""Initialize an UniFi camera."""
|
||||
super().__init__(data, device, description)
|
||||
self._attr_name = f"{self.device.name} {self.entity_description.name}"
|
||||
self._attr_name = f"{self.device.display_name} {self.entity_description.name}"
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
|
|
|
@ -36,9 +36,14 @@ def get_camera_channels(
|
|||
) -> Generator[tuple[UFPCamera, CameraChannel, bool], None, None]:
|
||||
"""Get all the camera channels."""
|
||||
for camera in protect.bootstrap.cameras.values():
|
||||
if not camera.is_adopted_by_us:
|
||||
continue
|
||||
|
||||
if not camera.channels:
|
||||
_LOGGER.warning(
|
||||
"Camera does not have any channels: %s (id: %s)", camera.name, camera.id
|
||||
"Camera does not have any channels: %s (id: %s)",
|
||||
camera.display_name,
|
||||
camera.id,
|
||||
)
|
||||
continue
|
||||
|
||||
|
@ -116,10 +121,10 @@ class ProtectCamera(ProtectDeviceEntity, Camera):
|
|||
|
||||
if self._secure:
|
||||
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.display_name} {self.channel.name}"
|
||||
else:
|
||||
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.display_name} {self.channel.name} Insecure"
|
||||
# only the default (first) channel is enabled by default
|
||||
self._attr_entity_registry_enabled_default = is_default and secure
|
||||
|
||||
|
|
|
@ -175,9 +175,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
user_input[CONF_VERIFY_SSL] = False
|
||||
nvr_data, errors = await self._async_get_nvr_data(user_input)
|
||||
if nvr_data and not errors:
|
||||
return self._async_create_entry(
|
||||
nvr_data.name or nvr_data.type, user_input
|
||||
)
|
||||
return self._async_create_entry(nvr_data.display_name, user_input)
|
||||
|
||||
placeholders = {
|
||||
"name": discovery_info["hostname"]
|
||||
|
@ -323,9 +321,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
await self.async_set_unique_id(nvr_data.mac)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self._async_create_entry(
|
||||
nvr_data.name or nvr_data.type, user_input
|
||||
)
|
||||
return self._async_create_entry(nvr_data.display_name, user_input)
|
||||
|
||||
user_input = user_input or {}
|
||||
return self.async_show_form(
|
||||
|
|
|
@ -120,7 +120,9 @@ class ProtectData:
|
|||
@callback
|
||||
def _async_process_ws_message(self, message: WSSubscriptionMessage) -> None:
|
||||
# removed packets are not processed yet
|
||||
if message.new_obj is None: # pragma: no cover
|
||||
if message.new_obj is None or not getattr(
|
||||
message.new_obj, "is_adopted_by_us", True
|
||||
):
|
||||
return
|
||||
|
||||
if message.new_obj.model in DEVICES_WITH_ENTITIES:
|
||||
|
|
|
@ -44,6 +44,9 @@ def _async_device_entities(
|
|||
|
||||
entities: list[ProtectDeviceEntity] = []
|
||||
for device in data.get_by_types({model_type}):
|
||||
if not device.is_adopted_by_us:
|
||||
continue
|
||||
|
||||
assert isinstance(device, (Camera, Light, Sensor, Viewer, Doorlock, Chime))
|
||||
for description in descs:
|
||||
if description.ufp_perm is not None:
|
||||
|
@ -69,7 +72,7 @@ def _async_device_entities(
|
|||
"Adding %s entity %s for %s",
|
||||
klass.__name__,
|
||||
description.name,
|
||||
device.name,
|
||||
device.display_name,
|
||||
)
|
||||
|
||||
return entities
|
||||
|
@ -126,12 +129,12 @@ class ProtectDeviceEntity(Entity):
|
|||
|
||||
if description is None:
|
||||
self._attr_unique_id = f"{self.device.mac}"
|
||||
self._attr_name = f"{self.device.name}"
|
||||
self._attr_name = f"{self.device.display_name}"
|
||||
else:
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{self.device.mac}_{description.key}"
|
||||
name = description.name or ""
|
||||
self._attr_name = f"{self.device.name} {name.title()}"
|
||||
self._attr_name = f"{self.device.display_name} {name.title()}"
|
||||
|
||||
self._attr_attribution = DEFAULT_ATTRIBUTION
|
||||
self._async_set_device_info()
|
||||
|
@ -147,7 +150,7 @@ class ProtectDeviceEntity(Entity):
|
|||
@callback
|
||||
def _async_set_device_info(self) -> None:
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=self.device.name,
|
||||
name=self.device.display_name,
|
||||
manufacturer=DEFAULT_BRAND,
|
||||
model=self.device.type,
|
||||
via_device=(DOMAIN, self.data.api.bootstrap.nvr.mac),
|
||||
|
@ -214,7 +217,7 @@ class ProtectNVREntity(ProtectDeviceEntity):
|
|||
connections={(dr.CONNECTION_NETWORK_MAC, self.device.mac)},
|
||||
identifiers={(DOMAIN, self.device.mac)},
|
||||
manufacturer=DEFAULT_BRAND,
|
||||
name=self.device.name,
|
||||
name=self.device.display_name,
|
||||
model=self.device.type,
|
||||
sw_version=str(self.device.version),
|
||||
configuration_url=self.device.api.base_url,
|
||||
|
|
|
@ -27,12 +27,12 @@ async def async_setup_entry(
|
|||
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
|
||||
entities = []
|
||||
for device in data.api.bootstrap.lights.values():
|
||||
if not device.is_adopted_by_us:
|
||||
continue
|
||||
|
||||
if device.can_write(data.api.bootstrap.auth_user):
|
||||
entities.append(ProtectLight(data, device))
|
||||
|
||||
if not entities:
|
||||
return
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
|
|
|
@ -26,13 +26,14 @@ async def async_setup_entry(
|
|||
"""Set up locks on a UniFi Protect NVR."""
|
||||
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
ProtectLock(
|
||||
data,
|
||||
lock,
|
||||
)
|
||||
for lock in data.api.bootstrap.doorlocks.values()
|
||||
)
|
||||
entities = []
|
||||
for device in data.api.bootstrap.doorlocks.values():
|
||||
if not device.is_adopted_by_us:
|
||||
continue
|
||||
|
||||
entities.append(ProtectLock(data, device))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class ProtectLock(ProtectDeviceEntity, LockEntity):
|
||||
|
@ -53,7 +54,7 @@ class ProtectLock(ProtectDeviceEntity, LockEntity):
|
|||
LockEntityDescription(key="lock"),
|
||||
)
|
||||
|
||||
self._attr_name = f"{self.device.name} Lock"
|
||||
self._attr_name = f"{self.device.display_name} Lock"
|
||||
|
||||
@callback
|
||||
def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None:
|
||||
|
@ -82,10 +83,10 @@ class ProtectLock(ProtectDeviceEntity, LockEntity):
|
|||
|
||||
async def async_unlock(self, **kwargs: Any) -> None:
|
||||
"""Unlock the lock."""
|
||||
_LOGGER.debug("Unlocking %s", self.device.name)
|
||||
_LOGGER.debug("Unlocking %s", self.device.display_name)
|
||||
return await self.device.open_lock()
|
||||
|
||||
async def async_lock(self, **kwargs: Any) -> None:
|
||||
"""Lock the lock."""
|
||||
_LOGGER.debug("Locking %s", self.device.name)
|
||||
_LOGGER.debug("Locking %s", self.device.display_name)
|
||||
return await self.device.close_lock()
|
||||
|
|
|
@ -40,16 +40,14 @@ async def async_setup_entry(
|
|||
"""Discover cameras with speakers on a UniFi Protect NVR."""
|
||||
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
ProtectMediaPlayer(
|
||||
data,
|
||||
camera,
|
||||
)
|
||||
for camera in data.api.bootstrap.cameras.values()
|
||||
if camera.feature_flags.has_speaker
|
||||
]
|
||||
)
|
||||
entities = []
|
||||
for device in data.api.bootstrap.cameras.values():
|
||||
if not device.is_adopted_by_us or not device.feature_flags.has_speaker:
|
||||
continue
|
||||
|
||||
entities.append(ProtectMediaPlayer(data, device))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity):
|
||||
|
@ -79,7 +77,7 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity):
|
|||
),
|
||||
)
|
||||
|
||||
self._attr_name = f"{self.device.name} Speaker"
|
||||
self._attr_name = f"{self.device.display_name} Speaker"
|
||||
self._attr_media_content_type = MEDIA_TYPE_MUSIC
|
||||
|
||||
@callback
|
||||
|
@ -108,7 +106,7 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity):
|
|||
self.device.talkback_stream is not None
|
||||
and self.device.talkback_stream.is_running
|
||||
):
|
||||
_LOGGER.debug("Stopping playback for %s Speaker", self.device.name)
|
||||
_LOGGER.debug("Stopping playback for %s Speaker", self.device.display_name)
|
||||
await self.device.stop_audio()
|
||||
self._async_updated_event(self.device)
|
||||
|
||||
|
@ -126,7 +124,9 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity):
|
|||
if media_type != MEDIA_TYPE_MUSIC:
|
||||
raise HomeAssistantError("Only music media type is supported")
|
||||
|
||||
_LOGGER.debug("Playing Media %s for %s Speaker", media_id, self.device.name)
|
||||
_LOGGER.debug(
|
||||
"Playing Media %s for %s Speaker", media_id, self.device.display_name
|
||||
)
|
||||
await self.async_media_stop()
|
||||
try:
|
||||
await self.device.play_audio(media_id, blocking=False)
|
||||
|
|
|
@ -14,8 +14,6 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .utils import async_device_by_id
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -69,7 +67,7 @@ async def async_migrate_buttons(
|
|||
bootstrap = await async_get_bootstrap(protect)
|
||||
count = 0
|
||||
for button in to_migrate:
|
||||
device = async_device_by_id(bootstrap, button.unique_id)
|
||||
device = bootstrap.get_device_from_id(button.unique_id)
|
||||
if device is None:
|
||||
continue
|
||||
|
||||
|
@ -130,7 +128,7 @@ async def async_migrate_device_ids(
|
|||
if parts[0] == bootstrap.nvr.id:
|
||||
device: NVR | ProtectAdoptableDeviceModel | None = bootstrap.nvr
|
||||
else:
|
||||
device = async_device_by_id(bootstrap, parts[0])
|
||||
device = bootstrap.get_device_from_id(parts[0])
|
||||
|
||||
if device is None:
|
||||
continue
|
||||
|
|
|
@ -5,9 +5,9 @@ from collections.abc import Callable, Coroutine
|
|||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import logging
|
||||
from typing import Any, Generic, TypeVar
|
||||
from typing import Any, Generic, TypeVar, Union
|
||||
|
||||
from pyunifiprotect.data import ProtectDeviceModel
|
||||
from pyunifiprotect.data import NVR, ProtectAdoptableDeviceModel
|
||||
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
|
||||
|
@ -15,7 +15,7 @@ from .utils import get_nested_attr
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
T = TypeVar("T", bound=ProtectDeviceModel)
|
||||
T = TypeVar("T", bound=Union[ProtectAdoptableDeviceModel, NVR])
|
||||
|
||||
|
||||
class PermRequired(int, Enum):
|
||||
|
@ -63,7 +63,7 @@ class ProtectSetableKeysMixin(ProtectRequiredKeysMixin[T]):
|
|||
|
||||
async def ufp_set(self, obj: T, value: Any) -> None:
|
||||
"""Set value for UniFi Protect device."""
|
||||
_LOGGER.debug("Setting %s to %s for %s", self.name, value, obj.name)
|
||||
_LOGGER.debug("Setting %s to %s for %s", self.name, value, obj.display_name)
|
||||
if self.ufp_set_method is not None:
|
||||
await getattr(obj, self.ufp_set_method)(value)
|
||||
elif self.ufp_set_method_fn is not None:
|
||||
|
|
|
@ -143,7 +143,7 @@ def _get_doorbell_options(api: ProtectApiClient) -> list[dict[str, Any]]:
|
|||
def _get_paired_camera_options(api: ProtectApiClient) -> list[dict[str, Any]]:
|
||||
options = [{"id": TYPE_EMPTY_VALUE, "name": "Not Paired"}]
|
||||
for camera in api.bootstrap.cameras.values():
|
||||
options.append({"id": camera.id, "name": camera.name or camera.type})
|
||||
options.append({"id": camera.id, "name": camera.display_name or camera.type})
|
||||
|
||||
return options
|
||||
|
||||
|
@ -353,7 +353,7 @@ class ProtectSelects(ProtectDeviceEntity, SelectEntity):
|
|||
) -> None:
|
||||
"""Initialize the unifi protect select entity."""
|
||||
super().__init__(data, device, description)
|
||||
self._attr_name = f"{self.device.name} {self.entity_description.name}"
|
||||
self._attr_name = f"{self.device.display_name} {self.entity_description.name}"
|
||||
self._async_set_options()
|
||||
|
||||
@callback
|
||||
|
@ -421,7 +421,10 @@ class ProtectSelects(ProtectDeviceEntity, SelectEntity):
|
|||
timeout_msg = f" with timeout of {duration} minute(s)"
|
||||
|
||||
_LOGGER.debug(
|
||||
'Setting message for %s to "%s"%s', self.device.name, message, timeout_msg
|
||||
'Setting message for %s to "%s"%s',
|
||||
self.device.display_name,
|
||||
message,
|
||||
timeout_msg,
|
||||
)
|
||||
await self.device.set_lcd_text(
|
||||
DoorbellMessageType.CUSTOM_MESSAGE, message, reset_at=reset_at
|
||||
|
|
|
@ -108,7 +108,7 @@ def _get_alarm_sound(obj: Sensor) -> str:
|
|||
|
||||
|
||||
ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||
ProtectSensorEntityDescription[ProtectDeviceModel](
|
||||
ProtectSensorEntityDescription(
|
||||
key="uptime",
|
||||
name="Uptime",
|
||||
icon="mdi:clock",
|
||||
|
@ -353,7 +353,7 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
|||
name="Paired Camera",
|
||||
icon="mdi:cctv",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
ufp_value="camera.name",
|
||||
ufp_value="camera.display_name",
|
||||
ufp_perm=PermRequired.NO_WRITE,
|
||||
),
|
||||
)
|
||||
|
@ -373,13 +373,13 @@ DOORLOCK_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
|||
name="Paired Camera",
|
||||
icon="mdi:cctv",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
ufp_value="camera.name",
|
||||
ufp_value="camera.display_name",
|
||||
ufp_perm=PermRequired.NO_WRITE,
|
||||
),
|
||||
)
|
||||
|
||||
NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||
ProtectSensorEntityDescription[ProtectDeviceModel](
|
||||
ProtectSensorEntityDescription(
|
||||
key="uptime",
|
||||
name="Uptime",
|
||||
icon="mdi:clock",
|
||||
|
@ -541,7 +541,7 @@ LIGHT_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
|||
name="Paired Camera",
|
||||
icon="mdi:cctv",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
ufp_value="camera.name",
|
||||
ufp_value="camera.display_name",
|
||||
ufp_perm=PermRequired.NO_WRITE,
|
||||
),
|
||||
)
|
||||
|
@ -618,11 +618,14 @@ def _async_motion_entities(
|
|||
entities: list[ProtectDeviceEntity] = []
|
||||
for device in data.api.bootstrap.cameras.values():
|
||||
for description in MOTION_TRIP_SENSORS:
|
||||
if not device.is_adopted_by_us:
|
||||
continue
|
||||
|
||||
entities.append(ProtectDeviceSensor(data, device, description))
|
||||
_LOGGER.debug(
|
||||
"Adding trip sensor entity %s for %s",
|
||||
description.name,
|
||||
device.name,
|
||||
device.display_name,
|
||||
)
|
||||
|
||||
if not device.feature_flags.has_smart_detect:
|
||||
|
@ -633,7 +636,7 @@ def _async_motion_entities(
|
|||
_LOGGER.debug(
|
||||
"Adding sensor entity %s for %s",
|
||||
description.name,
|
||||
device.name,
|
||||
device.display_name,
|
||||
)
|
||||
|
||||
return entities
|
||||
|
|
|
@ -22,7 +22,6 @@ from .const import DOMAIN
|
|||
from .data import ProtectData
|
||||
from .entity import ProtectDeviceEntity, async_all_device_entities
|
||||
from .models import PermRequired, ProtectSetableKeysMixin, T
|
||||
from .utils import async_get_is_highfps
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -81,7 +80,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
|||
icon="mdi:video-high-definition",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
ufp_required_field="feature_flags.has_highfps",
|
||||
ufp_value_fn=async_get_is_highfps,
|
||||
ufp_value="is_high_fps_enabled",
|
||||
ufp_set_method_fn=_set_highfps,
|
||||
ufp_perm=PermRequired.WRITE,
|
||||
),
|
||||
|
@ -328,7 +327,7 @@ class ProtectSwitch(ProtectDeviceEntity, SwitchEntity):
|
|||
) -> None:
|
||||
"""Initialize an UniFi Protect Switch."""
|
||||
super().__init__(data, device, description)
|
||||
self._attr_name = f"{self.device.name} {self.entity_description.name}"
|
||||
self._attr_name = f"{self.device.display_name} {self.entity_description.name}"
|
||||
self._switch_type = self.entity_description.key
|
||||
|
||||
if not isinstance(self.device, Camera):
|
||||
|
@ -362,7 +361,9 @@ class ProtectSwitch(ProtectDeviceEntity, SwitchEntity):
|
|||
|
||||
if self._switch_type == _KEY_PRIVACY_MODE:
|
||||
assert isinstance(self.device, Camera)
|
||||
_LOGGER.debug("Setting Privacy Mode to false for %s", self.device.name)
|
||||
_LOGGER.debug(
|
||||
"Setting Privacy Mode to false for %s", self.device.display_name
|
||||
)
|
||||
await self.device.set_privacy(
|
||||
False, self._previous_mic_level, self._previous_record_mode
|
||||
)
|
||||
|
|
|
@ -9,18 +9,15 @@ from typing import Any
|
|||
|
||||
from pyunifiprotect.data import (
|
||||
Bootstrap,
|
||||
Camera,
|
||||
Light,
|
||||
LightModeEnableType,
|
||||
LightModeType,
|
||||
ProtectAdoptableDeviceModel,
|
||||
ProtectDeviceModel,
|
||||
VideoMode,
|
||||
)
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .const import DEVICES_THAT_ADOPT, ModelType
|
||||
from .const import ModelType
|
||||
|
||||
|
||||
def get_nested_attr(obj: Any, attr: str) -> Any:
|
||||
|
@ -79,30 +76,10 @@ def async_get_devices_by_type(
|
|||
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
|
||||
def async_get_devices(
|
||||
bootstrap: Bootstrap, model_type: Iterable[ModelType]
|
||||
) -> Generator[ProtectDeviceModel, None, None]:
|
||||
) -> Generator[ProtectAdoptableDeviceModel, None, None]:
|
||||
"""Return all device by type."""
|
||||
return (
|
||||
device
|
||||
|
@ -111,13 +88,6 @@ def async_get_devices(
|
|||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_is_highfps(obj: Camera) -> bool:
|
||||
"""Return if camera has High FPS mode enabled."""
|
||||
|
||||
return bool(obj.video_mode == VideoMode.HIGH_FPS)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_light_motion_current(obj: Light) -> str:
|
||||
"""Get light motion mode for Flood Light."""
|
||||
|
|
|
@ -284,7 +284,7 @@ def ids_from_device_description(
|
|||
def generate_random_ids() -> tuple[str, str]:
|
||||
"""Generate random IDs for device."""
|
||||
|
||||
return random_hex(24).upper(), random_hex(12).upper()
|
||||
return random_hex(24).lower(), random_hex(12).upper()
|
||||
|
||||
|
||||
def regenerate_device_ids(device: ProtectAdoptableDeviceModel) -> None:
|
||||
|
|
|
@ -36,6 +36,7 @@ from .conftest import (
|
|||
MockEntityFixture,
|
||||
assert_entity_counts,
|
||||
ids_from_device_description,
|
||||
regenerate_device_ids,
|
||||
reset_objects,
|
||||
)
|
||||
|
||||
|
@ -65,11 +66,22 @@ async def camera_fixture(
|
|||
camera_obj.last_ring = now - timedelta(hours=1)
|
||||
camera_obj.is_dark = False
|
||||
camera_obj.is_motion_detected = False
|
||||
regenerate_device_ids(camera_obj)
|
||||
|
||||
no_camera_obj = mock_camera.copy()
|
||||
no_camera_obj._api = mock_entry.api
|
||||
no_camera_obj.channels[0]._api = mock_entry.api
|
||||
no_camera_obj.channels[1]._api = mock_entry.api
|
||||
no_camera_obj.channels[2]._api = mock_entry.api
|
||||
no_camera_obj.name = "Unadopted Camera"
|
||||
no_camera_obj.is_adopted = False
|
||||
regenerate_device_ids(no_camera_obj)
|
||||
|
||||
reset_objects(mock_entry.api.bootstrap)
|
||||
mock_entry.api.bootstrap.nvr.system_info.storage.devices = []
|
||||
mock_entry.api.bootstrap.cameras = {
|
||||
camera_obj.id: camera_obj,
|
||||
no_camera_obj.id: no_camera_obj,
|
||||
}
|
||||
|
||||
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||
|
@ -135,6 +147,7 @@ async def camera_none_fixture(
|
|||
|
||||
reset_objects(mock_entry.api.bootstrap)
|
||||
mock_entry.api.bootstrap.nvr.system_info.storage.devices = []
|
||||
mock_entry.api.bootstrap.nvr.system_info.ustorage = None
|
||||
mock_entry.api.bootstrap.cameras = {
|
||||
camera_obj.id: camera_obj,
|
||||
}
|
||||
|
@ -142,7 +155,7 @@ async def camera_none_fixture(
|
|||
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert_entity_counts(hass, Platform.BINARY_SENSOR, 8, 8)
|
||||
assert_entity_counts(hass, Platform.BINARY_SENSOR, 2, 2)
|
||||
|
||||
yield camera_obj
|
||||
|
||||
|
|
|
@ -529,13 +529,22 @@ async def test_camera_ws_update(
|
|||
new_camera = camera[0].copy()
|
||||
new_camera.is_recording = True
|
||||
|
||||
mock_msg = Mock()
|
||||
mock_msg.changed_data = {}
|
||||
mock_msg.new_obj = new_camera
|
||||
no_camera = camera[0].copy()
|
||||
no_camera.is_adopted = False
|
||||
|
||||
new_bootstrap.cameras = {new_camera.id: new_camera}
|
||||
mock_entry.api.bootstrap = new_bootstrap
|
||||
|
||||
mock_msg = Mock()
|
||||
mock_msg.changed_data = {}
|
||||
mock_msg.new_obj = new_camera
|
||||
mock_entry.api.ws_subscription(mock_msg)
|
||||
|
||||
mock_msg = Mock()
|
||||
mock_msg.changed_data = {}
|
||||
mock_msg.new_obj = no_camera
|
||||
mock_entry.api.ws_subscription(mock_msg)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(camera[1])
|
||||
|
|
|
@ -242,3 +242,27 @@ async def test_device_remove_devices(
|
|||
await remove_device(await hass_ws_client(hass), dead_device_entry.id, entry_id)
|
||||
is True
|
||||
)
|
||||
|
||||
|
||||
async def test_device_remove_devices_nvr(
|
||||
hass: HomeAssistant,
|
||||
mock_entry: MockEntityFixture,
|
||||
hass_ws_client: Callable[
|
||||
[HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse]
|
||||
],
|
||||
) -> None:
|
||||
"""Test we can only remove a NVR device that no longer exists."""
|
||||
assert await async_setup_component(hass, "config", {})
|
||||
|
||||
mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap)
|
||||
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
entry_id = mock_entry.entry.entry_id
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
live_device_entry = list(device_registry.devices.values())[0]
|
||||
assert (
|
||||
await remove_device(await hass_ws_client(hass), live_device_entry.id, entry_id)
|
||||
is False
|
||||
)
|
||||
|
|
|
@ -20,7 +20,7 @@ from homeassistant.const import (
|
|||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import MockEntityFixture, assert_entity_counts
|
||||
from .conftest import MockEntityFixture, assert_entity_counts, regenerate_device_ids
|
||||
|
||||
|
||||
@pytest.fixture(name="light")
|
||||
|
@ -36,9 +36,17 @@ async def light_fixture(
|
|||
light_obj._api = mock_entry.api
|
||||
light_obj.name = "Test Light"
|
||||
light_obj.is_light_on = False
|
||||
regenerate_device_ids(light_obj)
|
||||
|
||||
no_light_obj = mock_light.copy()
|
||||
no_light_obj._api = mock_entry.api
|
||||
no_light_obj.name = "Unadopted Light"
|
||||
no_light_obj.is_adopted = False
|
||||
regenerate_device_ids(no_light_obj)
|
||||
|
||||
mock_entry.api.bootstrap.lights = {
|
||||
light_obj.id: light_obj,
|
||||
no_light_obj.id: no_light_obj,
|
||||
}
|
||||
|
||||
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||
|
|
|
@ -23,7 +23,7 @@ from homeassistant.const import (
|
|||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import MockEntityFixture, assert_entity_counts
|
||||
from .conftest import MockEntityFixture, assert_entity_counts, regenerate_device_ids
|
||||
|
||||
|
||||
@pytest.fixture(name="doorlock")
|
||||
|
@ -39,9 +39,17 @@ async def doorlock_fixture(
|
|||
lock_obj._api = mock_entry.api
|
||||
lock_obj.name = "Test Lock"
|
||||
lock_obj.lock_status = LockStatusType.OPEN
|
||||
regenerate_device_ids(lock_obj)
|
||||
|
||||
no_lock_obj = mock_doorlock.copy()
|
||||
no_lock_obj._api = mock_entry.api
|
||||
no_lock_obj.name = "Unadopted Lock"
|
||||
no_lock_obj.is_adopted = False
|
||||
regenerate_device_ids(no_lock_obj)
|
||||
|
||||
mock_entry.api.bootstrap.doorlocks = {
|
||||
lock_obj.id: lock_obj,
|
||||
no_lock_obj.id: no_lock_obj,
|
||||
}
|
||||
|
||||
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||
|
|
|
@ -26,7 +26,7 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import MockEntityFixture, assert_entity_counts
|
||||
from .conftest import MockEntityFixture, assert_entity_counts, regenerate_device_ids
|
||||
|
||||
|
||||
@pytest.fixture(name="camera")
|
||||
|
@ -45,9 +45,20 @@ async def camera_fixture(
|
|||
camera_obj.channels[2]._api = mock_entry.api
|
||||
camera_obj.name = "Test Camera"
|
||||
camera_obj.feature_flags.has_speaker = True
|
||||
regenerate_device_ids(camera_obj)
|
||||
|
||||
no_camera_obj = mock_camera.copy()
|
||||
no_camera_obj._api = mock_entry.api
|
||||
no_camera_obj.channels[0]._api = mock_entry.api
|
||||
no_camera_obj.channels[1]._api = mock_entry.api
|
||||
no_camera_obj.channels[2]._api = mock_entry.api
|
||||
no_camera_obj.name = "Unadopted Camera"
|
||||
no_camera_obj.is_adopted = False
|
||||
regenerate_device_ids(no_camera_obj)
|
||||
|
||||
mock_entry.api.bootstrap.cameras = {
|
||||
camera_obj.id: camera_obj,
|
||||
no_camera_obj.id: no_camera_obj,
|
||||
}
|
||||
|
||||
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
|
||||
|
|
|
@ -5,6 +5,8 @@ from __future__ import annotations
|
|||
from unittest.mock import AsyncMock
|
||||
|
||||
from pyunifiprotect.data import Light
|
||||
from pyunifiprotect.data.bootstrap import ProtectDeviceRef
|
||||
from pyunifiprotect.exceptions import NvrError
|
||||
|
||||
from homeassistant.components.unifiprotect.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
|
@ -34,6 +36,10 @@ async def test_migrate_reboot_button(
|
|||
light1.id: light1,
|
||||
light2.id: light2,
|
||||
}
|
||||
mock_entry.api.bootstrap.id_lookup = {
|
||||
light1.id: ProtectDeviceRef(id=light1.id, model=light1.model),
|
||||
light2.id: ProtectDeviceRef(id=light2.id, model=light2.model),
|
||||
}
|
||||
mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap)
|
||||
|
||||
registry = er.async_get(hass)
|
||||
|
@ -77,6 +83,41 @@ async def test_migrate_reboot_button(
|
|||
assert light.unique_id == f"{light2.mac}_reboot"
|
||||
|
||||
|
||||
async def test_migrate_nvr_mac(
|
||||
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light
|
||||
):
|
||||
"""Test migrating unique ID of NVR to use MAC address."""
|
||||
|
||||
mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap)
|
||||
nvr = mock_entry.api.bootstrap.nvr
|
||||
regenerate_device_ids(nvr)
|
||||
|
||||
registry = er.async_get(hass)
|
||||
registry.async_get_or_create(
|
||||
Platform.SENSOR,
|
||||
DOMAIN,
|
||||
f"{nvr.id}_storage_utilization",
|
||||
config_entry=mock_entry.entry,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
assert registry.async_get(f"{Platform.SENSOR}.{DOMAIN}_storage_utilization") is None
|
||||
assert (
|
||||
registry.async_get(f"{Platform.SENSOR}.{DOMAIN}_storage_utilization_2") is None
|
||||
)
|
||||
sensor = registry.async_get(
|
||||
f"{Platform.SENSOR}.{DOMAIN}_{nvr.id}_storage_utilization"
|
||||
)
|
||||
assert sensor is not None
|
||||
assert sensor.unique_id == f"{nvr.mac}_storage_utilization"
|
||||
|
||||
|
||||
async def test_migrate_reboot_button_no_device(
|
||||
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light
|
||||
):
|
||||
|
@ -132,6 +173,9 @@ async def test_migrate_reboot_button_fail(
|
|||
mock_entry.api.bootstrap.lights = {
|
||||
light1.id: light1,
|
||||
}
|
||||
mock_entry.api.bootstrap.id_lookup = {
|
||||
light1.id: ProtectDeviceRef(id=light1.id, model=light1.model),
|
||||
}
|
||||
mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap)
|
||||
|
||||
registry = er.async_get(hass)
|
||||
|
@ -175,6 +219,9 @@ async def test_migrate_device_mac_button_fail(
|
|||
mock_entry.api.bootstrap.lights = {
|
||||
light1.id: light1,
|
||||
}
|
||||
mock_entry.api.bootstrap.id_lookup = {
|
||||
light1.id: ProtectDeviceRef(id=light1.id, model=light1.model)
|
||||
}
|
||||
mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap)
|
||||
|
||||
registry = er.async_get(hass)
|
||||
|
@ -203,3 +250,40 @@ async def test_migrate_device_mac_button_fail(
|
|||
light = registry.async_get(f"{Platform.BUTTON}.test_light_1")
|
||||
assert light is not None
|
||||
assert light.unique_id == f"{light1.id}_reboot"
|
||||
|
||||
|
||||
async def test_migrate_device_mac_bootstrap_fail(
|
||||
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light
|
||||
):
|
||||
"""Test migrating with a network error."""
|
||||
|
||||
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(side_effect=NvrError)
|
||||
|
||||
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.SETUP_RETRY
|
||||
|
|
Loading…
Add table
Reference in a new issue