hass-core/tests/components/unifiprotect/test_camera.py

359 lines
12 KiB
Python
Raw Normal View History

"""Test the UniFi Protect camera platform."""
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.exceptions import NvrError
from homeassistant.components.camera import Camera, async_get_image
from homeassistant.components.unifiprotect.const import (
ATTR_BITRATE,
ATTR_CHANNEL_ID,
ATTR_FPS,
ATTR_HEIGHT,
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.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component
from .conftest import MockEntityFixture, time_changed
async def validate_camera_entity(
hass: HomeAssistant,
camera: ProtectCamera,
channel_id: int,
secure: bool,
rtsp_enabled: bool,
enabled: bool,
):
"""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.unique_id == unique_id
if not enabled:
return
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
entity_state = hass.states.get(entity_id)
assert entity_state
assert entity_state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION
assert entity_state.attributes[ATTR_WIDTH] == channel.width
assert entity_state.attributes[ATTR_HEIGHT] == channel.height
assert entity_state.attributes[ATTR_FPS] == channel.fps
assert entity_state.attributes[ATTR_BITRATE] == channel.bitrate
assert entity_state.attributes[ATTR_CHANNEL_ID] == channel.id
async def test_basic_setup(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: ProtectCamera
):
"""Test working setup of unifiprotect entry."""
camera_high_only = mock_camera.copy(deep=True)
camera_high_only._api = mock_entry.api
camera_high_only.channels[0]._api = mock_entry.api
camera_high_only.channels[1]._api = mock_entry.api
camera_high_only.channels[2]._api = mock_entry.api
camera_high_only.name = "Test Camera 1"
camera_high_only.id = "test_high"
camera_high_only.channels[0].is_rtsp_enabled = True
camera_high_only.channels[0].name = "High"
camera_high_only.channels[0].rtsp_alias = "test_high_alias"
camera_high_only.channels[1].is_rtsp_enabled = False
camera_high_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.id = "test_all"
camera_all_channels.channels[0].is_rtsp_enabled = True
camera_all_channels.channels[0].name = "High"
camera_all_channels.channels[0].rtsp_alias = "test_high_alias"
camera_all_channels.channels[1].is_rtsp_enabled = True
camera_all_channels.channels[1].name = "Medium"
camera_all_channels.channels[1].rtsp_alias = "test_medium_alias"
camera_all_channels.channels[2].is_rtsp_enabled = True
camera_all_channels.channels[2].name = "Low"
camera_all_channels.channels[2].rtsp_alias = "test_low_alias"
camera_no_channels = mock_camera.copy(deep=True)
camera_no_channels._api = mock_entry.api
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.id = "test_none"
camera_no_channels.channels[0].is_rtsp_enabled = False
camera_no_channels.channels[0].name = "High"
camera_no_channels.channels[1].is_rtsp_enabled = False
camera_no_channels.channels[2].is_rtsp_enabled = False
mock_entry.api.bootstrap.cameras = {
camera_high_only.id: camera_high_only,
camera_all_channels.id: camera_all_channels,
camera_no_channels.id: camera_no_channels,
}
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
)
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
)
await validate_camera_entity(
hass, camera_no_channels, 0, secure=True, rtsp_enabled=False, enabled=True
)
async def test_missing_channels(
hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: ProtectCamera
):
"""Test setting up camera with no camera channels."""
camera = mock_camera.copy(deep=True)
camera.channels = []
mock_entry.api.bootstrap.cameras = {camera.id: camera}
await hass.config_entries.async_setup(mock_entry.entry.entry_id)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
assert len(hass.states.async_all()) == 0
assert len(entity_registry.entities) == 0
async def test_camera_image(
hass: HomeAssistant,
mock_entry: MockEntityFixture,
simple_camera: tuple[Camera, str],
):
"""Test retrieving camera image."""
mock_entry.api.get_camera_snapshot = AsyncMock()
await async_get_image(hass, simple_camera[1])
mock_entry.api.get_camera_snapshot.assert_called_once()
async def test_camera_generic_update(
hass: HomeAssistant,
mock_entry: MockEntityFixture,
simple_camera: tuple[ProtectCamera, str],
):
"""Tests generic entity update service."""
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"
mock_entry.api.update = AsyncMock(return_value=None)
await hass.services.async_call(
"homeassistant",
"update_entity",
{ATTR_ENTITY_ID: simple_camera[1]},
blocking=True,
)
state = hass.states.get(simple_camera[1])
assert state and state.state == "idle"
async def test_camera_interval_update(
hass: HomeAssistant,
mock_entry: MockEntityFixture,
simple_camera: tuple[ProtectCamera, str],
):
"""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"
new_bootstrap = copy(mock_entry.api.bootstrap)
new_camera = simple_camera[0].copy()
new_camera.is_recording = True
new_bootstrap.cameras = {new_camera.id: new_camera}
mock_entry.api.update = AsyncMock(return_value=new_bootstrap)
mock_entry.api.bootstrap = new_bootstrap
await time_changed(hass, DEFAULT_SCAN_INTERVAL)
state = hass.states.get(simple_camera[1])
assert state and state.state == "recording"
async def test_camera_bad_interval_update(
hass: HomeAssistant,
mock_entry: MockEntityFixture,
simple_camera: tuple[Camera, str],
):
"""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"
# update fails
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"
# next update succeeds
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"
async def test_camera_ws_update(
hass: HomeAssistant,
mock_entry: MockEntityFixture,
simple_camera: tuple[ProtectCamera, str],
):
"""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"
new_bootstrap = copy(mock_entry.api.bootstrap)
new_camera = simple_camera[0].copy()
new_camera.is_recording = True
mock_msg = Mock()
mock_msg.new_obj = new_camera
new_bootstrap.cameras = {new_camera.id: new_camera}
mock_entry.api.bootstrap = new_bootstrap
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done()
state = hass.states.get(simple_camera[1])
assert state and state.state == "recording"
async def test_camera_ws_update_offline(
hass: HomeAssistant,
mock_entry: MockEntityFixture,
simple_camera: tuple[ProtectCamera, str],
):
"""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"
# camera goes offline
new_bootstrap = copy(mock_entry.api.bootstrap)
new_camera = simple_camera[0].copy()
new_camera.is_connected = False
mock_msg = Mock()
mock_msg.new_obj = new_camera
new_bootstrap.cameras = {new_camera.id: new_camera}
mock_entry.api.bootstrap = new_bootstrap
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done()
state = hass.states.get(simple_camera[1])
assert state and state.state == "unavailable"
# camera comes back online
new_camera.is_connected = True
mock_msg = Mock()
mock_msg.new_obj = new_camera
new_bootstrap.cameras = {new_camera.id: new_camera}
mock_entry.api.bootstrap = new_bootstrap
mock_entry.api.ws_subscription(mock_msg)
await hass.async_block_till_done()
state = hass.states.get(simple_camera[1])
assert state and state.state == "idle"