From 0bb6113bfda69dc2c399df1907fe90392eb7febe Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Tue, 26 May 2026 21:41:25 +0200 Subject: [PATCH] Migrate more UniFi Protect entities to public API (#171785) Co-authored-by: RaHehl --- .../components/unifiprotect/number.py | 16 ++- .../components/unifiprotect/select.py | 25 +++- .../components/unifiprotect/strings.json | 2 +- .../components/unifiprotect/switch.py | 50 ++++---- tests/components/unifiprotect/test_number.py | 35 ++++++ tests/components/unifiprotect/test_select.py | 56 ++++++++- tests/components/unifiprotect/test_switch.py | 111 +++++++++++++++++- 7 files changed, 255 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index a3667b7ac81..7971b9b34ed 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -66,6 +66,14 @@ def _get_chime_duration(obj: Camera) -> int: return int(obj.chime_duration_seconds) +async def _set_chime_volume(obj: Chime, value: float) -> None: + """Set chime volume per paired camera via the public API.""" + level = int(value) + ring_settings = [setting.to_api_dict(volume=level) for setting in obj.ring_settings] + if ring_settings: + await obj.set_ring_settings_public(ring_settings) + + CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ProtectNumberEntityDescription( key="wdr_value", @@ -84,13 +92,13 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( translation_key="microphone_level", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=PERCENTAGE, - ufp_min=0, + ufp_min=1, ufp_max=100, ufp_step=1, ufp_required_field="has_mic", ufp_value="mic_volume", ufp_enabled="feature_flags.has_mic", - ufp_set_method="set_mic_volume", + ufp_set_method="set_mic_volume_public", ufp_perm=PermRequired.WRITE, ), ProtectNumberEntityDescription( @@ -221,7 +229,7 @@ DOORLOCK_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ) CHIME_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( - ProtectNumberEntityDescription( + ProtectNumberEntityDescription[Chime]( key="volume", translation_key="volume", entity_category=EntityCategory.CONFIG, @@ -230,7 +238,7 @@ CHIME_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ufp_max=100, ufp_step=1, ufp_value="volume", - ufp_set_method="set_volume", + ufp_set_method_fn=_set_chime_volume, ufp_perm=PermRequired.WRITE, ), ) diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index cc62077c812..c307c6ce5a9 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -21,6 +21,7 @@ from uiprotect.data import ( MountType, ProtectAdoptableDeviceModel, PTZPatrol, + PublicHdrMode, RecordingMode, Sensor, Viewer, @@ -184,11 +185,15 @@ async def _set_paired_camera(obj: Light | Sensor | Doorlock, camera_id: str) -> async def _set_doorbell_message(obj: Camera, message: str) -> None: if message.startswith(DoorbellMessageType.CUSTOM_MESSAGE.value): message = message.rsplit(":", maxsplit=1)[-1] - await obj.set_lcd_text(DoorbellMessageType.CUSTOM_MESSAGE, text=message) + await obj.set_lcd_message_public( + DoorbellMessageType.CUSTOM_MESSAGE, text=message + ) elif message == TYPE_EMPTY_VALUE: + # Public API has no endpoint to clear the LCD message; fall back to + # the non-deprecated legacy helper. await obj.set_lcd_text(None) else: - await obj.set_lcd_text(DoorbellMessageType(message)) + await obj.set_lcd_message_public(DoorbellMessageType(message)) async def _set_liveview(obj: Viewer, liveview_id: str) -> None: @@ -206,6 +211,18 @@ async def _set_ptz_patrol(obj: Camera, patrol_slot: str) -> None: await obj.ptz_patrol_start_public(slot=slot) +_HDR_MODE_MAP = { + "auto": PublicHdrMode.AUTO, + "always": PublicHdrMode.ON, + "off": PublicHdrMode.OFF, +} + + +async def _set_hdr_mode(obj: Camera, mode: str) -> None: + """Set HDR mode via the public API.""" + await obj.set_hdr_mode_public(_HDR_MODE_MAP[mode]) + + PTZ_PATROL_DESCRIPTION = ProtectSelectEntityDescription[Camera]( key=_KEY_PTZ_PATROL, translation_key="ptz_patrol", @@ -258,14 +275,14 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ufp_set_method="set_chime_type", ufp_perm=PermRequired.WRITE, ), - ProtectSelectEntityDescription( + ProtectSelectEntityDescription[Camera]( key="hdr_mode", translation_key="hdr_mode", entity_category=EntityCategory.CONFIG, ufp_required_field="feature_flags.has_hdr", ufp_options=HDR_MODES, ufp_value="hdr_mode_display", - ufp_set_method="set_hdr_mode", + ufp_set_method_fn=_set_hdr_mode, ufp_perm=PermRequired.WRITE, ), ) diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index 7668fb91a56..deec8671017 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -572,7 +572,7 @@ "detections_baby_cry": { "name": "[%key:component::unifiprotect::entity::binary_sensor::detections_baby_cry::name%]" }, - "detections_barking": { + "detections_bark": { "name": "[%key:component::unifiprotect::entity::binary_sensor::detections_barking::name%]" }, "detections_car_alarm": { diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 4abe0a0d615..52ae6167444 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -9,6 +9,7 @@ from uiprotect.data import ( Camera, ModelType, ProtectAdoptableDeviceModel, + PublicHdrMode, PublicRelayOutput, RecordingMode, Relay, @@ -53,7 +54,11 @@ class ProtectSwitchEntityDescription( async def _set_highfps(obj: Camera, value: bool) -> None: - await obj.set_video_mode(VideoMode.HIGH_FPS if value else VideoMode.DEFAULT) + await obj.set_video_mode_public(VideoMode.HIGH_FPS if value else VideoMode.DEFAULT) + + +async def _set_hdr(obj: Camera, value: bool) -> None: + await obj.set_hdr_mode_public(PublicHdrMode.AUTO if value else PublicHdrMode.OFF) CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( @@ -72,17 +77,17 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_required_field="feature_flags.has_led_status", ufp_value="led_settings.is_enabled", - ufp_set_method="set_status_light", + ufp_set_method="set_status_light_public", ufp_perm=PermRequired.WRITE, ), - ProtectSwitchEntityDescription( + ProtectSwitchEntityDescription[Camera]( key="hdr_mode", translation_key="hdr_mode", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, ufp_required_field="feature_flags.has_hdr", ufp_value="hdr_mode", - ufp_set_method="set_hdr", + ufp_set_method_fn=_set_hdr, ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription[Camera]( @@ -109,7 +114,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( translation_key="overlay_show_name", entity_category=EntityCategory.CONFIG, ufp_value="osd_settings.is_name_enabled", - ufp_set_method="set_osd_name", + ufp_set_method="set_osd_name_public", ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( @@ -117,7 +122,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( translation_key="overlay_show_date", entity_category=EntityCategory.CONFIG, ufp_value="osd_settings.is_date_enabled", - ufp_set_method="set_osd_date", + ufp_set_method="set_osd_date_public", ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( @@ -125,7 +130,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( translation_key="overlay_show_logo", entity_category=EntityCategory.CONFIG, ufp_value="osd_settings.is_logo_enabled", - ufp_set_method="set_osd_logo", + ufp_set_method="set_osd_logo_public", ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( @@ -133,7 +138,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( translation_key="overlay_show_nerd_mode", entity_category=EntityCategory.CONFIG, ufp_value="osd_settings.is_debug_enabled", - ufp_set_method="set_osd_bitrate", + ufp_set_method="set_osd_nerd_mode_public", ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( @@ -161,7 +166,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="can_detect_person", ufp_value="is_person_detection_on", ufp_enabled="is_recording_enabled", - ufp_set_method="set_person_detection", + ufp_set_method="set_person_detection_public", ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( @@ -171,7 +176,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="can_detect_vehicle", ufp_value="is_vehicle_detection_on", ufp_enabled="is_recording_enabled", - ufp_set_method="set_vehicle_detection", + ufp_set_method="set_vehicle_detection_public", ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( @@ -181,7 +186,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="can_detect_animal", ufp_value="is_animal_detection_on", ufp_enabled="is_recording_enabled", - ufp_set_method="set_animal_detection", + ufp_set_method="set_animal_detection_public", ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( @@ -191,7 +196,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="can_detect_package", ufp_value="is_package_detection_on", ufp_enabled="is_recording_enabled", - ufp_set_method="set_package_detection", + ufp_set_method="set_package_detection_public", ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( @@ -201,7 +206,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="can_detect_license_plate", ufp_value="is_license_plate_detection_on", ufp_enabled="is_recording_enabled", - ufp_set_method="set_license_plate_detection", + ufp_set_method="set_license_plate_detection_public", ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( @@ -211,7 +216,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="can_detect_smoke", ufp_value="is_smoke_detection_on", ufp_enabled="is_recording_enabled", - ufp_set_method="set_smoke_detection", + ufp_set_method="set_smoke_detection_public", ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( @@ -221,7 +226,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="can_detect_co", ufp_value="is_co_detection_on", ufp_enabled="is_recording_enabled", - ufp_set_method="set_cmonx_detection", + ufp_set_method="set_co_detection_public", ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( @@ -231,7 +236,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="can_detect_siren", ufp_value="is_siren_detection_on", ufp_enabled="is_recording_enabled", - ufp_set_method="set_siren_detection", + ufp_set_method="set_siren_detection_public", ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( @@ -241,7 +246,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="can_detect_baby_cry", ufp_value="is_baby_cry_detection_on", ufp_enabled="is_recording_enabled", - ufp_set_method="set_baby_cry_detection", + ufp_set_method="set_baby_cry_detection_public", ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( @@ -251,7 +256,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="can_detect_speaking", ufp_value="is_speaking_detection_on", ufp_enabled="is_recording_enabled", - ufp_set_method="set_speaking_detection", + ufp_set_method="set_speaking_detection_public", ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( @@ -261,7 +266,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="can_detect_bark", ufp_value="is_bark_detection_on", ufp_enabled="is_recording_enabled", - ufp_set_method="set_bark_detection", + ufp_set_method="set_bark_detection_public", ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( @@ -271,7 +276,8 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="can_detect_car_alarm", ufp_value="is_car_alarm_detection_on", ufp_enabled="is_recording_enabled", - ufp_set_method="set_car_alarm_detection", + # Public API renamed "car alarm" to "burglar"; internal model keeps the legacy name. + ufp_set_method="set_burglar_detection_public", ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( @@ -281,7 +287,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="can_detect_car_horn", ufp_value="is_car_horn_detection_on", ufp_enabled="is_recording_enabled", - ufp_set_method="set_car_horn_detection", + ufp_set_method="set_car_horn_detection_public", ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( @@ -291,7 +297,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="can_detect_glass_break", ufp_value="is_glass_break_detection_on", ufp_enabled="is_recording_enabled", - ufp_set_method="set_glass_break_detection", + ufp_set_method="set_glass_break_detection_public", ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( diff --git a/tests/components/unifiprotect/test_number.py b/tests/components/unifiprotect/test_number.py index 0d5039d52e5..7894c469b68 100644 --- a/tests/components/unifiprotect/test_number.py +++ b/tests/components/unifiprotect/test_number.py @@ -328,6 +328,41 @@ async def test_chime_ring_volume_set_value( mock_method.assert_called_once_with(doorbell, 80) +async def test_chime_volume_set_value( + hass: HomeAssistant, + ufp: MockUFPFixture, + chime: Chime, + doorbell: Camera, +) -> None: + """Test setting overall chime volume calls public ring-settings API.""" + _setup_chime_with_doorbell(chime, doorbell, volume=40) + + await init_entry(hass, ufp, [chime, doorbell], regenerate_ids=False) + + entity_id = "number.test_chime_volume" + + with patch_ufp_method( + chime, "set_ring_settings_public", new_callable=AsyncMock + ) as mock_method: + await hass.services.async_call( + "number", + "set_value", + {ATTR_ENTITY_ID: entity_id, "value": 75.0}, + blocking=True, + ) + + mock_method.assert_called_once_with( + [ + { + "cameraId": doorbell.id, + "volume": 75, + "repeatTimes": 1, + "ringtoneId": "test-ringtone-id", + } + ] + ) + + async def test_chime_ring_volume_multiple_cameras( hass: HomeAssistant, ufp: MockUFPFixture, diff --git a/tests/components/unifiprotect/test_select.py b/tests/components/unifiprotect/test_select.py index c9f14815482..c3a67f49bb9 100644 --- a/tests/components/unifiprotect/test_select.py +++ b/tests/components/unifiprotect/test_select.py @@ -19,6 +19,7 @@ from uiprotect.data import ( NvrArmModeStatus, PTZPatrol, PublicBootstrap, + PublicHdrMode, RecordingMode, Viewer, ) @@ -454,7 +455,7 @@ async def test_select_set_option_camera_doorbell_custom( ) with patch_ufp_method( - doorbell, "set_lcd_text", new_callable=AsyncMock + doorbell, "set_lcd_message_public", new_callable=AsyncMock ) as mock_method: await hass.services.async_call( "select", @@ -480,9 +481,14 @@ async def test_select_set_option_camera_doorbell_unifi( hass, Platform.SELECT, doorbell, CAMERA_SELECTS[2] ) - with patch_ufp_method( - doorbell, "set_lcd_text", new_callable=AsyncMock - ) as mock_method: + 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", @@ -493,7 +499,7 @@ async def test_select_set_option_camera_doorbell_unifi( blocking=True, ) - mock_method.assert_called_once_with(DoorbellMessageType.LEAVE_PACKAGE_AT_DOOR) + mock_public.assert_called_once_with(DoorbellMessageType.LEAVE_PACKAGE_AT_DOOR) await hass.services.async_call( "select", @@ -505,7 +511,7 @@ async def test_select_set_option_camera_doorbell_unifi( blocking=True, ) - mock_method.assert_called_with(None) + mock_legacy.assert_called_once_with(None) async def test_select_set_option_camera_doorbell_default( @@ -536,6 +542,44 @@ async def test_select_set_option_camera_doorbell_default( 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: diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 93ca4f4a36b..3d2ca313c0c 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -1,9 +1,18 @@ """Test the UniFi Protect switch platform.""" -from unittest.mock import AsyncMock, Mock +from unittest.mock import AsyncMock, Mock, call import pytest -from uiprotect.data import Camera, Light, Permission, RecordingMode, VideoMode +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 @@ -363,7 +372,7 @@ async def test_switch_camera_highfps( ) with patch_ufp_method( - doorbell, "set_video_mode", new_callable=AsyncMock + 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 @@ -378,6 +387,102 @@ async def test_switch_camera_highfps( 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: