Followup PR for UniFi Protect integration (#62806)
* Followup improvements from initial PR * Update tests/components/unifiprotect/conftest.py Co-authored-by: J. Nick Koston <nick@koston.org> * Update translations * Fixes log message * Fixes log message * Unknown to cannot connect * Update tests/components/unifiprotect/test_config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Fixes camera coverage Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>pull/62847/head
parent
cf6fb7bf39
commit
dc3f21dd1e
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue