diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index c83221b0ccf..40214b60766 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -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 diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 68c395faaf7..eb4b2024233 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -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 diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 01714868261..d647cdac64a 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -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.""" diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index c78e8e2f77a..a84346a8384 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -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 diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 9cd15c4e3c2..8e114c4f38b 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -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( diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 78a3c5ebac8..4a20e816ce2 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -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: diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 9bf3c8de7a0..65734569de2 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -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, diff --git a/homeassistant/components/unifiprotect/light.py b/homeassistant/components/unifiprotect/light.py index b200fb85e03..bd64905a289 100644 --- a/homeassistant/components/unifiprotect/light.py +++ b/homeassistant/components/unifiprotect/light.py @@ -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) diff --git a/homeassistant/components/unifiprotect/lock.py b/homeassistant/components/unifiprotect/lock.py index 9cef3e19e36..7258dc5f952 100644 --- a/homeassistant/components/unifiprotect/lock.py +++ b/homeassistant/components/unifiprotect/lock.py @@ -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() diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index c4fb1dbe15b..b0391c9d860 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -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) diff --git a/homeassistant/components/unifiprotect/migrate.py b/homeassistant/components/unifiprotect/migrate.py index 3273bd80408..893ca3e458a 100644 --- a/homeassistant/components/unifiprotect/migrate.py +++ b/homeassistant/components/unifiprotect/migrate.py @@ -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 diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index 81ad8438dd7..dee2006b429 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -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: diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 15377a37b27..17bdfa390a6 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -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 diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 57fe0d5aabd..012d52ae215 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -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 diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index efa91b3a6ba..8b3661ce324 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -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 ) diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index b2eb8c1ca65..72baab334f3 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -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.""" diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index adc69cc8bf9..68945ac0988 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -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: diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 8e868b4af21..da9969ad868 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -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 diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index 03b52c7e52e..66da8e8ec04 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -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]) diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index d36183ba135..5c06eedc4c9 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -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 + ) diff --git a/tests/components/unifiprotect/test_light.py b/tests/components/unifiprotect/test_light.py index c4f324f30fd..3bcca436911 100644 --- a/tests/components/unifiprotect/test_light.py +++ b/tests/components/unifiprotect/test_light.py @@ -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) diff --git a/tests/components/unifiprotect/test_lock.py b/tests/components/unifiprotect/test_lock.py index 36b3d140871..3ebfd2de22f 100644 --- a/tests/components/unifiprotect/test_lock.py +++ b/tests/components/unifiprotect/test_lock.py @@ -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) diff --git a/tests/components/unifiprotect/test_media_player.py b/tests/components/unifiprotect/test_media_player.py index a4cbc9e8d22..c18a407eadb 100644 --- a/tests/components/unifiprotect/test_media_player.py +++ b/tests/components/unifiprotect/test_media_player.py @@ -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) diff --git a/tests/components/unifiprotect/test_migrate.py b/tests/components/unifiprotect/test_migrate.py index b62aa9d7757..206c85e3654 100644 --- a/tests/components/unifiprotect/test_migrate.py +++ b/tests/components/unifiprotect/test_migrate.py @@ -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