diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 84cef044c6b..49e88f866b6 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -29,6 +29,7 @@ from .const import ( DEVICES_FOR_SUBSCRIBE, DOMAIN, MIN_REQUIRED_PROTECT_V, + OUTDATED_LOG_MESSAGE, PLATFORMS, ) from .data import ProtectData @@ -60,15 +61,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: nvr_info = await protect.get_nvr() except NotAuthorized as err: raise ConfigEntryAuthFailed(err) from err - except (asyncio.TimeoutError, NvrError, ServerDisconnectedError) as notreadyerror: - raise ConfigEntryNotReady from notreadyerror + except (asyncio.TimeoutError, NvrError, ServerDisconnectedError) as err: + raise ConfigEntryNotReady from err if nvr_info.version < MIN_REQUIRED_PROTECT_V: _LOGGER.error( - ( - "You are running v%s of UniFi Protect. Minimum required version is v%s. " - "Please upgrade UniFi Protect and then retry" - ), + OUTDATED_LOG_MESSAGE, nvr_info.version, MIN_REQUIRED_PROTECT_V, ) diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index 2aeaefd322a..95bc630e58d 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -1,9 +1,8 @@ """Support for Ubiquiti's UniFi Protect NVR.""" from __future__ import annotations -from collections.abc import Callable, Generator, Sequence +from collections.abc import Generator import logging -from typing import Any from pyunifiprotect.api import ProtectApiClient from pyunifiprotect.data import Camera as UFPCamera @@ -12,7 +11,7 @@ from pyunifiprotect.data.devices import CameraChannel from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ATTR_BITRATE, @@ -33,7 +32,7 @@ def get_camera_channels( ) -> Generator[tuple[UFPCamera, CameraChannel, bool], None, None]: """Get all the camera channels.""" for camera in protect.bootstrap.cameras.values(): - if len(camera.channels) == 0: + if not camera.channels: _LOGGER.warning( "Camera does not have any channels: %s (id: %s)", camera.name, camera.id ) @@ -53,7 +52,7 @@ def get_camera_channels( async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[Sequence[Entity]], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Discover cameras on a UniFi Protect NVR.""" data: ProtectData = hass.data[DOMAIN][entry.entry_id] @@ -143,12 +142,7 @@ class ProtectCamera(ProtectDeviceEntity, Camera): self._attr_is_recording = self.device.is_connected and self.device.is_recording self._async_set_stream_source() - - @callback - def _async_update_extra_attrs_from_protect(self) -> dict[str, Any]: - """Add additional Attributes to Camera.""" - return { - **super()._async_update_extra_attrs_from_protect(), + self._attr_extra_state_attributes = { ATTR_WIDTH: self.channel.width, ATTR_HEIGHT: self.channel.height, ATTR_FPS: self.channel.fps, diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 805318a9af5..5298e162f1f 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -30,6 +30,7 @@ from .const import ( DEFAULT_VERIFY_SSL, DOMAIN, MIN_REQUIRED_PROTECT_V, + OUTDATED_LOG_MESSAGE, ) _LOGGER = logging.getLogger(__name__) @@ -55,7 +56,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return OptionsFlowHandler(config_entry) @callback - async def _async_create_entry(self, title: str, data: dict[str, Any]) -> FlowResult: + def _async_create_entry(self, title: str, data: dict[str, Any]) -> FlowResult: return self.async_create_entry( title=title, data={**data, CONF_ID: title}, @@ -66,7 +67,6 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) - @callback async def _async_get_nvr_data( self, user_input: dict[str, Any], @@ -97,10 +97,14 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors[CONF_PASSWORD] = "invalid_auth" except NvrError as ex: _LOGGER.debug(ex) - errors["base"] = "nvr_error" + errors["base"] = "cannot_connect" else: if nvr_data.version < MIN_REQUIRED_PROTECT_V: - _LOGGER.debug("UniFi Protect Version not supported") + _LOGGER.debug( + OUTDATED_LOG_MESSAGE, + nvr_data.version, + MIN_REQUIRED_PROTECT_V, + ) errors["base"] = "protect_version" return nvr_data, errors @@ -156,7 +160,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(nvr_data.mac) self._abort_if_unique_id_configured() - return await self._async_create_entry(nvr_data.name, user_input) + return self._async_create_entry(nvr_data.name, user_input) user_input = user_input or {} return self.async_show_form( diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 049cb5cc0ac..37ba103f4ba 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -1,25 +1,18 @@ """Constant definitions for UniFi Protect Integration.""" -# from typing_extensions import Required -from datetime import timedelta - from pyunifiprotect.data.types import ModelType, Version +from homeassistant.const import Platform + DOMAIN = "unifiprotect" -ATTR_EVENT_SCORE = "event_score" -ATTR_EVENT_OBJECT = "event_object" -ATTR_EVENT_THUMB = "event_thumbnail" ATTR_WIDTH = "width" ATTR_HEIGHT = "height" ATTR_FPS = "fps" ATTR_BITRATE = "bitrate" ATTR_CHANNEL_ID = "channel_id" -CONF_DOORBELL_TEXT = "doorbell_text" CONF_DISABLE_RTSP = "disable_rtsp" -CONF_MESSAGE = "message" -CONF_DURATION = "duration" CONF_ALL_UPDATES = "all_updates" CONF_OVERRIDE_CHOST = "override_connection_host" @@ -32,11 +25,9 @@ CONFIG_OPTIONS = [ DEFAULT_PORT = 443 DEFAULT_ATTRIBUTION = "Powered by UniFi Protect Server" DEFAULT_BRAND = "Ubiquiti" -DEFAULT_SCAN_INTERVAL = 2 +DEFAULT_SCAN_INTERVAL = 5 DEFAULT_VERIFY_SSL = False -RING_INTERVAL = timedelta(seconds=3) - DEVICE_TYPE_CAMERA = "camera" DEVICES_THAT_ADOPT = { ModelType.CAMERA, @@ -48,7 +39,6 @@ DEVICES_WITH_ENTITIES = DEVICES_THAT_ADOPT | {ModelType.NVR} DEVICES_FOR_SUBSCRIBE = DEVICES_WITH_ENTITIES | {ModelType.EVENT} MIN_REQUIRED_PROTECT_V = Version("1.20.0") +OUTDATED_LOG_MESSAGE = "You are running v%s of UniFi Protect. Minimum required version is v%s. Please upgrade UniFi Protect and then retry" -PLATFORMS = [ - "camera", -] +PLATFORMS = [Platform.CAMERA] diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 61e9f6225b6..c7ae7344345 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -1,11 +1,8 @@ """Shared Entity definition for UniFi Protect Integration.""" from __future__ import annotations -from typing import Any - from pyunifiprotect.data import ProtectAdoptableDeviceModel -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription @@ -45,6 +42,7 @@ class ProtectDeviceEntity(Entity): name = description.name or "" self._attr_name = f"{self.device.name} {name.title()}" + self._attr_attribution = DEFAULT_ATTRIBUTION self._async_set_device_info() self._async_update_device_from_protect() @@ -55,16 +53,6 @@ class ProtectDeviceEntity(Entity): """ await self.data.async_refresh() - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return UniFi Protect device attributes.""" - attrs = super().extra_state_attributes or {} - return { - **attrs, - ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, - **self._extra_state_attributes, - } - @callback def _async_set_device_info(self) -> None: self._attr_device_info = DeviceInfo( @@ -77,13 +65,6 @@ class ProtectDeviceEntity(Entity): configuration_url=self.device.protect_url, ) - @callback - def _async_update_extra_attrs_from_protect( # pylint: disable=no-self-use - self, - ) -> dict[str, Any]: - """Calculate extra state attributes. Primarily for subclass to override.""" - return {} - @callback def _async_update_device_from_protect(self) -> None: """Update Entity object from Protect device.""" @@ -95,7 +76,6 @@ class ProtectDeviceEntity(Entity): self._attr_available = ( self.data.last_update_success and self.device.is_connected ) - self._extra_state_attributes = self._async_update_extra_attrs_from_protect() @callback def _async_updated_event(self) -> None: diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index 4dd000aade8..e397e0cc922 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -12,7 +12,7 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "protect_version": "Minimum required version is v1.20.0. Please upgrade UniFi Protect and then retry." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" @@ -22,7 +22,7 @@ "step": { "init": { "title": "UniFi Protect Options", - "description": "Realtime metrics option should only be enabled if you have enabled the diagnostics sensors and want them updated in realtime. If if not enabled, they will only update once every 15 minutes.", + "description": "Realtime metrics option should only be enabled if you have enabled the diagnostics sensors and want them updated in realtime. If not enabled, they will only update once every 15 minutes.", "data": { "disable_rtsp": "Disable the RTSP stream", "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", @@ -31,4 +31,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json index d62fcd651ec..a28b9af1e32 100644 --- a/homeassistant/components/unifiprotect/translations/en.json +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "protect_version": "Minimum required version is v1.20.0. Please upgrade UniFi Protect and then retry." }, "step": { "user": { @@ -26,7 +26,7 @@ "disable_rtsp": "Disable the RTSP stream", "override_connection_host": "Override Connection Host" }, - "description": "Realtime metrics option should only be enabled if you have enabled the diagnostics sensors and want them updated in realtime. If if not enabled, they will only update once every 15 minutes.", + "description": "Realtime metrics option should only be enabled if you have enabled the diagnostics sensors and want them updated in realtime. If not enabled, they will only update once every 15 minutes.", "title": "UniFi Protect Options" } } diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index ba0dcabd6bc..ac4e1491b38 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -161,8 +161,22 @@ async def simple_camera( yield (camera, "camera.test_camera_high") -async def time_changed(hass, seconds): +async def time_changed(hass: HomeAssistant, seconds: int) -> None: """Trigger time changed.""" next_update = dt_util.utcnow() + timedelta(seconds) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() + + +async def enable_entity( + hass: HomeAssistant, entry_id: str, entity_id: str +) -> er.RegistryEntry: + """Enable a disabled entity.""" + entity_registry = er.async_get(hass) + + updated_entity = entity_registry.async_update_entity(entity_id, disabled_by=None) + assert not updated_entity.disabled + await hass.config_entries.async_reload(entry_id) + await hass.async_block_till_done() + + return updated_entity diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index d3a122c401c..1ab9e56ff11 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -2,13 +2,18 @@ from __future__ import annotations from copy import copy -from typing import cast from unittest.mock import AsyncMock, Mock from pyunifiprotect.data import Camera as ProtectCamera +from pyunifiprotect.data.devices import CameraChannel from pyunifiprotect.exceptions import NvrError -from homeassistant.components.camera import Camera, async_get_image +from homeassistant.components.camera import ( + SUPPORT_STREAM, + Camera, + async_get_image, + async_get_stream_source, +) from homeassistant.components.unifiprotect.const import ( ATTR_BITRATE, ATTR_CHANNEL_ID, @@ -17,60 +22,96 @@ from homeassistant.components.unifiprotect.const import ( ATTR_WIDTH, DEFAULT_ATTRIBUTION, DEFAULT_SCAN_INTERVAL, - DOMAIN, ) -from homeassistant.components.unifiprotect.data import ProtectData -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from .conftest import MockEntityFixture, time_changed +from .conftest import MockEntityFixture, enable_entity, time_changed -async def validate_camera_entity( +def validate_default_camera_entity( hass: HomeAssistant, camera: ProtectCamera, channel_id: int, - secure: bool, - rtsp_enabled: bool, - enabled: bool, -): +) -> str: """Validate a camera entity.""" channel = camera.channels[channel_id] entity_name = f"{camera.name} {channel.name}" unique_id = f"{camera.id}_{channel.id}" - if not secure: - entity_name += " Insecure" - unique_id += "_insecure" entity_id = f"camera.{entity_name.replace(' ', '_').lower()}" entity_registry = er.async_get(hass) entity = entity_registry.async_get(entity_id) assert entity - assert entity.disabled is (not enabled) + assert entity.disabled is False assert entity.unique_id == unique_id - if not enabled: - return + return entity_id - camera_platform = hass.data.get("camera") - assert camera_platform - ha_camera = cast(Camera, camera_platform.get_entity(entity_id)) - assert ha_camera - if rtsp_enabled: - if secure: - assert await ha_camera.stream_source() == channel.rtsps_url - else: - assert await ha_camera.stream_source() == channel.rtsp_url - else: - assert await ha_camera.stream_source() is None +def validate_rtsps_camera_entity( + hass: HomeAssistant, + camera: ProtectCamera, + channel_id: int, +) -> str: + """Validate a disabled RTSPS camera entity.""" + + channel = camera.channels[channel_id] + + entity_name = f"{camera.name} {channel.name}" + unique_id = f"{camera.id}_{channel.id}" + entity_id = f"camera.{entity_name.replace(' ', '_').lower()}" + + entity_registry = er.async_get(hass) + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.disabled is True + assert entity.unique_id == unique_id + + return entity_id + + +def validate_rtsp_camera_entity( + hass: HomeAssistant, + camera: ProtectCamera, + channel_id: int, +) -> str: + """Validate a disabled RTSP camera entity.""" + + channel = camera.channels[channel_id] + + entity_name = f"{camera.name} {channel.name} Insecure" + unique_id = f"{camera.id}_{channel.id}_insecure" + entity_id = f"camera.{entity_name.replace(' ', '_').lower()}" + + entity_registry = er.async_get(hass) + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.disabled is True + assert entity.unique_id == unique_id + + return entity_id + + +def validate_common_camera_state( + hass: HomeAssistant, + channel: CameraChannel, + entity_id: str, + features: int = SUPPORT_STREAM, +): + """Validate state that is common to all camera entity, regradless of type.""" entity_state = hass.states.get(entity_id) assert entity_state assert entity_state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + assert entity_state.attributes[ATTR_SUPPORTED_FEATURES] == features assert entity_state.attributes[ATTR_WIDTH] == channel.width assert entity_state.attributes[ATTR_HEIGHT] == channel.height assert entity_state.attributes[ATTR_FPS] == channel.fps @@ -78,6 +119,48 @@ async def validate_camera_entity( assert entity_state.attributes[ATTR_CHANNEL_ID] == channel.id +async def validate_rtsps_camera_state( + hass: HomeAssistant, + camera: ProtectCamera, + channel_id: int, + entity_id: str, + features: int = SUPPORT_STREAM, +): + """Validate a camera's state.""" + channel = camera.channels[channel_id] + + assert await async_get_stream_source(hass, entity_id) == channel.rtsps_url + validate_common_camera_state(hass, channel, entity_id, features) + + +async def validate_rtsp_camera_state( + hass: HomeAssistant, + camera: ProtectCamera, + channel_id: int, + entity_id: str, + features: int = SUPPORT_STREAM, +): + """Validate a camera's state.""" + channel = camera.channels[channel_id] + + assert await async_get_stream_source(hass, entity_id) == channel.rtsp_url + validate_common_camera_state(hass, channel, entity_id, features) + + +async def validate_no_stream_camera_state( + hass: HomeAssistant, + camera: ProtectCamera, + channel_id: int, + entity_id: str, + features: int = SUPPORT_STREAM, +): + """Validate a camera's state.""" + channel = camera.channels[channel_id] + + assert await async_get_stream_source(hass, entity_id) is None + validate_common_camera_state(hass, channel, entity_id, features) + + async def test_basic_setup( hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: ProtectCamera ): @@ -96,12 +179,25 @@ async def test_basic_setup( camera_high_only.channels[1].is_rtsp_enabled = False camera_high_only.channels[2].is_rtsp_enabled = False + camera_medium_only = mock_camera.copy(deep=True) + camera_medium_only._api = mock_entry.api + camera_medium_only.channels[0]._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.name = "Test Camera 2" + camera_medium_only.id = "test_medium" + camera_medium_only.channels[0].is_rtsp_enabled = False + camera_medium_only.channels[1].is_rtsp_enabled = True + camera_medium_only.channels[1].name = "Medium" + camera_medium_only.channels[1].rtsp_alias = "test_medium_alias" + camera_medium_only.channels[2].is_rtsp_enabled = False + camera_all_channels = mock_camera.copy(deep=True) camera_all_channels._api = mock_entry.api camera_all_channels.channels[0]._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.name = "Test Camera 2" + 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].name = "High" @@ -118,7 +214,7 @@ async def test_basic_setup( camera_no_channels.channels[0]._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.name = "Test Camera 3" + 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].name = "High" @@ -127,6 +223,7 @@ async def test_basic_setup( mock_entry.api.bootstrap.cameras = { camera_high_only.id: camera_high_only, + camera_medium_only.id: camera_medium_only, camera_all_channels.id: camera_all_channels, camera_no_channels.id: camera_no_channels, } @@ -134,34 +231,55 @@ async def test_basic_setup( await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - await validate_camera_entity( - hass, camera_high_only, 0, secure=True, rtsp_enabled=True, enabled=True - ) - await validate_camera_entity( - hass, camera_high_only, 0, secure=False, rtsp_enabled=True, enabled=False - ) + entity_registry = er.async_get(hass) - await validate_camera_entity( - hass, camera_all_channels, 0, secure=True, rtsp_enabled=True, enabled=True - ) - await validate_camera_entity( - hass, camera_all_channels, 0, secure=False, rtsp_enabled=True, enabled=False - ) - await validate_camera_entity( - hass, camera_all_channels, 1, secure=True, rtsp_enabled=True, enabled=False - ) - await validate_camera_entity( - hass, camera_all_channels, 1, secure=False, rtsp_enabled=True, enabled=False - ) - await validate_camera_entity( - hass, camera_all_channels, 2, secure=True, rtsp_enabled=True, enabled=False - ) - await validate_camera_entity( - hass, camera_all_channels, 2, secure=False, rtsp_enabled=True, enabled=False - ) + assert len(hass.states.async_all()) == 4 + assert len(entity_registry.entities) == 11 - await validate_camera_entity( - hass, camera_no_channels, 0, secure=True, rtsp_enabled=False, enabled=True + # test camera 1 + entity_id = validate_default_camera_entity(hass, camera_high_only, 0) + await validate_rtsps_camera_state(hass, camera_high_only, 0, entity_id) + + entity_id = validate_rtsp_camera_entity(hass, camera_high_only, 0) + await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await validate_rtsp_camera_state(hass, camera_high_only, 0, entity_id) + + # test camera 2 + entity_id = validate_default_camera_entity(hass, camera_medium_only, 1) + await validate_rtsps_camera_state(hass, camera_medium_only, 1, entity_id) + + entity_id = validate_rtsp_camera_entity(hass, camera_medium_only, 1) + await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await validate_rtsp_camera_state(hass, camera_medium_only, 1, entity_id) + + # test camera 3 + entity_id = validate_default_camera_entity(hass, camera_all_channels, 0) + await validate_rtsps_camera_state(hass, camera_all_channels, 0, entity_id) + + entity_id = validate_rtsp_camera_entity(hass, camera_all_channels, 0) + await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await validate_rtsp_camera_state(hass, camera_all_channels, 0, entity_id) + + entity_id = validate_rtsps_camera_entity(hass, camera_all_channels, 1) + await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await validate_rtsps_camera_state(hass, camera_all_channels, 1, entity_id) + + entity_id = validate_rtsp_camera_entity(hass, camera_all_channels, 1) + await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await validate_rtsp_camera_state(hass, camera_all_channels, 1, entity_id) + + entity_id = validate_rtsps_camera_entity(hass, camera_all_channels, 2) + await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await validate_rtsps_camera_state(hass, camera_all_channels, 2, entity_id) + + entity_id = validate_rtsp_camera_entity(hass, camera_all_channels, 2) + await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await validate_rtsp_camera_state(hass, camera_all_channels, 2, entity_id) + + # test camera 4 + entity_id = validate_default_camera_entity(hass, camera_no_channels, 0) + await validate_no_stream_camera_state( + hass, camera_no_channels, 0, entity_id, features=0 ) @@ -206,10 +324,6 @@ async def test_camera_generic_update( assert await async_setup_component(hass, "homeassistant", {}) - data: ProtectData = hass.data[DOMAIN][mock_entry.entry.entry_id] - assert data - assert data.last_update_success - state = hass.states.get(simple_camera[1]) assert state and state.state == "idle" @@ -232,10 +346,6 @@ async def test_camera_interval_update( ): """Interval updates updates camera entity.""" - data: ProtectData = hass.data[DOMAIN][mock_entry.entry.entry_id] - assert data - assert data.last_update_success - state = hass.states.get(simple_camera[1]) assert state and state.state == "idle" @@ -259,10 +369,6 @@ async def test_camera_bad_interval_update( ): """Interval updates marks camera unavailable.""" - data: ProtectData = hass.data[DOMAIN][mock_entry.entry.entry_id] - assert data - assert data.last_update_success - state = hass.states.get(simple_camera[1]) assert state and state.state == "idle" @@ -270,7 +376,6 @@ async def test_camera_bad_interval_update( mock_entry.api.update = AsyncMock(side_effect=NvrError) await time_changed(hass, DEFAULT_SCAN_INTERVAL) - assert not data.last_update_success state = hass.states.get(simple_camera[1]) assert state and state.state == "unavailable" @@ -278,7 +383,6 @@ async def test_camera_bad_interval_update( mock_entry.api.update = AsyncMock(return_value=mock_entry.api.bootstrap) await time_changed(hass, DEFAULT_SCAN_INTERVAL) - assert data.last_update_success state = hass.states.get(simple_camera[1]) assert state and state.state == "idle" @@ -290,10 +394,6 @@ async def test_camera_ws_update( ): """WS update updates camera entity.""" - data: ProtectData = hass.data[DOMAIN][mock_entry.entry.entry_id] - assert data - assert data.last_update_success - state = hass.states.get(simple_camera[1]) assert state and state.state == "idle" @@ -320,10 +420,6 @@ async def test_camera_ws_update_offline( ): """WS updates marks camera unavailable.""" - data: ProtectData = hass.data[DOMAIN][mock_entry.entry.entry_id] - assert data - assert data.last_update_success - state = hass.states.get(simple_camera[1]) assert state and state.state == "idle" diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 80c6019c28a..9065f7f964b 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -129,7 +129,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "nvr_error"} + assert result2["errors"] == {"base": "cannot_connect"} async def test_form_reauth_auth(hass: HomeAssistant) -> None: @@ -190,7 +190,7 @@ async def test_form_reauth_auth(hass: HomeAssistant) -> None: assert result3["reason"] == "reauth_successful" -async def test_form_options(hass: HomeAssistant) -> None: +async def test_form_options(hass: HomeAssistant, mock_client) -> None: """Test we handle options flows.""" mock_config = MockConfigEntry( domain=DOMAIN, @@ -207,7 +207,12 @@ async def test_form_options(hass: HomeAssistant) -> None: ) mock_config.add_to_hass(hass) - # Integration not setup, since we are only flipping bits in options entry + with patch("homeassistant.components.unifiprotect.ProtectApiClient") as mock_api: + mock_api.return_value = mock_client + + await hass.config_entries.async_setup(mock_config.entry_id) + await hass.async_block_till_done() + assert mock_config.state == config_entries.ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(mock_config.entry_id) assert result["type"] == RESULT_TYPE_FORM diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index bc42573e26b..4b651c9cda0 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -5,7 +5,7 @@ from unittest.mock import AsyncMock from pyunifiprotect import NotAuthorized, NvrError -from homeassistant.components.unifiprotect.const import CONF_DISABLE_RTSP, DOMAIN +from homeassistant.components.unifiprotect.const import CONF_DISABLE_RTSP from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -21,7 +21,6 @@ async def test_setup(hass: HomeAssistant, mock_entry: MockEntityFixture): 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 mock_entry.entry.entry_id in hass.data[DOMAIN] async def test_reload(hass: HomeAssistant, mock_entry: MockEntityFixture): @@ -37,7 +36,6 @@ async def test_reload(hass: HomeAssistant, mock_entry: MockEntityFixture): await hass.async_block_till_done() assert mock_entry.entry.state == ConfigEntryState.LOADED - assert mock_entry.entry.entry_id in hass.data[DOMAIN] assert mock_entry.api.async_disconnect_ws.called