"""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, DoorbellMessageType, IRLEDMode, LCDMessage, Light, LightModeEnableType, LightModeType, Liveview, RecordingMode, Viewer, ) from pyunifiprotect.data.nvr import DoorbellMessage 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), )