"""Test the UniFi Protect switch platform.""" from unittest.mock import AsyncMock, Mock, call import pytest from uiprotect.data import ( Camera, Light, Permission, PublicHdrMode, RecordingMode, SmartDetectAudioType, SmartDetectObjectType, VideoMode, ) from uiprotect.exceptions import ClientError, NotAuthorized from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.switch import ( ATTR_PREV_MIC, ATTR_PREV_RECORD, CAMERA_SWITCHES, LIGHT_SWITCHES, PRIVACY_MODE_SWITCH, ProtectSwitchEntityDescription, ) from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, STATE_OFF, 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, enable_entity, ids_from_device_description, init_entry, remove_entities, ) CAMERA_SWITCHES_BASIC = [ d for d in CAMERA_SWITCHES if ( not d.translation_key.startswith("detections_") and d.key not in {"ssh", "color_night_vision", "track_person", "hdr_mode"} ) or d.key in { "detections_motion", "detections_person", "detections_vehicle", "detections_animal", } ] CAMERA_SWITCHES_NO_EXTRA = [ d for d in CAMERA_SWITCHES_BASIC if d.key not in ("high_fps", "privacy_mode", "hdr_mode") ] async def test_switch_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.SWITCH, 17, 15) await remove_entities(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.SWITCH, 2, 2) await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) assert_entity_counts(hass, Platform.SWITCH, 17, 15) async def test_switch_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.SWITCH, 4, 3) await remove_entities(hass, ufp, [light]) assert_entity_counts(hass, Platform.SWITCH, 2, 2) await adopt_devices(hass, ufp, [light]) assert_entity_counts(hass, Platform.SWITCH, 4, 3) async def test_switch_nvr(hass: HomeAssistant, ufp: MockUFPFixture) -> None: """Test switch entity setup for light devices.""" await init_entry(hass, ufp, []) assert_entity_counts(hass, Platform.SWITCH, 2, 2) nvr = ufp.api.bootstrap.nvr entity_id = "switch.unifiprotect_insights_enabled" with patch_ufp_method(nvr, "set_insights", new_callable=AsyncMock) as mock_method: await hass.services.async_call( "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) mock_method.assert_called_once_with(True) await hass.services.async_call( "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) mock_method.assert_called_with(False) async def test_switch_setup_no_perm( hass: HomeAssistant, ufp: MockUFPFixture, light: Light, doorbell: Camera, ) -> None: """Test switch entity setup for light devices.""" ufp.api.bootstrap.auth_user.all_permissions = [ Permission.unifi_dict_to_dict({"rawPermission": "light:read:*"}) ] await init_entry(hass, ufp, [light, doorbell]) assert_entity_counts(hass, Platform.SWITCH, 0, 0) async def test_switch_setup_light( hass: HomeAssistant, entity_registry: er.EntityRegistry, ufp: MockUFPFixture, light: Light, ) -> None: """Test switch entity setup for light devices.""" await init_entry(hass, ufp, [light]) assert_entity_counts(hass, Platform.SWITCH, 4, 3) description = LIGHT_SWITCHES[1] unique_id, entity_id = await ids_from_device_description( hass, Platform.SWITCH, 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 == STATE_OFF assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION description = LIGHT_SWITCHES[0] unique_id = f"{light.mac}_{description.key}" entity_id = f"switch.test_light_{description.translation_key}" entity = entity_registry.async_get(entity_id) assert entity assert entity.disabled is True assert entity.unique_id == unique_id await enable_entity(hass, ufp.entry.entry_id, entity_id) state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION async def test_switch_setup_camera_all( hass: HomeAssistant, entity_registry: er.EntityRegistry, ufp: MockUFPFixture, doorbell: Camera, ) -> None: """Test switch entity setup for camera devices (all enabled feature flags).""" await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SWITCH, 17, 15) for description in CAMERA_SWITCHES_BASIC: unique_id, entity_id = await ids_from_device_description( hass, Platform.SWITCH, 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 == STATE_OFF assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION description = CAMERA_SWITCHES[0] unique_id = f"{doorbell.mac}_{description.key}" entity_id = f"switch.test_camera_{description.translation_key}" entity = entity_registry.async_get(entity_id) assert entity assert entity.disabled is True assert entity.unique_id == unique_id await enable_entity(hass, ufp.entry.entry_id, entity_id) state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION async def test_switch_setup_camera_none( hass: HomeAssistant, entity_registry: er.EntityRegistry, ufp: MockUFPFixture, camera: Camera, ) -> None: """Test switch entity setup for camera devices (no enabled feature flags).""" await init_entry(hass, ufp, [camera]) assert_entity_counts(hass, Platform.SWITCH, 8, 7) for description in CAMERA_SWITCHES_BASIC: if description.ufp_required_field is not None: continue unique_id, entity_id = await ids_from_device_description( hass, Platform.SWITCH, 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 == STATE_OFF assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION description = CAMERA_SWITCHES[0] unique_id = f"{camera.mac}_{description.key}" entity_id = f"switch.test_camera_{description.translation_key}" entity = entity_registry.async_get(entity_id) assert entity assert entity.disabled is True assert entity.unique_id == unique_id await enable_entity(hass, ufp.entry.entry_id, entity_id) state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION async def test_switch_light_status( hass: HomeAssistant, ufp: MockUFPFixture, light: Light ) -> None: """Tests status light switch for lights.""" await init_entry(hass, ufp, [light]) assert_entity_counts(hass, Platform.SWITCH, 4, 3) description = LIGHT_SWITCHES[1] _, entity_id = await ids_from_device_description( hass, Platform.SWITCH, light, description ) with patch_ufp_method( light, "set_status_light", new_callable=AsyncMock ) as mock_method: await hass.services.async_call( "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) mock_method.assert_called_once_with(True) await hass.services.async_call( "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) mock_method.assert_called_with(False) async def test_switch_camera_ssh( hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ) -> None: """Tests SSH switch for cameras.""" await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SWITCH, 17, 15) description = CAMERA_SWITCHES[0] _, entity_id = await ids_from_device_description( hass, Platform.SWITCH, doorbell, description ) await enable_entity(hass, ufp.entry.entry_id, entity_id) with patch_ufp_method(doorbell, "set_ssh", new_callable=AsyncMock) as mock_method: await hass.services.async_call( "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) mock_method.assert_called_once_with(True) await hass.services.async_call( "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) mock_method.assert_called_with(False) @pytest.mark.parametrize("description", CAMERA_SWITCHES_NO_EXTRA) async def test_switch_camera_simple( hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, description: ProtectSwitchEntityDescription, ) -> None: """Tests all simple switches for cameras.""" await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SWITCH, 17, 15) assert description.ufp_set_method is not None with patch_ufp_method( doorbell, description.ufp_set_method, new_callable=AsyncMock ) as mock_method: _, entity_id = await ids_from_device_description( hass, Platform.SWITCH, doorbell, description ) await hass.services.async_call( "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) mock_method.assert_called_once_with(True) await hass.services.async_call( "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) mock_method.assert_called_with(False) async def test_switch_camera_highfps( hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ) -> None: """Tests High FPS switch for cameras.""" await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SWITCH, 17, 15) description = CAMERA_SWITCHES[3] _, entity_id = await ids_from_device_description( hass, Platform.SWITCH, doorbell, description ) with patch_ufp_method( doorbell, "set_video_mode_public", new_callable=AsyncMock ) as mock_method: await hass.services.async_call( "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) mock_method.assert_called_once_with(VideoMode.HIGH_FPS) await hass.services.async_call( "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) mock_method.assert_called_with(VideoMode.DEFAULT) CAMERA_SWITCHES_DETECTIONS_EXTRA = [ d for d in CAMERA_SWITCHES if d.translation_key.startswith("detections_") and d.key not in { "detections_motion", "detections_person", "detections_vehicle", "detections_animal", } ] async def test_switch_camera_hdr( hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ) -> None: """Tests HDR mode switch uses the public API helper.""" await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SWITCH, 17, 15) description = next(d for d in CAMERA_SWITCHES if d.key == "hdr_mode") _, entity_id = await ids_from_device_description( hass, Platform.SWITCH, doorbell, description ) await enable_entity(hass, ufp.entry.entry_id, entity_id) with patch_ufp_method( doorbell, "set_hdr_mode_public", new_callable=AsyncMock ) as mock_method: await hass.services.async_call( "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) await hass.services.async_call( "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) mock_method.assert_has_calls( [call(PublicHdrMode.AUTO), call(PublicHdrMode.OFF)] ) assert mock_method.call_count == 2 @pytest.mark.parametrize("description", CAMERA_SWITCHES_DETECTIONS_EXTRA) async def test_switch_camera_detections_public_api( hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, description: ProtectSwitchEntityDescription, ) -> None: """Tests detection switches call the public API setters.""" doorbell.feature_flags.smart_detect_types = [ SmartDetectObjectType.PERSON, SmartDetectObjectType.VEHICLE, SmartDetectObjectType.ANIMAL, SmartDetectObjectType.PACKAGE, SmartDetectObjectType.LICENSE_PLATE, ] doorbell.feature_flags.smart_detect_audio_types = [ SmartDetectAudioType.SMOKE, SmartDetectAudioType.CMONX, SmartDetectAudioType.SIREN, SmartDetectAudioType.BABY_CRY, SmartDetectAudioType.SPEAK, SmartDetectAudioType.BARK, SmartDetectAudioType.BURGLAR, SmartDetectAudioType.CAR_HORN, SmartDetectAudioType.GLASS_BREAK, ] await init_entry(hass, ufp, [doorbell]) assert description.ufp_set_method is not None assert description.ufp_set_method.endswith("_public") _, entity_id = await ids_from_device_description( hass, Platform.SWITCH, doorbell, description ) with patch_ufp_method( doorbell, description.ufp_set_method, new_callable=AsyncMock ) as mock_method: await hass.services.async_call( "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) await hass.services.async_call( "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) mock_method.assert_has_calls([call(True), call(False)]) assert mock_method.call_count == 2 async def test_switch_camera_privacy( hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ) -> None: """Tests Privacy Mode switch for cameras with privacy mode defaulted on.""" previous_mic = doorbell.mic_volume = 53 previous_record = doorbell.recording_settings.mode = RecordingMode.DETECTIONS await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SWITCH, 17, 15) description = PRIVACY_MODE_SWITCH _, entity_id = await ids_from_device_description( hass, Platform.SWITCH, doorbell, description ) state = hass.states.get(entity_id) assert state and state.state == "off" assert ATTR_PREV_MIC not in state.attributes assert ATTR_PREV_RECORD not in state.attributes with patch_ufp_method( doorbell, "set_privacy", new_callable=AsyncMock ) as mock_set_privacy: await hass.services.async_call( "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) mock_set_privacy.assert_called_with(True, 0, RecordingMode.NEVER) new_doorbell = doorbell.model_copy() new_doorbell.add_privacy_zone() new_doorbell.mic_volume = 0 new_doorbell.recording_settings.mode = RecordingMode.NEVER ufp.api.bootstrap.cameras = {new_doorbell.id: new_doorbell} mock_msg = Mock() mock_msg.changed_data = {} mock_msg.new_obj = new_doorbell ufp.ws_msg(mock_msg) state = hass.states.get(entity_id) assert state and state.state == "on" assert state.attributes[ATTR_PREV_MIC] == previous_mic assert state.attributes[ATTR_PREV_RECORD] == previous_record.value mock_set_privacy.reset_mock() await hass.services.async_call( "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) mock_set_privacy.assert_called_with(False, previous_mic, previous_record) async def test_switch_camera_privacy_already_on( hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ) -> None: """Tests Privacy Mode switch for cameras with privacy mode defaulted on.""" doorbell.add_privacy_zone() await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SWITCH, 17, 15) description = PRIVACY_MODE_SWITCH _, entity_id = await ids_from_device_description( hass, Platform.SWITCH, doorbell, description ) with patch_ufp_method( doorbell, "set_privacy", new_callable=AsyncMock ) as mock_set_privacy: await hass.services.async_call( "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) mock_set_privacy.assert_called_once_with(False, 100, RecordingMode.ALWAYS) async def test_switch_turn_on_client_error( hass: HomeAssistant, ufp: MockUFPFixture, light: Light ) -> None: """Test switch turn on with ClientError raises HomeAssistantError.""" await init_entry(hass, ufp, [light]) description = LIGHT_SWITCHES[1] _, entity_id = await ids_from_device_description( hass, Platform.SWITCH, light, description ) with ( patch_ufp_method( light, "set_status_light", new_callable=AsyncMock, side_effect=ClientError("Test error"), ), pytest.raises(HomeAssistantError), ): await hass.services.async_call( "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) async def test_switch_turn_on_not_authorized( hass: HomeAssistant, ufp: MockUFPFixture, light: Light ) -> None: """Test switch turn on with NotAuthorized raises HomeAssistantError.""" await init_entry(hass, ufp, [light]) description = LIGHT_SWITCHES[1] _, entity_id = await ids_from_device_description( hass, Platform.SWITCH, light, description ) with ( patch_ufp_method( light, "set_status_light", new_callable=AsyncMock, side_effect=NotAuthorized("Not authorized"), ), pytest.raises(HomeAssistantError), ): await hass.services.async_call( "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True )