"""Test the UniFi Protect select platform.""" # pylint: disable=protected-access from __future__ import annotations from copy import copy from datetime import timedelta from unittest.mock import AsyncMock, Mock, patch import pytest from pyunifiprotect.data import Camera, Light from pyunifiprotect.data.devices import LCDMessage, Viewer from pyunifiprotect.data.nvr import DoorbellMessage, Liveview from pyunifiprotect.data.types import ( DoorbellMessageType, IRLEDMode, LightModeEnableType, LightModeType, RecordingMode, ) from homeassistant.components.select.const import ATTR_OPTIONS from homeassistant.components.unifiprotect.const import ( ATTR_DURATION, ATTR_MESSAGE, DEFAULT_ATTRIBUTION, ) from homeassistant.components.unifiprotect.select import ( CAMERA_SELECTS, LIGHT_MODE_OFF, LIGHT_SELECTS, SERVICE_SET_DOORBELL_MESSAGE, VIEWER_SELECTS, ) from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, ATTR_OPTION, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow from .conftest import ( MockEntityFixture, assert_entity_counts, ids_from_device_description, ) @pytest.fixture(name="viewer") async def viewer_fixture( hass: HomeAssistant, mock_entry: MockEntityFixture, mock_viewer: Viewer, mock_liveview: Liveview, ): """Fixture for a single viewport for testing the select platform.""" # disable pydantic validation so mocking can happen Viewer.__config__.validate_assignment = False viewer_obj = mock_viewer.copy(deep=True) viewer_obj._api = mock_entry.api viewer_obj.name = "Test Viewer" viewer_obj.liveview_id = mock_liveview.id mock_entry.api.bootstrap.reset_objects() mock_entry.api.bootstrap.viewers = { viewer_obj.id: viewer_obj, } mock_entry.api.bootstrap.liveviews = {mock_liveview.id: mock_liveview} await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() assert_entity_counts(hass, Platform.SELECT, 1, 1) yield viewer_obj Viewer.__config__.validate_assignment = True @pytest.fixture(name="camera") async def camera_fixture( hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera ): """Fixture for a single camera for testing the select platform.""" # disable pydantic validation so mocking can happen Camera.__config__.validate_assignment = False camera_obj = mock_camera.copy(deep=True) camera_obj._api = mock_entry.api camera_obj.channels[0]._api = mock_entry.api camera_obj.channels[1]._api = mock_entry.api camera_obj.channels[2]._api = mock_entry.api camera_obj.name = "Test Camera" camera_obj.feature_flags.has_lcd_screen = True camera_obj.feature_flags.has_chime = True camera_obj.recording_settings.mode = RecordingMode.ALWAYS camera_obj.isp_settings.ir_led_mode = IRLEDMode.AUTO camera_obj.lcd_message = None camera_obj.chime_duration = 0 mock_entry.api.bootstrap.reset_objects() mock_entry.api.bootstrap.cameras = { camera_obj.id: camera_obj, } await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() assert_entity_counts(hass, Platform.SELECT, 4, 4) yield camera_obj Camera.__config__.validate_assignment = True @pytest.fixture(name="light") async def light_fixture( hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light, camera: Camera, ): """Fixture for a single light for testing the select platform.""" # disable pydantic validation so mocking can happen Light.__config__.validate_assignment = False light_obj = mock_light.copy(deep=True) light_obj._api = mock_entry.api light_obj.name = "Test Light" light_obj.camera_id = None light_obj.light_mode_settings.mode = LightModeType.MOTION light_obj.light_mode_settings.enable_at = LightModeEnableType.DARK mock_entry.api.bootstrap.reset_objects() mock_entry.api.bootstrap.cameras = {camera.id: camera} mock_entry.api.bootstrap.lights = { light_obj.id: light_obj, } await hass.config_entries.async_reload(mock_entry.entry.entry_id) await hass.async_block_till_done() assert_entity_counts(hass, Platform.SELECT, 6, 6) yield light_obj Light.__config__.validate_assignment = True @pytest.fixture(name="camera_none") async def camera_none_fixture( hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera ): """Fixture for a single camera for testing the select platform.""" # disable pydantic validation so mocking can happen Camera.__config__.validate_assignment = False camera_obj = mock_camera.copy(deep=True) camera_obj._api = mock_entry.api camera_obj.channels[0]._api = mock_entry.api camera_obj.channels[1]._api = mock_entry.api camera_obj.channels[2]._api = mock_entry.api camera_obj.name = "Test Camera" camera_obj.feature_flags.has_lcd_screen = False camera_obj.feature_flags.has_chime = False camera_obj.recording_settings.mode = RecordingMode.ALWAYS camera_obj.isp_settings.ir_led_mode = IRLEDMode.AUTO mock_entry.api.bootstrap.reset_objects() mock_entry.api.bootstrap.cameras = { camera_obj.id: camera_obj, } await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() assert_entity_counts(hass, Platform.SELECT, 2, 2) yield camera_obj Camera.__config__.validate_assignment = True async def test_select_setup_light( hass: HomeAssistant, light: Light, ): """Test select entity setup for light devices.""" entity_registry = er.async_get(hass) expected_values = ("On Motion - When Dark", "Not Paired") for index, description in enumerate(LIGHT_SELECTS): unique_id, entity_id = ids_from_device_description( Platform.SELECT, light, description ) entity = entity_registry.async_get(entity_id) assert entity assert entity.unique_id == unique_id state = hass.states.get(entity_id) assert state assert state.state == expected_values[index] assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION async def test_select_setup_viewer( hass: HomeAssistant, viewer: Viewer, ): """Test select entity setup for light devices.""" entity_registry = er.async_get(hass) description = VIEWER_SELECTS[0] unique_id, entity_id = ids_from_device_description( Platform.SELECT, viewer, description ) entity = entity_registry.async_get(entity_id) assert entity assert entity.unique_id == unique_id state = hass.states.get(entity_id) assert state assert state.state == viewer.liveview.name assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION async def test_select_setup_camera_all( hass: HomeAssistant, camera: Camera, ): """Test select entity setup for camera devices (all features).""" entity_registry = er.async_get(hass) expected_values = ("Always", "Auto", "Default Message (Welcome)", "None") for index, description in enumerate(CAMERA_SELECTS): unique_id, entity_id = ids_from_device_description( Platform.SELECT, camera, description ) entity = entity_registry.async_get(entity_id) assert entity assert entity.unique_id == unique_id state = hass.states.get(entity_id) assert state assert state.state == expected_values[index] assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION async def test_select_setup_camera_none( hass: HomeAssistant, camera_none: Camera, ): """Test select entity setup for camera devices (no features).""" entity_registry = er.async_get(hass) expected_values = ("Always", "Auto", "Default Message (Welcome)") for index, description in enumerate(CAMERA_SELECTS): if index == 2: return unique_id, entity_id = ids_from_device_description( Platform.SELECT, camera_none, description ) entity = entity_registry.async_get(entity_id) assert entity assert entity.unique_id == unique_id state = hass.states.get(entity_id) assert state assert state.state == expected_values[index] assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION async def test_select_update_liveview( hass: HomeAssistant, mock_entry: MockEntityFixture, viewer: Viewer, mock_liveview: Liveview, ): """Test select entity update (new Liveview).""" _, entity_id = ids_from_device_description( Platform.SELECT, viewer, VIEWER_SELECTS[0] ) state = hass.states.get(entity_id) assert state expected_options = state.attributes[ATTR_OPTIONS] new_bootstrap = copy(mock_entry.api.bootstrap) new_liveview = copy(mock_liveview) new_liveview.id = "test_id" mock_msg = Mock() mock_msg.changed_data = {} mock_msg.new_obj = new_liveview new_bootstrap.liveviews = {**new_bootstrap.liveviews, new_liveview.id: new_liveview} mock_entry.api.bootstrap = new_bootstrap mock_entry.api.ws_subscription(mock_msg) await hass.async_block_till_done() state = hass.states.get(entity_id) assert state assert state.attributes[ATTR_OPTIONS] == expected_options async def test_select_update_doorbell_settings( hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera ): """Test select entity update (new Doorbell Message).""" expected_length = ( len(mock_entry.api.bootstrap.nvr.doorbell_settings.all_messages) + 1 ) _, entity_id = ids_from_device_description( Platform.SELECT, camera, CAMERA_SELECTS[2] ) state = hass.states.get(entity_id) assert state assert len(state.attributes[ATTR_OPTIONS]) == expected_length expected_length += 1 new_nvr = copy(mock_entry.api.bootstrap.nvr) new_nvr.__fields__["update_all_messages"] = Mock() new_nvr.update_all_messages = Mock() new_nvr.doorbell_settings.all_messages = [ *new_nvr.doorbell_settings.all_messages, DoorbellMessage( type=DoorbellMessageType.CUSTOM_MESSAGE, text="Test2", ), ] mock_msg = Mock() mock_msg.changed_data = {"doorbell_settings": {}} mock_msg.new_obj = new_nvr mock_entry.api.bootstrap.nvr = new_nvr mock_entry.api.ws_subscription(mock_msg) await hass.async_block_till_done() new_nvr.update_all_messages.assert_called_once() state = hass.states.get(entity_id) assert state assert len(state.attributes[ATTR_OPTIONS]) == expected_length async def test_select_update_doorbell_message( hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera, ): """Test select entity update (change doorbell message).""" _, entity_id = ids_from_device_description( Platform.SELECT, camera, CAMERA_SELECTS[2] ) state = hass.states.get(entity_id) assert state assert state.state == "Default Message (Welcome)" new_bootstrap = copy(mock_entry.api.bootstrap) new_camera = camera.copy() new_camera.lcd_message = LCDMessage( type=DoorbellMessageType.CUSTOM_MESSAGE, text="Test" ) mock_msg = Mock() mock_msg.changed_data = {} 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(entity_id) assert state assert state.state == "Test" async def test_select_set_option_light_motion( hass: HomeAssistant, light: Light, ): """Test Light Mode select.""" _, entity_id = ids_from_device_description(Platform.SELECT, light, LIGHT_SELECTS[0]) light.__fields__["set_light_settings"] = Mock() light.set_light_settings = AsyncMock() await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: LIGHT_MODE_OFF}, blocking=True, ) light.set_light_settings.assert_called_once_with( LightModeType.MANUAL, enable_at=None ) async def test_select_set_option_light_camera( hass: HomeAssistant, light: Light, ): """Test Paired Camera select.""" _, entity_id = ids_from_device_description(Platform.SELECT, light, LIGHT_SELECTS[1]) light.__fields__["set_paired_camera"] = Mock() light.set_paired_camera = AsyncMock() camera = list(light.api.bootstrap.cameras.values())[0] await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: camera.name}, blocking=True, ) light.set_paired_camera.assert_called_once_with(camera) await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Not Paired"}, blocking=True, ) light.set_paired_camera.assert_called_with(None) async def test_select_set_option_camera_recording( hass: HomeAssistant, camera: Camera, ): """Test Recording Mode select.""" _, entity_id = ids_from_device_description( Platform.SELECT, camera, CAMERA_SELECTS[0] ) camera.__fields__["set_recording_mode"] = Mock() camera.set_recording_mode = AsyncMock() await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Never"}, blocking=True, ) camera.set_recording_mode.assert_called_once_with(RecordingMode.NEVER) async def test_select_set_option_camera_ir( hass: HomeAssistant, camera: Camera, ): """Test Infrared Mode select.""" _, entity_id = ids_from_device_description( Platform.SELECT, camera, CAMERA_SELECTS[1] ) camera.__fields__["set_ir_led_model"] = Mock() camera.set_ir_led_model = AsyncMock() await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Always Enable"}, blocking=True, ) camera.set_ir_led_model.assert_called_once_with(IRLEDMode.ON) async def test_select_set_option_camera_doorbell_custom( hass: HomeAssistant, camera: Camera, ): """Test Doorbell Text select (user defined message).""" _, entity_id = ids_from_device_description( Platform.SELECT, camera, CAMERA_SELECTS[2] ) camera.__fields__["set_lcd_text"] = Mock() camera.set_lcd_text = AsyncMock() await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Test"}, blocking=True, ) camera.set_lcd_text.assert_called_once_with( DoorbellMessageType.CUSTOM_MESSAGE, text="Test" ) async def test_select_set_option_camera_doorbell_unifi( hass: HomeAssistant, camera: Camera, ): """Test Doorbell Text select (unifi message).""" _, entity_id = ids_from_device_description( Platform.SELECT, camera, CAMERA_SELECTS[2] ) camera.__fields__["set_lcd_text"] = Mock() camera.set_lcd_text = AsyncMock() await hass.services.async_call( "select", "select_option", { ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "LEAVE PACKAGE AT DOOR", }, blocking=True, ) camera.set_lcd_text.assert_called_once_with( DoorbellMessageType.LEAVE_PACKAGE_AT_DOOR ) await hass.services.async_call( "select", "select_option", { ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Default Message (Welcome)", }, blocking=True, ) camera.set_lcd_text.assert_called_with(None) async def test_select_set_option_camera_doorbell_default( hass: HomeAssistant, camera: Camera, ): """Test Doorbell Text select (default message).""" _, entity_id = ids_from_device_description( Platform.SELECT, camera, CAMERA_SELECTS[2] ) camera.__fields__["set_lcd_text"] = Mock() camera.set_lcd_text = AsyncMock() await hass.services.async_call( "select", "select_option", { ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Default Message (Welcome)", }, blocking=True, ) camera.set_lcd_text.assert_called_once_with(None) async def test_select_set_option_viewer( hass: HomeAssistant, viewer: Viewer, ): """Test Liveview select.""" _, entity_id = ids_from_device_description( Platform.SELECT, viewer, VIEWER_SELECTS[0] ) viewer.__fields__["set_liveview"] = Mock() viewer.set_liveview = AsyncMock() liveview = list(viewer.api.bootstrap.liveviews.values())[0] await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: liveview.name}, blocking=True, ) viewer.set_liveview.assert_called_once_with(liveview) async def test_select_service_doorbell_invalid( hass: HomeAssistant, camera: Camera, ): """Test Doorbell Text service (invalid).""" _, entity_id = ids_from_device_description( Platform.SELECT, camera, CAMERA_SELECTS[1] ) camera.__fields__["set_lcd_text"] = Mock() camera.set_lcd_text = AsyncMock() with pytest.raises(HomeAssistantError): await hass.services.async_call( "unifiprotect", SERVICE_SET_DOORBELL_MESSAGE, {ATTR_ENTITY_ID: entity_id, ATTR_MESSAGE: "Test"}, blocking=True, ) assert not camera.set_lcd_text.called async def test_select_service_doorbell_success( hass: HomeAssistant, camera: Camera, ): """Test Doorbell Text service (success).""" _, entity_id = ids_from_device_description( Platform.SELECT, camera, CAMERA_SELECTS[2] ) camera.__fields__["set_lcd_text"] = Mock() camera.set_lcd_text = AsyncMock() await hass.services.async_call( "unifiprotect", SERVICE_SET_DOORBELL_MESSAGE, { ATTR_ENTITY_ID: entity_id, ATTR_MESSAGE: "Test", }, blocking=True, ) camera.set_lcd_text.assert_called_once_with( DoorbellMessageType.CUSTOM_MESSAGE, "Test", reset_at=None ) @patch("homeassistant.components.unifiprotect.select.utcnow") async def test_select_service_doorbell_with_reset( mock_now, hass: HomeAssistant, camera: Camera, ): """Test Doorbell Text service (success with reset time).""" now = utcnow() mock_now.return_value = now _, entity_id = ids_from_device_description( Platform.SELECT, camera, CAMERA_SELECTS[2] ) camera.__fields__["set_lcd_text"] = Mock() camera.set_lcd_text = AsyncMock() await hass.services.async_call( "unifiprotect", SERVICE_SET_DOORBELL_MESSAGE, { ATTR_ENTITY_ID: entity_id, ATTR_MESSAGE: "Test", ATTR_DURATION: 60, }, blocking=True, ) camera.set_lcd_text.assert_called_once_with( DoorbellMessageType.CUSTOM_MESSAGE, "Test", reset_at=now + timedelta(minutes=60), )