"""The tests for UVC camera module.""" from datetime import datetime, timedelta, timezone from unittest.mock import call, patch import pytest import requests from uvcclient import camera, nvr from homeassistant.components.camera import ( DEFAULT_CONTENT_TYPE, SERVICE_DISABLE_MOTION, SERVICE_ENABLE_MOTION, STATE_RECORDING, CameraEntityFeature, async_get_image, async_get_stream_source, ) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed @pytest.fixture(name="mock_remote") def mock_remote_fixture(camera_info): """Mock the nvr.UVCRemote class.""" with patch("homeassistant.components.uvc.camera.nvr.UVCRemote") as mock_remote: def setup(host, port, apikey, ssl=False): """Set instance attributes.""" mock_remote.return_value._host = host mock_remote.return_value._port = port mock_remote.return_value._apikey = apikey mock_remote.return_value._ssl = ssl return mock_remote.return_value mock_remote.side_effect = setup mock_remote.return_value.get_camera.return_value = camera_info mock_cameras = [ {"uuid": "one", "name": "Front", "id": "id1"}, {"uuid": "two", "name": "Back", "id": "id2"}, ] mock_remote.return_value.index.return_value = mock_cameras mock_remote.return_value.server_version = (3, 2, 0) yield mock_remote @pytest.fixture(name="camera_info") def camera_info_fixture(): """Mock the camera info of a camera.""" return { "model": "UVC", "recordingSettings": { "fullTimeRecordEnabled": True, "motionRecordEnabled": False, }, "host": "host-a", "internalHost": "host-b", "username": "admin", "lastRecordingStartTime": 1610070992367, "channels": [ { "id": "0", "width": 1920, "height": 1080, "fps": 25, "bitrate": 6000000, "isRtspEnabled": True, "rtspUris": [ "rtsp://host-a:7447/uuid_rtspchannel_0", "rtsp://foo:7447/uuid_rtspchannel_0", ], }, { "id": "1", "width": 1024, "height": 576, "fps": 15, "bitrate": 1200000, "isRtspEnabled": False, "rtspUris": [ "rtsp://host-a:7447/uuid_rtspchannel_1", "rtsp://foo:7447/uuid_rtspchannel_1", ], }, ], } @pytest.fixture(name="camera_v320") def camera_v320_fixture(): """Mock the v320 camera.""" with patch( "homeassistant.components.uvc.camera.uvc_camera.UVCCameraClientV320" ) as camera: camera.return_value.get_snapshot.return_value = "test_image" yield camera @pytest.fixture(name="camera_v313") def camera_v313_fixture(): """Mock the v320 camera.""" with patch( "homeassistant.components.uvc.camera.uvc_camera.UVCCameraClient" ) as camera: camera.return_value.get_snapshot.return_value = "test_image" yield camera async def test_setup_full_config(hass, mock_remote, camera_info): """Test the setup with full configuration.""" config = { "platform": "uvc", "nvr": "foo", "password": "bar", "port": 123, "key": "secret", } def mock_get_camera(uuid): """Create a mock camera.""" if uuid == "id3": camera_info["model"] = "airCam" return camera_info mock_remote.return_value.index.return_value.append( {"uuid": "three", "name": "Old AirCam", "id": "id3"} ) mock_remote.return_value.get_camera.side_effect = mock_get_camera assert await async_setup_component(hass, "camera", {"camera": config}) await hass.async_block_till_done() assert mock_remote.call_count == 1 assert mock_remote.call_args == call("foo", 123, "secret", ssl=False) camera_states = hass.states.async_all("camera") assert len(camera_states) == 2 state = hass.states.get("camera.front") assert state assert state.name == "Front" state = hass.states.get("camera.back") assert state assert state.name == "Back" entity_registry = async_get_entity_registry(hass) entity_entry = entity_registry.async_get("camera.front") assert entity_entry.unique_id == "id1" entity_entry = entity_registry.async_get("camera.back") assert entity_entry.unique_id == "id2" async def test_setup_partial_config(hass, mock_remote): """Test the setup with partial configuration.""" config = {"platform": "uvc", "nvr": "foo", "key": "secret"} assert await async_setup_component(hass, "camera", {"camera": config}) await hass.async_block_till_done() assert mock_remote.call_count == 1 assert mock_remote.call_args == call("foo", 7080, "secret", ssl=False) camera_states = hass.states.async_all("camera") assert len(camera_states) == 2 state = hass.states.get("camera.front") assert state assert state.name == "Front" state = hass.states.get("camera.back") assert state assert state.name == "Back" entity_registry = async_get_entity_registry(hass) entity_entry = entity_registry.async_get("camera.front") assert entity_entry.unique_id == "id1" entity_entry = entity_registry.async_get("camera.back") assert entity_entry.unique_id == "id2" async def test_setup_partial_config_v31x(hass, mock_remote): """Test the setup with a v3.1.x server.""" config = {"platform": "uvc", "nvr": "foo", "key": "secret"} mock_remote.return_value.server_version = (3, 1, 3) assert await async_setup_component(hass, "camera", {"camera": config}) await hass.async_block_till_done() assert mock_remote.call_count == 1 assert mock_remote.call_args == call("foo", 7080, "secret", ssl=False) camera_states = hass.states.async_all("camera") assert len(camera_states) == 2 state = hass.states.get("camera.front") assert state assert state.name == "Front" state = hass.states.get("camera.back") assert state assert state.name == "Back" entity_registry = async_get_entity_registry(hass) entity_entry = entity_registry.async_get("camera.front") assert entity_entry.unique_id == "one" entity_entry = entity_registry.async_get("camera.back") assert entity_entry.unique_id == "two" @pytest.mark.parametrize( "config", [ {"platform": "uvc", "nvr": "foo"}, {"platform": "uvc", "key": "secret"}, {"platform": "uvc", "nvr": "foo", "key": "secret", "port": "invalid"}, ], ) async def test_setup_incomplete_config(hass, mock_remote, config): """Test the setup with incomplete or invalid configuration.""" assert await async_setup_component(hass, "camera", config) await hass.async_block_till_done() camera_states = hass.states.async_all("camera") assert not camera_states @pytest.mark.parametrize( "error, ready_states", [ (nvr.NotAuthorized, 0), (nvr.NvrError, 2), (requests.exceptions.ConnectionError, 2), ], ) async def test_setup_nvr_errors_during_indexing(hass, mock_remote, error, ready_states): """Set up test for NVR errors during indexing.""" config = {"platform": "uvc", "nvr": "foo", "key": "secret"} now = utcnow() mock_remote.return_value.index.side_effect = error assert await async_setup_component(hass, "camera", {"camera": config}) await hass.async_block_till_done() camera_states = hass.states.async_all("camera") assert not camera_states # resolve the error mock_remote.return_value.index.side_effect = None async_fire_time_changed(hass, now + timedelta(seconds=31)) await hass.async_block_till_done() camera_states = hass.states.async_all("camera") assert len(camera_states) == ready_states @pytest.mark.parametrize( "error, ready_states", [ (nvr.NotAuthorized, 0), (nvr.NvrError, 2), (requests.exceptions.ConnectionError, 2), ], ) async def test_setup_nvr_errors_during_initialization( hass, mock_remote, error, ready_states ): """Set up test for NVR errors during initialization.""" config = {"platform": "uvc", "nvr": "foo", "key": "secret"} now = utcnow() mock_remote.side_effect = error assert await async_setup_component(hass, "camera", {"camera": config}) await hass.async_block_till_done() assert not mock_remote.index.called camera_states = hass.states.async_all("camera") assert not camera_states # resolve the error mock_remote.side_effect = None async_fire_time_changed(hass, now + timedelta(seconds=31)) await hass.async_block_till_done() camera_states = hass.states.async_all("camera") assert len(camera_states) == ready_states async def test_properties(hass, mock_remote): """Test the properties.""" config = {"platform": "uvc", "nvr": "foo", "key": "secret"} assert await async_setup_component(hass, "camera", {"camera": config}) await hass.async_block_till_done() camera_states = hass.states.async_all("camera") assert len(camera_states) == 2 state = hass.states.get("camera.front") assert state assert state.name == "Front" assert state.state == STATE_RECORDING assert state.attributes["brand"] == "Ubiquiti" assert state.attributes["model_name"] == "UVC" assert state.attributes["supported_features"] == CameraEntityFeature.STREAM async def test_motion_recording_mode_properties(hass, mock_remote): """Test the properties.""" config = {"platform": "uvc", "nvr": "foo", "key": "secret"} now = utcnow() assert await async_setup_component(hass, "camera", {"camera": config}) await hass.async_block_till_done() state = hass.states.get("camera.front") assert state assert state.state == STATE_RECORDING mock_remote.return_value.get_camera.return_value["recordingSettings"][ "fullTimeRecordEnabled" ] = False mock_remote.return_value.get_camera.return_value["recordingSettings"][ "motionRecordEnabled" ] = True async_fire_time_changed(hass, now + timedelta(seconds=31)) await hass.async_block_till_done() state = hass.states.get("camera.front") assert state assert state.state != STATE_RECORDING assert state.attributes["last_recording_start_time"] == datetime( 2021, 1, 8, 1, 56, 32, 367000, tzinfo=timezone.utc ) mock_remote.return_value.get_camera.return_value["recordingIndicator"] = "DISABLED" async_fire_time_changed(hass, now + timedelta(seconds=61)) await hass.async_block_till_done() state = hass.states.get("camera.front") assert state assert state.state != STATE_RECORDING mock_remote.return_value.get_camera.return_value[ "recordingIndicator" ] = "MOTION_INPROGRESS" async_fire_time_changed(hass, now + timedelta(seconds=91)) await hass.async_block_till_done() state = hass.states.get("camera.front") assert state assert state.state == STATE_RECORDING mock_remote.return_value.get_camera.return_value[ "recordingIndicator" ] = "MOTION_FINISHED" async_fire_time_changed(hass, now + timedelta(seconds=121)) await hass.async_block_till_done() state = hass.states.get("camera.front") assert state assert state.state == STATE_RECORDING async def test_stream(hass, mock_remote): """Test the RTSP stream URI.""" config = {"platform": "uvc", "nvr": "foo", "key": "secret"} assert await async_setup_component(hass, "camera", {"camera": config}) await hass.async_block_till_done() stream_source = await async_get_stream_source(hass, "camera.front") assert stream_source == "rtsp://foo:7447/uuid_rtspchannel_0" async def test_login(hass, mock_remote, camera_v320): """Test the login.""" config = {"platform": "uvc", "nvr": "foo", "key": "secret"} assert await async_setup_component(hass, "camera", {"camera": config}) await hass.async_block_till_done() image = await async_get_image(hass, "camera.front") assert camera_v320.call_count == 1 assert camera_v320.call_args == call("host-a", "admin", "ubnt") assert camera_v320.return_value.login.call_count == 1 assert image.content_type == DEFAULT_CONTENT_TYPE assert image.content == "test_image" async def test_login_v31x(hass, mock_remote, camera_v313): """Test login with v3.1.x server.""" mock_remote.return_value.server_version = (3, 1, 3) config = {"platform": "uvc", "nvr": "foo", "key": "secret"} assert await async_setup_component(hass, "camera", {"camera": config}) await hass.async_block_till_done() image = await async_get_image(hass, "camera.front") assert camera_v313.call_count == 1 assert camera_v313.call_args == call("host-a", "admin", "ubnt") assert camera_v313.return_value.login.call_count == 1 assert image.content_type == DEFAULT_CONTENT_TYPE assert image.content == "test_image" @pytest.mark.parametrize( "error", [OSError, camera.CameraConnectError, camera.CameraAuthError] ) async def test_login_tries_both_addrs_and_caches(hass, mock_remote, camera_v320, error): """Test the login tries.""" responses = [0] def mock_login(*a): """Mock login.""" try: responses.pop(0) raise error except IndexError: pass snapshots = [0] def mock_snapshots(*a): """Mock get snapshots.""" try: snapshots.pop(0) raise camera.CameraAuthError() except IndexError: pass return "test_image" camera_v320.return_value.login.side_effect = mock_login config = {"platform": "uvc", "nvr": "foo", "key": "secret"} assert await async_setup_component(hass, "camera", {"camera": config}) await hass.async_block_till_done() image = await async_get_image(hass, "camera.front") assert camera_v320.call_count == 2 assert camera_v320.call_args == call("host-b", "admin", "ubnt") assert image.content_type == DEFAULT_CONTENT_TYPE assert image.content == "test_image" camera_v320.reset_mock() camera_v320.return_value.get_snapshot.side_effect = mock_snapshots image = await async_get_image(hass, "camera.front") assert camera_v320.call_count == 1 assert camera_v320.call_args == call("host-b", "admin", "ubnt") assert camera_v320.return_value.login.call_count == 1 assert image.content_type == DEFAULT_CONTENT_TYPE assert image.content == "test_image" async def test_login_fails_both_properly(hass, mock_remote, camera_v320): """Test if login fails properly.""" camera_v320.return_value.login.side_effect = OSError config = {"platform": "uvc", "nvr": "foo", "key": "secret"} assert await async_setup_component(hass, "camera", {"camera": config}) await hass.async_block_till_done() with pytest.raises(HomeAssistantError): await async_get_image(hass, "camera.front") assert camera_v320.return_value.get_snapshot.call_count == 0 @pytest.mark.parametrize( "source_error, raised_error, snapshot_calls", [ (camera.CameraConnectError, HomeAssistantError, 1), (camera.CameraAuthError, camera.CameraAuthError, 2), ], ) async def test_camera_image_error( hass, mock_remote, camera_v320, source_error, raised_error, snapshot_calls ): """Test the camera image error.""" camera_v320.return_value.get_snapshot.side_effect = source_error config = {"platform": "uvc", "nvr": "foo", "key": "secret"} assert await async_setup_component(hass, "camera", {"camera": config}) await hass.async_block_till_done() with pytest.raises(raised_error): await async_get_image(hass, "camera.front") assert camera_v320.return_value.get_snapshot.call_count == snapshot_calls async def test_enable_disable_motion_detection(hass, mock_remote, camera_info): """Test enable and disable motion detection.""" def set_recordmode(uuid, mode): """Set record mode.""" motion_record_enabled = mode == "motion" camera_info["recordingSettings"]["motionRecordEnabled"] = motion_record_enabled mock_remote.return_value.set_recordmode.side_effect = set_recordmode config = {"platform": "uvc", "nvr": "foo", "key": "secret"} assert await async_setup_component(hass, "camera", {"camera": config}) await hass.async_block_till_done() state = hass.states.get("camera.front") assert state assert "motion_detection" not in state.attributes await hass.services.async_call( "camera", SERVICE_ENABLE_MOTION, {"entity_id": "camera.front"}, True ) await hass.async_block_till_done() state = hass.states.get("camera.front") assert state assert state.attributes["motion_detection"] await hass.services.async_call( "camera", SERVICE_DISABLE_MOTION, {"entity_id": "camera.front"}, True ) await hass.async_block_till_done() state = hass.states.get("camera.front") assert state assert "motion_detection" not in state.attributes mock_remote.return_value.set_recordmode.side_effect = nvr.NvrError await hass.services.async_call( "camera", SERVICE_ENABLE_MOTION, {"entity_id": "camera.front"}, True ) await hass.async_block_till_done() state = hass.states.get("camera.front") assert state assert "motion_detection" not in state.attributes mock_remote.return_value.set_recordmode.side_effect = set_recordmode await hass.services.async_call( "camera", SERVICE_ENABLE_MOTION, {"entity_id": "camera.front"}, True ) await hass.async_block_till_done() state = hass.states.get("camera.front") assert state assert state.attributes["motion_detection"] mock_remote.return_value.set_recordmode.side_effect = nvr.NvrError await hass.services.async_call( "camera", SERVICE_DISABLE_MOTION, {"entity_id": "camera.front"}, True ) await hass.async_block_till_done() state = hass.states.get("camera.front") assert state assert state.attributes["motion_detection"]