"""Test the UniFi Protect select platform.""" from copy import copy from unittest.mock import AsyncMock, Mock import pytest from uiprotect.data import ( NVR, ArmProfile, Camera, DoorbellMessageType, IRLEDMode, LCDMessage, Light, LightModeEnableType, LightModeType, Liveview, NvrArmMode, NvrArmModeStatus, PTZPatrol, PublicBootstrap, PublicHdrMode, RecordingMode, Viewer, ) from uiprotect.data.nvr import DoorbellMessage from uiprotect.exceptions import GlobalAlarmManagerError from homeassistant.components.select import ATTR_OPTIONS from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.select import ( CAMERA_SELECTS, LIGHT_MODE_OFF, LIGHT_SELECTS, PTZ_PATROL_STOP, 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 . import patch_ufp_method from .utils import ( MockUFPFixture, adopt_devices, assert_entity_counts, ids_from_device_description, init_entry, remove_entities, ) async def test_select_camera_remove( hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, unadopted_camera: Camera ) -> None: """Test removing and re-adding a camera device.""" ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.SELECT, 5, 5) await remove_entities(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.SELECT, 0, 0) await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.SELECT, 5, 5) async def test_select_light_remove( hass: HomeAssistant, ufp: MockUFPFixture, light: Light ) -> None: """Test removing and re-adding a light device.""" ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [light]) assert_entity_counts(hass, Platform.SELECT, 2, 2) await remove_entities(hass, ufp, [light]) assert_entity_counts(hass, Platform.SELECT, 0, 0) await adopt_devices(hass, ufp, [light]) assert_entity_counts(hass, Platform.SELECT, 2, 2) async def test_select_viewer_remove( hass: HomeAssistant, ufp: MockUFPFixture, viewer: Viewer ) -> None: """Test removing and re-adding a light device.""" ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [viewer]) assert_entity_counts(hass, Platform.SELECT, 1, 1) await remove_entities(hass, ufp, [viewer]) assert_entity_counts(hass, Platform.SELECT, 0, 0) await adopt_devices(hass, ufp, [viewer]) assert_entity_counts(hass, Platform.SELECT, 1, 1) async def test_select_setup_light( hass: HomeAssistant, entity_registry: er.EntityRegistry, ufp: MockUFPFixture, light: Light, ) -> None: """Test select entity setup for light devices.""" light.light_mode_settings.enable_at = LightModeEnableType.DARK await init_entry(hass, ufp, [light]) assert_entity_counts(hass, Platform.SELECT, 2, 2) expected_values = ("motion_dark", "Not Paired") for index, description in enumerate(LIGHT_SELECTS): unique_id, entity_id = await ids_from_device_description( hass, 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, entity_registry: er.EntityRegistry, ufp: MockUFPFixture, viewer: Viewer, liveview: Liveview, ) -> None: """Test select entity setup for light devices.""" ufp.api.bootstrap.liveviews = {liveview.id: liveview} await init_entry(hass, ufp, [viewer]) assert_entity_counts(hass, Platform.SELECT, 1, 1) description = VIEWER_SELECTS[0] unique_id, entity_id = await ids_from_device_description( hass, 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, entity_registry: er.EntityRegistry, ufp: MockUFPFixture, doorbell: Camera, ) -> None: """Test select entity setup for camera devices (all features).""" await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SELECT, 5, 5) expected_values = ( "always", "auto", "Default Message (Welcome)", "none", "off", ) for index, description in enumerate(CAMERA_SELECTS): unique_id, entity_id = await ids_from_device_description( hass, Platform.SELECT, doorbell, 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, entity_registry: er.EntityRegistry, ufp: MockUFPFixture, camera: Camera, ) -> None: """Test select entity setup for camera devices (no features).""" await init_entry(hass, ufp, [camera]) assert_entity_counts(hass, Platform.SELECT, 2, 2) expected_values = ("always", "auto", "Default Message (Welcome)") for index, description in enumerate(CAMERA_SELECTS): if index == 2: return unique_id, entity_id = await ids_from_device_description( hass, 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_update_liveview( hass: HomeAssistant, ufp: MockUFPFixture, viewer: Viewer, liveview: Liveview ) -> None: """Test select entity update (new Liveview).""" ufp.api.bootstrap.liveviews = {liveview.id: liveview} await init_entry(hass, ufp, [viewer]) assert_entity_counts(hass, Platform.SELECT, 1, 1) _, entity_id = await ids_from_device_description( hass, Platform.SELECT, viewer, VIEWER_SELECTS[0] ) state = hass.states.get(entity_id) assert state expected_options = state.attributes[ATTR_OPTIONS] new_liveview = copy(liveview) new_liveview.id = "test_id" mock_msg = Mock() mock_msg.changed_data = {} mock_msg.new_obj = new_liveview ufp.api.bootstrap.liveviews = { **ufp.api.bootstrap.liveviews, new_liveview.id: new_liveview, } ufp.ws_msg(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, ufp: MockUFPFixture, doorbell: Camera ) -> None: """Test select entity update (new Doorbell Message).""" await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SELECT, 5, 5) expected_length = len(ufp.api.bootstrap.nvr.doorbell_settings.all_messages) + 1 _, entity_id = await ids_from_device_description( hass, Platform.SELECT, doorbell, 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(ufp.api.bootstrap.nvr) 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 with patch_ufp_method(new_nvr, "update_all_messages") as mock_method: ufp.api.bootstrap.nvr = new_nvr ufp.ws_msg(mock_msg) await hass.async_block_till_done() mock_method.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, ufp: MockUFPFixture, doorbell: Camera ) -> None: """Test select entity update (change doorbell message).""" await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SELECT, 5, 5) _, entity_id = await ids_from_device_description( hass, Platform.SELECT, doorbell, CAMERA_SELECTS[2] ) state = hass.states.get(entity_id) assert state assert state.state == "Default Message (Welcome)" new_camera = doorbell.model_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 ufp.api.bootstrap.cameras = {new_camera.id: new_camera} ufp.ws_msg(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, ufp: MockUFPFixture, light: Light ) -> None: """Test Light Mode select.""" await init_entry(hass, ufp, [light]) assert_entity_counts(hass, Platform.SELECT, 2, 2) _, entity_id = await ids_from_device_description( hass, Platform.SELECT, light, LIGHT_SELECTS[0] ) with patch_ufp_method( light, "set_light_settings", new_callable=AsyncMock ) as mock_method: await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: LIGHT_MODE_OFF}, blocking=True, ) mock_method.assert_called_once_with(LightModeType.MANUAL, enable_at=None) async def test_select_set_option_light_camera( hass: HomeAssistant, ufp: MockUFPFixture, light: Light, camera: Camera ) -> None: """Test Paired Camera select.""" await init_entry(hass, ufp, [light, camera]) assert_entity_counts(hass, Platform.SELECT, 4, 4) _, entity_id = await ids_from_device_description( hass, Platform.SELECT, light, LIGHT_SELECTS[1] ) camera = list(light.api.bootstrap.cameras.values())[0] with patch_ufp_method( light, "set_paired_camera", new_callable=AsyncMock ) as mock_method: await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: camera.name}, blocking=True, ) mock_method.assert_called_once_with(camera) await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Not Paired"}, blocking=True, ) mock_method.assert_called_with(None) async def test_select_set_option_camera_recording( hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ) -> None: """Test Recording Mode select.""" await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SELECT, 5, 5) _, entity_id = await ids_from_device_description( hass, Platform.SELECT, doorbell, CAMERA_SELECTS[0] ) with patch_ufp_method( doorbell, "set_recording_mode", new_callable=AsyncMock ) as mock_method: await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "never"}, blocking=True, ) mock_method.assert_called_once_with(RecordingMode.NEVER) async def test_select_set_option_camera_ir( hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ) -> None: """Test Infrared Mode select.""" await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SELECT, 5, 5) _, entity_id = await ids_from_device_description( hass, Platform.SELECT, doorbell, CAMERA_SELECTS[1] ) with patch_ufp_method( doorbell, "set_ir_led_model", new_callable=AsyncMock ) as mock_method: await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "on"}, blocking=True, ) mock_method.assert_called_once_with(IRLEDMode.ON) async def test_select_set_option_camera_doorbell_custom( hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ) -> None: """Test Doorbell Text select (user defined message).""" await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SELECT, 5, 5) _, entity_id = await ids_from_device_description( hass, Platform.SELECT, doorbell, CAMERA_SELECTS[2] ) with patch_ufp_method( doorbell, "set_lcd_message_public", new_callable=AsyncMock ) as mock_method: await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Test"}, blocking=True, ) mock_method.assert_called_once_with( DoorbellMessageType.CUSTOM_MESSAGE, text="Test" ) async def test_select_set_option_camera_doorbell_unifi( hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ) -> None: """Test Doorbell Text select (unifi message).""" await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SELECT, 5, 5) _, entity_id = await ids_from_device_description( hass, Platform.SELECT, doorbell, CAMERA_SELECTS[2] ) with ( patch_ufp_method( doorbell, "set_lcd_message_public", new_callable=AsyncMock ) as mock_public, patch_ufp_method( doorbell, "set_lcd_text", new_callable=AsyncMock ) as mock_legacy, ): await hass.services.async_call( "select", "select_option", { ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "LEAVE PACKAGE AT DOOR", }, blocking=True, ) mock_public.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, ) mock_legacy.assert_called_once_with(None) async def test_select_set_option_camera_doorbell_default( hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ) -> None: """Test Doorbell Text select (default message).""" await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SELECT, 5, 5) _, entity_id = await ids_from_device_description( hass, Platform.SELECT, doorbell, CAMERA_SELECTS[2] ) with patch_ufp_method( doorbell, "set_lcd_text", new_callable=AsyncMock ) as mock_method: await hass.services.async_call( "select", "select_option", { ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Default Message (Welcome)", }, blocking=True, ) mock_method.assert_called_once_with(None) @pytest.mark.parametrize( ("option", "expected"), [ pytest.param("auto", PublicHdrMode.AUTO, id="auto"), pytest.param("always", PublicHdrMode.ON, id="always"), pytest.param("off", PublicHdrMode.OFF, id="off"), ], ) async def test_select_set_option_camera_hdr_mode( hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, option: str, expected: PublicHdrMode, ) -> None: """Test HDR mode select calls public API with mapped value.""" await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SELECT, 5, 5) description = next(d for d in CAMERA_SELECTS if d.key == "hdr_mode") _, entity_id = await ids_from_device_description( hass, Platform.SELECT, doorbell, description ) with patch_ufp_method( doorbell, "set_hdr_mode_public", new_callable=AsyncMock ) as mock_method: await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: option}, blocking=True, ) mock_method.assert_called_once_with(expected) async def test_select_set_option_viewer( hass: HomeAssistant, ufp: MockUFPFixture, viewer: Viewer, liveview: Liveview ) -> None: """Test Liveview select.""" ufp.api.bootstrap.liveviews = {liveview.id: liveview} await init_entry(hass, ufp, [viewer]) assert_entity_counts(hass, Platform.SELECT, 1, 1) _, entity_id = await ids_from_device_description( hass, Platform.SELECT, viewer, VIEWER_SELECTS[0] ) liveview = list(viewer.api.bootstrap.liveviews.values())[0] with patch_ufp_method( viewer, "set_liveview", new_callable=AsyncMock ) as mock_method: await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: liveview.name}, blocking=True, ) mock_method.assert_called_once_with(liveview) # --- PTZ Patrol Test Helpers --- def _get_ptz_entity_id(hass: HomeAssistant, camera: Camera, key: str) -> str | None: """Get PTZ entity ID by unique_id from entity registry.""" entity_registry = er.async_get(hass) unique_id = f"{camera.mac}_{key}" return entity_registry.async_get_entity_id( Platform.SELECT, "unifiprotect", unique_id ) def _make_patrols(camera_id: str) -> list[PTZPatrol]: """Create mock PTZ patrols.""" return [ PTZPatrol( id="patrol1", name="Patrol 1", slot=0, presets=[0, 1], presetDurationSeconds=10, camera=camera_id, ), PTZPatrol( id="patrol2", name="Patrol 2", slot=1, presets=[0], presetDurationSeconds=5, camera=camera_id, ), ] async def _setup_ptz_camera( hass: HomeAssistant, ufp: MockUFPFixture, ptz_camera: Camera, *, patrols: list[PTZPatrol] | None = None, ) -> None: """Set up PTZ camera with mocked patrols.""" ptz_camera.get_ptz_patrols.return_value = patrols or [] ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, [ptz_camera]) # --- PTZ Patrol Tests --- async def test_select_ptz_patrol_setup( hass: HomeAssistant, ufp: MockUFPFixture, ptz_camera: Camera ) -> None: """Test PTZ patrol select entity setup.""" await _setup_ptz_camera(hass, ufp, ptz_camera, patrols=_make_patrols(ptz_camera.id)) # PTZ camera should have 1 additional select entity (patrol) # Regular camera has 2 (recording_mode, infrared_mode), PTZ has 2 + 1 = 3 assert_entity_counts(hass, Platform.SELECT, 3, 3) entity_id = _get_ptz_entity_id(hass, ptz_camera, "ptz_patrol") assert entity_id is not None state = hass.states.get(entity_id) assert state is not None assert state.state == PTZ_PATROL_STOP options = state.attributes.get(ATTR_OPTIONS, []) assert options == ["stop", "Patrol 1", "Patrol 2"] async def test_select_ptz_patrol_start( hass: HomeAssistant, ufp: MockUFPFixture, ptz_camera: Camera ) -> None: """Test starting a PTZ patrol.""" await _setup_ptz_camera( hass, ufp, ptz_camera, patrols=_make_patrols(ptz_camera.id)[:1] ) entity_id = _get_ptz_entity_id(hass, ptz_camera, "ptz_patrol") assert entity_id is not None with patch_ufp_method( ptz_camera, "ptz_patrol_start_public", new_callable=AsyncMock ) as mock_method: await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "Patrol 1"}, blocking=True, ) mock_method.assert_called_once_with(slot=0) async def test_select_ptz_patrol_stop( hass: HomeAssistant, ufp: MockUFPFixture, ptz_camera: Camera ) -> None: """Test stopping a PTZ patrol.""" await _setup_ptz_camera( hass, ufp, ptz_camera, patrols=_make_patrols(ptz_camera.id)[:1] ) entity_id = _get_ptz_entity_id(hass, ptz_camera, "ptz_patrol") assert entity_id is not None with patch_ufp_method( ptz_camera, "ptz_patrol_stop_public", new_callable=AsyncMock ) as mock_method: await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: entity_id, ATTR_OPTION: "stop"}, blocking=True, ) mock_method.assert_called_once() async def test_select_ptz_patrol_active_state( hass: HomeAssistant, ufp: MockUFPFixture, ptz_camera: Camera ) -> None: """Test PTZ patrol shows active patrol from device state.""" patrols = _make_patrols(ptz_camera.id) ptz_camera.active_patrol_slot = 0 await _setup_ptz_camera(hass, ufp, ptz_camera, patrols=patrols) entity_id = _get_ptz_entity_id(hass, ptz_camera, "ptz_patrol") assert entity_id is not None state = hass.states.get(entity_id) assert state is not None assert state.state == "Patrol 1" async def test_select_ptz_patrol_websocket_update( hass: HomeAssistant, ufp: MockUFPFixture, ptz_camera: Camera ) -> None: """Test PTZ patrol state updates via websocket.""" patrols = _make_patrols(ptz_camera.id) await _setup_ptz_camera(hass, ufp, ptz_camera, patrols=patrols) entity_id = _get_ptz_entity_id(hass, ptz_camera, "ptz_patrol") assert entity_id is not None # Initially stopped state = hass.states.get(entity_id) assert state is not None assert state.state == PTZ_PATROL_STOP # Simulate websocket update: patrol starts new_camera = ptz_camera.model_copy() new_camera.active_patrol_slot = 1 mock_msg = Mock() mock_msg.changed_data = {} mock_msg.new_obj = new_camera ufp.api.bootstrap.cameras = {new_camera.id: new_camera} ufp.ws_msg(mock_msg) await hass.async_block_till_done() state = hass.states.get(entity_id) assert state is not None assert state.state == "Patrol 2" # Simulate websocket update: patrol stops new_camera2 = ptz_camera.model_copy() new_camera2.active_patrol_slot = None mock_msg2 = Mock() mock_msg2.changed_data = {} mock_msg2.new_obj = new_camera2 ufp.api.bootstrap.cameras = {new_camera2.id: new_camera2} ufp.ws_msg(mock_msg2) await hass.async_block_till_done() state = hass.states.get(entity_id) assert state is not None assert state.state == PTZ_PATROL_STOP async def test_select_ptz_camera_adopt( hass: HomeAssistant, ufp: MockUFPFixture, ptz_camera: Camera ) -> None: """Test adopting a new PTZ camera creates patrol entity.""" ufp.api.bootstrap.nvr.system_info.ustorage = None await init_entry(hass, ufp, []) assert_entity_counts(hass, Platform.SELECT, 0, 0) ptz_camera._api = ufp.api for channel in ptz_camera.channels: channel._api = ufp.api ptz_camera.get_ptz_patrols.return_value = _make_patrols(ptz_camera.id) await adopt_devices(hass, ufp, [ptz_camera]) await hass.async_block_till_done() # Should have 2 regular camera selects + 1 patrol select = 3 assert_entity_counts(hass, Platform.SELECT, 3, 3) patrol_entity_id = _get_ptz_entity_id(hass, ptz_camera, "ptz_patrol") assert patrol_entity_id is not None # --- NVR Arm Profile Select Tests --- ARM_PROFILE_ENTITY_ID = "select.unifiprotect_alarm_profile" def _make_arm_profile(profile_id: str, name: str) -> Mock: """Create an ArmProfile mock for testing.""" profile = Mock(spec=ArmProfile) profile.id = profile_id profile.name = name return profile def _make_nvr_arm_mode(profile_id: str | None = None) -> Mock: """Create an NvrArmMode mock for testing.""" arm_mode = Mock(spec=NvrArmMode) arm_mode.status = NvrArmModeStatus.DISABLED arm_mode.arm_profile_id = profile_id return arm_mode def _make_public_bootstrap(arm_mode: Mock | None, profiles: dict[str, Mock]) -> Mock: """Create a PublicBootstrap mock with arm profiles for testing.""" pb = Mock(spec=PublicBootstrap) pb.arm_mode = arm_mode pb.arm_profiles = profiles pb.relays = {} pb.sirens = {} return pb async def test_select_nvr_arm_profile_not_created_without_public_bootstrap( hass: HomeAssistant, ufp: MockUFPFixture, ) -> None: """Arm profile select is NOT created when has_public_bootstrap is False.""" ufp.api.has_public_bootstrap = False await init_entry(hass, ufp, []) assert hass.states.get(ARM_PROFILE_ENTITY_ID) is None async def test_select_nvr_arm_profile_not_created_without_arm_mode( hass: HomeAssistant, ufp: MockUFPFixture, ) -> None: """Arm profile select is NOT created when arm_mode is None (old firmware).""" profile = _make_arm_profile("p1", "Home") pb = _make_public_bootstrap(arm_mode=None, profiles={"p1": profile}) ufp.api.has_public_bootstrap = True ufp.api.public_bootstrap = pb await init_entry(hass, ufp, []) assert hass.states.get(ARM_PROFILE_ENTITY_ID) is None async def test_select_nvr_arm_profile_not_created_without_profiles( hass: HomeAssistant, ufp: MockUFPFixture, ) -> None: """Arm profile select is NOT created when no arm profiles exist.""" arm_mode = _make_nvr_arm_mode() pb = _make_public_bootstrap(arm_mode=arm_mode, profiles={}) ufp.api.has_public_bootstrap = True ufp.api.public_bootstrap = pb await init_entry(hass, ufp, []) assert hass.states.get(ARM_PROFILE_ENTITY_ID) is None async def test_select_nvr_arm_profile_created( hass: HomeAssistant, entity_registry: er.EntityRegistry, ufp: MockUFPFixture, nvr: NVR, ) -> None: """Arm profile select IS created with correct options and current option.""" profile_home = _make_arm_profile("p1", "Home") profile_away = _make_arm_profile("p2", "Away") profiles = {"p1": profile_home, "p2": profile_away} arm_mode = _make_nvr_arm_mode(profile_id="p2") pb = _make_public_bootstrap(arm_mode=arm_mode, profiles=profiles) ufp.api.has_public_bootstrap = True ufp.api.public_bootstrap = pb await init_entry(hass, ufp, []) entity = entity_registry.async_get(ARM_PROFILE_ENTITY_ID) assert entity is not None assert entity.unique_id == f"{nvr.mac}_nvr_arm_profile" state = hass.states.get(ARM_PROFILE_ENTITY_ID) assert state is not None assert state.state == "Away (p2)" assert set(state.attributes[ATTR_OPTIONS]) == {"Home (p1)", "Away (p2)"} assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION async def test_select_nvr_arm_profile_duplicate_names( hass: HomeAssistant, ufp: MockUFPFixture, ) -> None: """Duplicate profile names are disambiguated with an id suffix.""" profile_a = _make_arm_profile("aaaaaa111111", "Home") profile_b = _make_arm_profile("bbbbbb222222", "Home") profile_c = _make_arm_profile("cccccc333333", "Away") profiles = {p.id: p for p in (profile_a, profile_b, profile_c)} arm_mode = _make_nvr_arm_mode(profile_id="aaaaaa111111") pb = _make_public_bootstrap(arm_mode=arm_mode, profiles=profiles) ufp.api.has_public_bootstrap = True ufp.api.public_bootstrap = pb await init_entry(hass, ufp, []) state = hass.states.get(ARM_PROFILE_ENTITY_ID) assert state is not None # All labels always include the last 6 chars of the id for stability. assert state.state == "Home (111111)" assert set(state.attributes[ATTR_OPTIONS]) == { "Home (111111)", "Home (222222)", "Away (333333)", } async def test_select_nvr_arm_profile_duplicate_names_select_option( hass: HomeAssistant, ufp: MockUFPFixture, ) -> None: """Selecting a disambiguated duplicate name maps back to the correct id.""" profile_a = _make_arm_profile("aaaaaa111111", "Home") profile_b = _make_arm_profile("bbbbbb222222", "Home") profiles = {p.id: p for p in (profile_a, profile_b)} arm_mode = _make_nvr_arm_mode(profile_id="aaaaaa111111") pb = _make_public_bootstrap(arm_mode=arm_mode, profiles=profiles) ufp.api.has_public_bootstrap = True ufp.api.public_bootstrap = pb ufp.api.set_current_arm_profile_public = AsyncMock() await init_entry(hass, ufp, []) await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: ARM_PROFILE_ENTITY_ID, ATTR_OPTION: "Home (222222)"}, blocking=True, ) ufp.api.set_current_arm_profile_public.assert_called_once_with("bbbbbb222222") async def test_select_nvr_arm_profile_select_option( hass: HomeAssistant, ufp: MockUFPFixture, ) -> None: """Selecting an arm profile calls set_current_arm_profile_public.""" profile_home = _make_arm_profile("p1", "Home") profile_away = _make_arm_profile("p2", "Away") profiles = {"p1": profile_home, "p2": profile_away} arm_mode = _make_nvr_arm_mode(profile_id="p1") pb = _make_public_bootstrap(arm_mode=arm_mode, profiles=profiles) ufp.api.has_public_bootstrap = True ufp.api.public_bootstrap = pb ufp.api.set_current_arm_profile_public = AsyncMock() await init_entry(hass, ufp, []) await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: ARM_PROFILE_ENTITY_ID, ATTR_OPTION: "Away (p2)"}, blocking=True, ) ufp.api.set_current_arm_profile_public.assert_called_once_with("p2") async def test_select_nvr_arm_profile_global_alarm_error( hass: HomeAssistant, ufp: MockUFPFixture, ) -> None: """GlobalAlarmManagerError on profile change raises HomeAssistantError.""" profile_home = _make_arm_profile("p1", "Home") profiles = {"p1": profile_home} arm_mode = _make_nvr_arm_mode(profile_id="p1") pb = _make_public_bootstrap(arm_mode=arm_mode, profiles=profiles) ufp.api.has_public_bootstrap = True ufp.api.public_bootstrap = pb ufp.api.set_current_arm_profile_public = AsyncMock( side_effect=GlobalAlarmManagerError() ) await init_entry(hass, ufp, []) with pytest.raises(HomeAssistantError) as exc_info: await hass.services.async_call( "select", "select_option", {ATTR_ENTITY_ID: ARM_PROFILE_ENTITY_ID, ATTR_OPTION: "Home (p1)"}, blocking=True, ) assert exc_info.value.translation_key == "global_alarm_manager" async def test_select_nvr_arm_profile_ws_update( hass: HomeAssistant, ufp: MockUFPFixture, nvr: NVR, ) -> None: """Arm profile select updates via the public devices websocket.""" profile_home = _make_arm_profile("p1", "Home") profile_away = _make_arm_profile("p2", "Away") profiles = {"p1": profile_home, "p2": profile_away} arm_mode = _make_nvr_arm_mode(profile_id="p1") pb = _make_public_bootstrap(arm_mode=arm_mode, profiles=profiles) ufp.api.has_public_bootstrap = True ufp.api.public_bootstrap = pb await init_entry(hass, ufp, []) state = hass.states.get(ARM_PROFILE_ENTITY_ID) assert state is not None assert state.state == "Home (p1)" # Simulate the NVR arm_profile_id changing via the public devices websocket arm_mode.arm_profile_id = "p2" mock_msg = Mock() mock_msg.changed_data = {} mock_msg.old_obj = nvr mock_msg.new_obj = nvr assert ufp.devices_ws_subscription is not None ufp.devices_ws_subscription(mock_msg) await hass.async_block_till_done() state = hass.states.get(ARM_PROFILE_ENTITY_ID) assert state is not None assert state.state == "Away (p2)"