1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-28 03:06:30 +01:00

Migrate more UniFi Protect entities to public API (#171785)

Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
This commit is contained in:
Raphael Hehl
2026-05-26 21:41:25 +02:00
committed by GitHub
parent b6fa89c032
commit 0bb6113bfd
7 changed files with 255 additions and 40 deletions
@@ -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,
),
)
@@ -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,
),
)
@@ -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": {
+28 -22
View File
@@ -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(
@@ -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,
+50 -6
View File
@@ -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:
+108 -3
View File
@@ -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: