Migrate UniFi Protect camera config switches to the public API (#174963)

This commit is contained in:
Raphael Hehl
2026-08-23 22:32:40 +02:00
committed by GitHub
parent 74a4168e5d
commit 9d12e00daf
3 changed files with 258 additions and 39 deletions
+20 -34
View File
@@ -77,7 +77,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
translation_key="status_light", translation_key="status_light",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_required_field="feature_flags.has_led_status", ufp_required_field="feature_flags.has_led_status",
ufp_value="led_settings.is_enabled", ufp_public_value="led_settings.is_enabled",
ufp_set_method="set_status_light_public", ufp_set_method="set_status_light_public",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -96,7 +96,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
translation_key="high_fps", translation_key="high_fps",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_required_field="feature_flags.has_highfps", ufp_required_field="feature_flags.has_highfps",
ufp_value="is_high_fps_enabled", ufp_public_value="is_high_fps_enabled",
ufp_set_method_fn=_set_highfps, ufp_set_method_fn=_set_highfps,
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -114,7 +114,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
key="osd_name", key="osd_name",
translation_key="overlay_show_name", translation_key="overlay_show_name",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_value="osd_settings.is_name_enabled", ufp_public_value="osd_settings.is_name_enabled",
ufp_set_method="set_osd_name_public", ufp_set_method="set_osd_name_public",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -122,7 +122,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
key="osd_date", key="osd_date",
translation_key="overlay_show_date", translation_key="overlay_show_date",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_value="osd_settings.is_date_enabled", ufp_public_value="osd_settings.is_date_enabled",
ufp_set_method="set_osd_date_public", ufp_set_method="set_osd_date_public",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -130,7 +130,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
key="osd_logo", key="osd_logo",
translation_key="overlay_show_logo", translation_key="overlay_show_logo",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_value="osd_settings.is_logo_enabled", ufp_public_value="osd_settings.is_logo_enabled",
ufp_set_method="set_osd_logo_public", ufp_set_method="set_osd_logo_public",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -138,7 +138,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
key="osd_bitrate", key="osd_bitrate",
translation_key="overlay_show_nerd_mode", translation_key="overlay_show_nerd_mode",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_value="osd_settings.is_debug_enabled", ufp_public_value="osd_settings.is_debug_enabled",
ufp_set_method="set_osd_nerd_mode_public", ufp_set_method="set_osd_nerd_mode_public",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -165,8 +165,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
translation_key="detections_person", translation_key="detections_person",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_person", ufp_required_field="can_detect_person",
ufp_value="is_person_detection_on", ufp_public_value="is_person_detection_on",
ufp_enabled="is_recording_enabled",
ufp_set_method="set_person_detection_public", ufp_set_method="set_person_detection_public",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -175,8 +174,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
translation_key="detections_vehicle", translation_key="detections_vehicle",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_vehicle", ufp_required_field="can_detect_vehicle",
ufp_value="is_vehicle_detection_on", ufp_public_value="is_vehicle_detection_on",
ufp_enabled="is_recording_enabled",
ufp_set_method="set_vehicle_detection_public", ufp_set_method="set_vehicle_detection_public",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -185,8 +183,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
translation_key="detections_animal", translation_key="detections_animal",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_animal", ufp_required_field="can_detect_animal",
ufp_value="is_animal_detection_on", ufp_public_value="is_animal_detection_on",
ufp_enabled="is_recording_enabled",
ufp_set_method="set_animal_detection_public", ufp_set_method="set_animal_detection_public",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -195,8 +192,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
translation_key="detections_package", translation_key="detections_package",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_package", ufp_required_field="can_detect_package",
ufp_value="is_package_detection_on", ufp_public_value="is_package_detection_on",
ufp_enabled="is_recording_enabled",
ufp_set_method="set_package_detection_public", ufp_set_method="set_package_detection_public",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -205,8 +201,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
translation_key="detections_license_plate", translation_key="detections_license_plate",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_license_plate", ufp_required_field="can_detect_license_plate",
ufp_value="is_license_plate_detection_on", ufp_public_value="is_license_plate_detection_on",
ufp_enabled="is_recording_enabled",
ufp_set_method="set_license_plate_detection_public", ufp_set_method="set_license_plate_detection_public",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -215,8 +210,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
translation_key="detections_smoke", translation_key="detections_smoke",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_smoke", ufp_required_field="can_detect_smoke",
ufp_value="is_smoke_detection_on", ufp_public_value="is_smoke_detection_on",
ufp_enabled="is_recording_enabled",
ufp_set_method="set_smoke_detection_public", ufp_set_method="set_smoke_detection_public",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -225,8 +219,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
translation_key="detections_co_alarm", translation_key="detections_co_alarm",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_co", ufp_required_field="can_detect_co",
ufp_value="is_co_detection_on", ufp_public_value="is_co_detection_on",
ufp_enabled="is_recording_enabled",
ufp_set_method="set_co_detection_public", ufp_set_method="set_co_detection_public",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -235,8 +228,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
translation_key="detections_siren", translation_key="detections_siren",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_siren", ufp_required_field="can_detect_siren",
ufp_value="is_siren_detection_on", ufp_public_value="is_siren_detection_on",
ufp_enabled="is_recording_enabled",
ufp_set_method="set_siren_detection_public", ufp_set_method="set_siren_detection_public",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -245,8 +237,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
translation_key="detections_baby_cry", translation_key="detections_baby_cry",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_baby_cry", ufp_required_field="can_detect_baby_cry",
ufp_value="is_baby_cry_detection_on", ufp_public_value="is_baby_cry_detection_on",
ufp_enabled="is_recording_enabled",
ufp_set_method="set_baby_cry_detection_public", ufp_set_method="set_baby_cry_detection_public",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -255,8 +246,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
translation_key="detections_speak", translation_key="detections_speak",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_speaking", ufp_required_field="can_detect_speaking",
ufp_value="is_speaking_detection_on", ufp_public_value="is_speaking_detection_on",
ufp_enabled="is_recording_enabled",
ufp_set_method="set_speaking_detection_public", ufp_set_method="set_speaking_detection_public",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -265,8 +255,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
translation_key="detections_bark", translation_key="detections_bark",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_bark", ufp_required_field="can_detect_bark",
ufp_value="is_bark_detection_on", ufp_public_value="is_bark_detection_on",
ufp_enabled="is_recording_enabled",
ufp_set_method="set_bark_detection_public", ufp_set_method="set_bark_detection_public",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -275,9 +264,8 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
translation_key="detections_car_alarm", translation_key="detections_car_alarm",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_car_alarm", ufp_required_field="can_detect_car_alarm",
ufp_value="is_car_alarm_detection_on",
ufp_enabled="is_recording_enabled",
# Public API renamed "car alarm" to "burglar"; internal model keeps the legacy name. # Public API renamed "car alarm" to "burglar"; internal model keeps the legacy name.
ufp_public_value="is_car_alarm_detection_on",
ufp_set_method="set_burglar_detection_public", ufp_set_method="set_burglar_detection_public",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -286,8 +274,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
translation_key="detections_car_horn", translation_key="detections_car_horn",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_car_horn", ufp_required_field="can_detect_car_horn",
ufp_value="is_car_horn_detection_on", ufp_public_value="is_car_horn_detection_on",
ufp_enabled="is_recording_enabled",
ufp_set_method="set_car_horn_detection_public", ufp_set_method="set_car_horn_detection_public",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -296,8 +283,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
translation_key="detections_glass_break", translation_key="detections_glass_break",
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
ufp_required_field="can_detect_glass_break", ufp_required_field="can_detect_glass_break",
ufp_value="is_glass_break_detection_on", ufp_public_value="is_glass_break_detection_on",
ufp_enabled="is_recording_enabled",
ufp_set_method="set_glass_break_detection_public", ufp_set_method="set_glass_break_detection_public",
ufp_perm=PermRequired.WRITE, ufp_perm=PermRequired.WRITE,
), ),
@@ -1,5 +1,6 @@
"""Test the UniFi Protect switch platform.""" """Test the UniFi Protect switch platform."""
from typing import Any
from unittest.mock import AsyncMock, Mock, call from unittest.mock import AsyncMock, Mock, call
import pytest import pytest
@@ -44,9 +45,11 @@ from .utils import (
enable_entity, enable_entity,
ids_from_device_description, ids_from_device_description,
init_entry, init_entry,
make_public_camera,
make_public_light, make_public_light,
public_device_ws_message, public_device_ws_message,
remove_entities, remove_entities,
setup_public_camera,
setup_public_light, setup_public_light,
) )
@@ -194,6 +197,7 @@ async def test_switch_setup_camera_all(
) -> None: ) -> None:
"""Test switch entity setup for camera devices (all enabled feature flags).""" """Test switch entity setup for camera devices (all enabled feature flags)."""
setup_public_camera(ufp)
await init_entry(hass, ufp, [doorbell]) await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 17, 15) assert_entity_counts(hass, Platform.SWITCH, 17, 15)
@@ -237,6 +241,7 @@ async def test_switch_setup_camera_none(
) -> None: ) -> None:
"""Test switch entity setup for camera devices (no enabled feature flags).""" """Test switch entity setup for camera devices (no enabled feature flags)."""
setup_public_camera(ufp)
await init_entry(hass, ufp, [camera]) await init_entry(hass, ufp, [camera])
assert_entity_counts(hass, Platform.SWITCH, 8, 7) assert_entity_counts(hass, Platform.SWITCH, 8, 7)
@@ -378,6 +383,7 @@ async def test_switch_camera_simple(
) -> None: ) -> None:
"""Tests all simple switches for cameras.""" """Tests all simple switches for cameras."""
setup_public_camera(ufp)
await init_entry(hass, ufp, [doorbell]) await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 17, 15) assert_entity_counts(hass, Platform.SWITCH, 17, 15)
@@ -408,6 +414,7 @@ async def test_switch_camera_highfps(
) -> None: ) -> None:
"""Tests High FPS switch for cameras.""" """Tests High FPS switch for cameras."""
setup_public_camera(ufp)
await init_entry(hass, ufp, [doorbell]) await init_entry(hass, ufp, [doorbell])
assert_entity_counts(hass, Platform.SWITCH, 17, 15) assert_entity_counts(hass, Platform.SWITCH, 17, 15)
@@ -506,6 +513,7 @@ async def test_switch_camera_detections_public_api(
SmartDetectAudioType.GLASS_BREAK, SmartDetectAudioType.GLASS_BREAK,
] ]
setup_public_camera(ufp)
await init_entry(hass, ufp, [doorbell]) await init_entry(hass, ufp, [doorbell])
assert description.ufp_set_method is not None assert description.ufp_set_method is not None
@@ -529,6 +537,205 @@ async def test_switch_camera_detections_public_api(
assert mock_method.call_count == 2 assert mock_method.call_count == 2
async def test_switch_camera_status_light_public_value(
hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
) -> None:
"""Status light reads from the public object and refreshes on a public WS update."""
setup_public_camera(ufp)
await init_entry(hass, ufp, [doorbell])
description = next(d for d in CAMERA_SWITCHES if d.key == "status_light")
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, doorbell, description
)
assert hass.states.get(entity_id).state == STATE_OFF
public = make_public_camera(doorbell, status_light=True)
ufp.devices_ws_subscription(public_device_ws_message(public))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON
@pytest.mark.parametrize(
("switch_key", "camera_kwarg"),
[
("osd_name", "osd_name"),
("osd_date", "osd_date"),
("osd_logo", "osd_logo"),
("osd_bitrate", "osd_debug"),
],
)
async def test_switch_camera_osd_public_value(
hass: HomeAssistant,
ufp: MockUFPFixture,
doorbell: Camera,
switch_key: str,
camera_kwarg: str,
) -> None:
"""Each OSD switch reads its own public osd_settings flag independently."""
setup_public_camera(ufp)
await init_entry(hass, ufp, [doorbell])
description = next(d for d in CAMERA_SWITCHES if d.key == switch_key)
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, doorbell, description
)
assert hass.states.get(entity_id).state == STATE_OFF
public = make_public_camera(doorbell, **{camera_kwarg: True})
ufp.devices_ws_subscription(public_device_ws_message(public))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON
# Only the switch under test is enabled; ``make_public_camera`` defaults both
# lists to every type, so each case pins both to keep the others off.
CAMERA_SWITCHES_DETECTION_READ = [
("smart_person", [SmartDetectObjectType.PERSON], []),
("smart_vehicle", [SmartDetectObjectType.VEHICLE], []),
("smart_animal", [SmartDetectObjectType.ANIMAL], []),
("smart_package", [SmartDetectObjectType.PACKAGE], []),
("smart_licenseplate", [SmartDetectObjectType.LICENSE_PLATE], []),
("smart_smoke", [], [SmartDetectAudioType.SMOKE]),
("smart_cmonx", [], [SmartDetectAudioType.CMONX]),
("smart_siren", [], [SmartDetectAudioType.SIREN]),
("smart_baby_cry", [], [SmartDetectAudioType.BABY_CRY]),
("smart_speak", [], [SmartDetectAudioType.SPEAK]),
("smart_bark", [], [SmartDetectAudioType.BARK]),
("smart_car_alarm", [], [SmartDetectAudioType.BURGLAR]),
("smart_car_horn", [], [SmartDetectAudioType.CAR_HORN]),
("smart_glass_break", [], [SmartDetectAudioType.GLASS_BREAK]),
]
@pytest.mark.parametrize(
("key", "object_types", "audio_types"), CAMERA_SWITCHES_DETECTION_READ
)
async def test_switch_camera_detection_public_value(
hass: HomeAssistant,
ufp: MockUFPFixture,
doorbell: Camera,
key: str,
object_types: list[SmartDetectObjectType],
audio_types: list[SmartDetectAudioType],
) -> None:
"""Each detection toggle reads its on/off state from its own public flag."""
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,
]
setup_public_camera(ufp)
async def _prime_without_camera() -> Any:
pb = ufp.api.public_bootstrap
pb.cameras = {}
return pb
ufp.api.update_public = AsyncMock(side_effect=_prime_without_camera)
await init_entry(hass, ufp, [doorbell])
description = next(d for d in CAMERA_SWITCHES if d.key == key)
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, doorbell, description
)
all_off = make_public_camera(doorbell, object_types=[], audio_types=[])
ufp.devices_ws_subscription(public_device_ws_message(all_off))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF
public = make_public_camera(
doorbell, object_types=object_types, audio_types=audio_types
)
ufp.devices_ws_subscription(public_device_ws_message(public))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON
async def test_switch_camera_highfps_public_value(
hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
) -> None:
"""The high FPS switch reads video_mode from the public object."""
setup_public_camera(ufp)
await init_entry(hass, ufp, [doorbell])
description = next(d for d in CAMERA_SWITCHES if d.key == "high_fps")
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, doorbell, description
)
assert hass.states.get(entity_id).state == STATE_OFF
public = make_public_camera(doorbell, video_mode=VideoMode.HIGH_FPS)
ufp.devices_ws_subscription(public_device_ws_message(public))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_ON
async def test_switch_camera_detection_unavailable_without_public(
hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
) -> None:
"""A migrated detection toggle is unavailable without a public object."""
async def _prime_without_camera() -> Any:
pb = ufp.api.public_bootstrap
pb.cameras = {}
return pb
ufp.api.update_public = AsyncMock(side_effect=_prime_without_camera)
await init_entry(hass, ufp, [doorbell])
description = next(d for d in CAMERA_SWITCHES if d.key == "smart_person")
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, doorbell, description
)
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
async def test_switch_camera_detection_available_with_recording_disabled(
hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
) -> None:
"""A migrated detection toggle stays available with recording disabled.
Unlike the legacy private-only switch, the public detection toggles no
longer gate their availability on ``is_recording_enabled`` (breaking change).
"""
doorbell.recording_settings.mode = RecordingMode.NEVER
setup_public_camera(ufp)
await init_entry(hass, ufp, [doorbell])
description = next(d for d in CAMERA_SWITCHES if d.key == "smart_person")
_, entity_id = await ids_from_device_description(
hass, Platform.SWITCH, doorbell, description
)
assert hass.states.get(entity_id).state != STATE_UNAVAILABLE
async def test_switch_camera_privacy( async def test_switch_camera_privacy(
hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera
) -> None: ) -> None:
+31 -5
View File
@@ -25,15 +25,18 @@ from uiprotect.data import (
Sensor, Sensor,
SmartDetectAudioType, SmartDetectAudioType,
SmartDetectObjectType, SmartDetectObjectType,
VideoMode,
WSSubscriptionMessage, WSSubscriptionMessage,
) )
from uiprotect.data.bootstrap import ProtectDeviceRef from uiprotect.data.bootstrap import ProtectDeviceRef
from uiprotect.data.public_devices import ( from uiprotect.data.public_devices import (
PublicCamera, PublicCamera,
PublicCameraLedSettings,
PublicHdrMode, PublicHdrMode,
PublicLight, PublicLight,
PublicLightDeviceSettings, PublicLightDeviceSettings,
PublicLightModeSettings, PublicLightModeSettings,
PublicOsdSettings,
PublicSensor, PublicSensor,
PublicSensorLeakSettings, PublicSensorLeakSettings,
PublicSensorMotionSettingsRead, PublicSensorMotionSettingsRead,
@@ -425,6 +428,12 @@ def make_public_camera(
camera: Camera, camera: Camera,
*, *,
state: DeviceState | None = None, state: DeviceState | None = None,
status_light: bool = False,
osd_name: bool = False,
osd_date: bool = False,
osd_logo: bool = False,
osd_debug: bool = False,
video_mode: VideoMode | None = None,
is_motion_detected: bool = False, is_motion_detected: bool = False,
is_smart_currently_detected: bool = False, is_smart_currently_detected: bool = False,
is_person_currently_detected: bool = False, is_person_currently_detected: bool = False,
@@ -445,13 +454,19 @@ def make_public_camera(
mic_volume: int | None = None, mic_volume: int | None = None,
hdr_type: PublicHdrMode | None = None, hdr_type: PublicHdrMode | None = None,
) -> Mock: ) -> Mock:
"""Build a public-API camera mirroring a private camera's migrated fields. """Build a public-API camera for a private camera's migrated fields.
The stream tiers/mic/HDR back the migrated stream and select entities; the The stream tiers/mic/HDR back the migrated stream and select entities; the
``is_*`` flags back the migrated ``ufp_public_value`` detection paths and the ``is_*`` flags back the migrated ``ufp_public_value`` detection paths and the
``smart_detect_settings`` types back the per-type ``ufp_public_enabled_fn`` ``smart_detect_settings`` types back the per-type ``ufp_public_enabled_fn``
gates (default: all types enabled). ``mic_volume`` and ``hdr_type`` default to gates (default: all types enabled). ``state``, ``video_mode``, ``mic_volume``
values derived from the private fixture so the public mirror matches it. and ``hdr_type`` (derived from the private ``hdr_mode_display``) mirror the
private camera when not overridden.
``status_light`` and the ``osd_*`` flags deliberately default to off instead
of mirroring, so a test overriding one sets a value the private object would
not produce and a wrong ``ufp_public_value``/``ufp_public_value_fn`` fails
the test.
""" """
public = Mock(spec=PublicCamera) public = Mock(spec=PublicCamera)
public.id = camera.id public.id = camera.id
@@ -461,6 +476,14 @@ def make_public_camera(
public.type = camera.type public.type = camera.type
public.model = ModelType.CAMERA public.model = ModelType.CAMERA
public.state = DeviceState[camera.state.name] if state is None else state public.state = DeviceState[camera.state.name] if state is None else state
public.led_settings = PublicCameraLedSettings(is_enabled=status_light)
public.osd_settings = PublicOsdSettings(
is_name_enabled=osd_name,
is_date_enabled=osd_date,
is_logo_enabled=osd_logo,
is_debug_enabled=osd_debug,
)
public.video_mode = camera.video_mode if video_mode is None else video_mode
public.mic_volume = camera.mic_volume if mic_volume is None else mic_volume public.mic_volume = camera.mic_volume if mic_volume is None else mic_volume
public.is_motion_detected = is_motion_detected public.is_motion_detected = is_motion_detected
public.is_smart_currently_detected = is_smart_currently_detected public.is_smart_currently_detected = is_smart_currently_detected
@@ -482,12 +505,15 @@ def make_public_camera(
audio_types=_ALL_AUDIO_TYPES if audio_types is None else audio_types, audio_types=_ALL_AUDIO_TYPES if audio_types is None else audio_types,
) )
# A Mock(spec) does not evaluate properties, so mirror the PublicCamera # A Mock(spec) does not evaluate properties, so mirror the PublicCamera
# parity properties the migrated detection sensors gate on using the # parity properties the migrated switches read and the detection sensors
# library's own logic. # gate on, using the library's own logic.
for name in ( for name in (
"is_high_fps_enabled",
"is_person_detection_on", "is_person_detection_on",
"is_vehicle_detection_on", "is_vehicle_detection_on",
"is_animal_detection_on", "is_animal_detection_on",
"is_package_detection_on",
"is_license_plate_detection_on",
"is_smoke_detection_on", "is_smoke_detection_on",
"is_co_detection_on", "is_co_detection_on",
"is_siren_detection_on", "is_siren_detection_on",