uiprotect 3.8.0__py3-none-any.whl → 7.32.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uiprotect/__init__.py +1 -3
- uiprotect/_compat.py +13 -0
- uiprotect/api.py +975 -92
- uiprotect/cli/__init__.py +111 -24
- uiprotect/cli/aiports.py +58 -0
- uiprotect/cli/backup.py +5 -4
- uiprotect/cli/base.py +5 -5
- uiprotect/cli/cameras.py +152 -13
- uiprotect/cli/chimes.py +5 -6
- uiprotect/cli/doorlocks.py +2 -3
- uiprotect/cli/events.py +7 -8
- uiprotect/cli/lights.py +11 -3
- uiprotect/cli/liveviews.py +1 -2
- uiprotect/cli/sensors.py +2 -3
- uiprotect/cli/viewers.py +2 -3
- uiprotect/data/__init__.py +2 -0
- uiprotect/data/base.py +96 -97
- uiprotect/data/bootstrap.py +116 -45
- uiprotect/data/convert.py +17 -2
- uiprotect/data/devices.py +409 -164
- uiprotect/data/nvr.py +236 -118
- uiprotect/data/types.py +94 -59
- uiprotect/data/user.py +132 -13
- uiprotect/data/websocket.py +2 -1
- uiprotect/stream.py +13 -6
- uiprotect/test_util/__init__.py +47 -7
- uiprotect/test_util/anonymize.py +4 -5
- uiprotect/utils.py +99 -45
- uiprotect/websocket.py +11 -6
- {uiprotect-3.8.0.dist-info → uiprotect-7.32.0.dist-info}/METADATA +77 -21
- uiprotect-7.32.0.dist-info/RECORD +39 -0
- {uiprotect-3.8.0.dist-info → uiprotect-7.32.0.dist-info}/WHEEL +1 -1
- uiprotect-3.8.0.dist-info/RECORD +0 -37
- {uiprotect-3.8.0.dist-info → uiprotect-7.32.0.dist-info}/entry_points.txt +0 -0
- {uiprotect-3.8.0.dist-info → uiprotect-7.32.0.dist-info/licenses}/LICENSE +0 -0
uiprotect/data/devices.py
CHANGED
|
@@ -7,12 +7,14 @@ import logging
|
|
|
7
7
|
import warnings
|
|
8
8
|
from collections.abc import Callable
|
|
9
9
|
from datetime import datetime, timedelta
|
|
10
|
-
from functools import cache
|
|
11
|
-
from ipaddress import IPv4Address
|
|
10
|
+
from functools import cache, lru_cache
|
|
11
|
+
from ipaddress import IPv4Address, IPv6Address
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
14
14
|
|
|
15
|
-
from
|
|
15
|
+
from convertertools import pop_dict_set_if_none, pop_dict_tuple
|
|
16
|
+
from pydantic import model_validator
|
|
17
|
+
from pydantic.fields import PrivateAttr
|
|
16
18
|
|
|
17
19
|
from ..exceptions import BadRequest, NotAuthorized, StreamError
|
|
18
20
|
from ..stream import TalkbackStream
|
|
@@ -22,8 +24,10 @@ from ..utils import (
|
|
|
22
24
|
convert_smart_types,
|
|
23
25
|
convert_to_datetime,
|
|
24
26
|
convert_video_modes,
|
|
27
|
+
format_host_for_url,
|
|
25
28
|
from_js_time,
|
|
26
29
|
serialize_point,
|
|
30
|
+
timedelta_total_seconds,
|
|
27
31
|
to_js_time,
|
|
28
32
|
utc_now,
|
|
29
33
|
)
|
|
@@ -78,21 +82,11 @@ from .types import (
|
|
|
78
82
|
from .user import User
|
|
79
83
|
|
|
80
84
|
if TYPE_CHECKING:
|
|
85
|
+
from ..api import RTSPSStreams
|
|
81
86
|
from .nvr import Event, Liveview
|
|
82
87
|
|
|
83
88
|
PRIVACY_ZONE_NAME = "pyufp_privacy_zone"
|
|
84
|
-
LUX_MAPPING_VALUES = [
|
|
85
|
-
30,
|
|
86
|
-
25,
|
|
87
|
-
20,
|
|
88
|
-
15,
|
|
89
|
-
12,
|
|
90
|
-
10,
|
|
91
|
-
7,
|
|
92
|
-
5,
|
|
93
|
-
3,
|
|
94
|
-
1,
|
|
95
|
-
]
|
|
89
|
+
LUX_MAPPING_VALUES = [30, 25, 20, 15, 12, 10, 7, 5, 3, 1, 0]
|
|
96
90
|
|
|
97
91
|
_LOGGER = logging.getLogger(__name__)
|
|
98
92
|
|
|
@@ -102,9 +96,10 @@ class LightDeviceSettings(ProtectBaseObject):
|
|
|
102
96
|
is_indicator_enabled: bool
|
|
103
97
|
# Brightness
|
|
104
98
|
led_level: LEDLevel
|
|
105
|
-
lux_sensitivity: LowMedHigh
|
|
106
99
|
pir_duration: timedelta
|
|
107
100
|
pir_sensitivity: PercentInt
|
|
101
|
+
# lux_sensitivity exists in Private API but not in Public API - filtered when sending updates
|
|
102
|
+
lux_sensitivity: LowMedHigh | None = None
|
|
108
103
|
|
|
109
104
|
@classmethod
|
|
110
105
|
@cache
|
|
@@ -132,7 +127,7 @@ class Light(ProtectMotionDeviceModel):
|
|
|
132
127
|
light_device_settings: LightDeviceSettings
|
|
133
128
|
light_on_settings: LightOnSettings
|
|
134
129
|
light_mode_settings: LightModeSettings
|
|
135
|
-
camera_id: str | None
|
|
130
|
+
camera_id: str | None = None
|
|
136
131
|
is_camera_paired: bool
|
|
137
132
|
|
|
138
133
|
@classmethod
|
|
@@ -169,6 +164,14 @@ class Light(ProtectMotionDeviceModel):
|
|
|
169
164
|
self.camera_id = camera.id
|
|
170
165
|
await self.save_device(data_before_changes, force_emit=True)
|
|
171
166
|
|
|
167
|
+
async def set_flood_light(self, enabled: bool) -> None:
|
|
168
|
+
"""Sets the flood light (force on) for the light"""
|
|
169
|
+
|
|
170
|
+
def callback() -> None:
|
|
171
|
+
self.light_on_settings.is_led_force_on = enabled
|
|
172
|
+
|
|
173
|
+
await self.queue_update(callback)
|
|
174
|
+
|
|
172
175
|
async def set_status_light(self, enabled: bool) -> None:
|
|
173
176
|
"""Sets the status indicator light for the light"""
|
|
174
177
|
|
|
@@ -254,23 +257,31 @@ class CameraChannel(ProtectBaseObject):
|
|
|
254
257
|
name: str # read only
|
|
255
258
|
enabled: bool # read only
|
|
256
259
|
is_rtsp_enabled: bool
|
|
257
|
-
rtsp_alias: str | None # read only
|
|
260
|
+
rtsp_alias: str | None = None # read only
|
|
258
261
|
width: int
|
|
259
262
|
height: int
|
|
260
|
-
fps: int
|
|
263
|
+
fps: int | None = None # read only
|
|
261
264
|
bitrate: int
|
|
262
|
-
min_bitrate: int # read only
|
|
263
|
-
max_bitrate: int # read only
|
|
264
|
-
min_client_adaptive_bit_rate: int | None # read only
|
|
265
|
-
min_motion_adaptive_bit_rate: int | None # read only
|
|
265
|
+
min_bitrate: int | None = None # read only
|
|
266
|
+
max_bitrate: int | None = None # read only
|
|
267
|
+
min_client_adaptive_bit_rate: int | None = None # read only
|
|
268
|
+
min_motion_adaptive_bit_rate: int | None = None # read only
|
|
266
269
|
fps_values: list[int] # read only
|
|
267
270
|
idr_interval: int
|
|
268
271
|
# 3.0.22+
|
|
269
272
|
auto_bitrate: bool | None = None
|
|
270
273
|
auto_fps: bool | None = None
|
|
271
274
|
|
|
275
|
+
_parent: Camera | None = PrivateAttr(None)
|
|
272
276
|
_rtsp_url: str | None = PrivateAttr(None)
|
|
273
277
|
_rtsps_url: str | None = PrivateAttr(None)
|
|
278
|
+
_rtsps_no_srtp_url: str | None = PrivateAttr(None)
|
|
279
|
+
|
|
280
|
+
def _get_connection_host(self) -> IPv4Address | IPv6Address | str:
|
|
281
|
+
"""Get connection host (camera's for stacked NVR, otherwise NVR's)."""
|
|
282
|
+
if self._parent is not None and self._parent.connection_host is not None:
|
|
283
|
+
return self._parent.connection_host
|
|
284
|
+
return self._api.connection_host
|
|
274
285
|
|
|
275
286
|
@property
|
|
276
287
|
def rtsp_url(self) -> str | None:
|
|
@@ -279,7 +290,11 @@ class CameraChannel(ProtectBaseObject):
|
|
|
279
290
|
|
|
280
291
|
if self._rtsp_url is not None:
|
|
281
292
|
return self._rtsp_url
|
|
282
|
-
|
|
293
|
+
|
|
294
|
+
host = format_host_for_url(self._get_connection_host())
|
|
295
|
+
self._rtsp_url = (
|
|
296
|
+
f"rtsp://{host}:{self._api.bootstrap.nvr.ports.rtsp}/{self.rtsp_alias}"
|
|
297
|
+
)
|
|
283
298
|
return self._rtsp_url
|
|
284
299
|
|
|
285
300
|
@property
|
|
@@ -289,12 +304,28 @@ class CameraChannel(ProtectBaseObject):
|
|
|
289
304
|
|
|
290
305
|
if self._rtsps_url is not None:
|
|
291
306
|
return self._rtsps_url
|
|
292
|
-
|
|
307
|
+
|
|
308
|
+
host = format_host_for_url(self._get_connection_host())
|
|
309
|
+
self._rtsps_url = f"rtsps://{host}:{self._api.bootstrap.nvr.ports.rtsps}/{self.rtsp_alias}?enableSrtp"
|
|
293
310
|
return self._rtsps_url
|
|
294
311
|
|
|
312
|
+
@property
|
|
313
|
+
def rtsps_no_srtp_url(self) -> str | None:
|
|
314
|
+
if not self.is_rtsp_enabled or self.rtsp_alias is None:
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
if self._rtsps_no_srtp_url is not None:
|
|
318
|
+
return self._rtsps_no_srtp_url
|
|
319
|
+
|
|
320
|
+
host = format_host_for_url(self._get_connection_host())
|
|
321
|
+
self._rtsps_no_srtp_url = (
|
|
322
|
+
f"rtsps://{host}:{self._api.bootstrap.nvr.ports.rtsps}/{self.rtsp_alias}"
|
|
323
|
+
)
|
|
324
|
+
return self._rtsps_no_srtp_url
|
|
325
|
+
|
|
295
326
|
@property
|
|
296
327
|
def is_package(self) -> bool:
|
|
297
|
-
return self.fps <= 2
|
|
328
|
+
return self.fps is not None and self.fps <= 2
|
|
298
329
|
|
|
299
330
|
|
|
300
331
|
class ISPSettings(ProtectBaseObject):
|
|
@@ -323,8 +354,8 @@ class ISPSettings(ProtectBaseObject):
|
|
|
323
354
|
d_zoom_stream_id: int
|
|
324
355
|
focus_mode: FocusMode | None = None
|
|
325
356
|
focus_position: int
|
|
326
|
-
touch_focus_x: int | None
|
|
327
|
-
touch_focus_y: int | None
|
|
357
|
+
touch_focus_x: int | None = None
|
|
358
|
+
touch_focus_y: int | None = None
|
|
328
359
|
zoom_position: PercentInt
|
|
329
360
|
mount_position: MountPosition | None = None
|
|
330
361
|
# requires 2.8.14+
|
|
@@ -341,10 +372,7 @@ class ISPSettings(ProtectBaseObject):
|
|
|
341
372
|
exclude: set[str] | None = None,
|
|
342
373
|
) -> dict[str, Any]:
|
|
343
374
|
data = super().unifi_dict(data=data, exclude=exclude)
|
|
344
|
-
|
|
345
|
-
if "focusMode" in data and data["focusMode"] is None:
|
|
346
|
-
del data["focusMode"]
|
|
347
|
-
|
|
375
|
+
pop_dict_set_if_none(data, {"focusMode"})
|
|
348
376
|
return data
|
|
349
377
|
|
|
350
378
|
|
|
@@ -359,7 +387,21 @@ class OSDSettings(ProtectBaseObject):
|
|
|
359
387
|
class LEDSettings(ProtectBaseObject):
|
|
360
388
|
# Status Light
|
|
361
389
|
is_enabled: bool
|
|
362
|
-
blink_rate: int
|
|
390
|
+
blink_rate: int | None = (
|
|
391
|
+
None # in milliseconds between blinks, 0 = solid (removed in Protect 6.x)
|
|
392
|
+
)
|
|
393
|
+
# 6.2+
|
|
394
|
+
welcome_led: bool | None = None
|
|
395
|
+
flood_led: bool | None = None
|
|
396
|
+
|
|
397
|
+
def unifi_dict(
|
|
398
|
+
self,
|
|
399
|
+
data: dict[str, Any] | None = None,
|
|
400
|
+
exclude: set[str] | None = None,
|
|
401
|
+
) -> dict[str, Any]:
|
|
402
|
+
data = super().unifi_dict(data=data, exclude=exclude)
|
|
403
|
+
pop_dict_set_if_none(data, {"blinkRate", "welcomeLed", "floodLed"})
|
|
404
|
+
return data
|
|
363
405
|
|
|
364
406
|
|
|
365
407
|
class SpeakerSettings(ProtectBaseObject):
|
|
@@ -367,6 +409,23 @@ class SpeakerSettings(ProtectBaseObject):
|
|
|
367
409
|
# Status Sounds
|
|
368
410
|
are_system_sounds_enabled: bool
|
|
369
411
|
volume: PercentInt
|
|
412
|
+
# Doorbell ring volume (for doorbells)
|
|
413
|
+
ring_volume: PercentInt | None = None
|
|
414
|
+
ringtone_id: str | None = None
|
|
415
|
+
repeat_times: int | None = None
|
|
416
|
+
# Actual speaker output volume (for cameras with speakers)
|
|
417
|
+
speaker_volume: PercentInt | None = None
|
|
418
|
+
|
|
419
|
+
def unifi_dict(
|
|
420
|
+
self,
|
|
421
|
+
data: dict[str, Any] | None = None,
|
|
422
|
+
exclude: set[str] | None = None,
|
|
423
|
+
) -> dict[str, Any]:
|
|
424
|
+
data = super().unifi_dict(data=data, exclude=exclude)
|
|
425
|
+
pop_dict_set_if_none(
|
|
426
|
+
data, {"ringVolume", "ringtoneId", "repeatTimes", "speakerVolume"}
|
|
427
|
+
)
|
|
428
|
+
return data
|
|
370
429
|
|
|
371
430
|
|
|
372
431
|
class RecordingSettings(ProtectBaseObject):
|
|
@@ -391,6 +450,8 @@ class RecordingSettings(ProtectBaseObject):
|
|
|
391
450
|
retention_duration: datetime | None = None
|
|
392
451
|
smart_detect_post_padding: timedelta | None = None
|
|
393
452
|
smart_detect_pre_padding: timedelta | None = None
|
|
453
|
+
# requires 5.2.39+
|
|
454
|
+
create_access_event: bool | None = None
|
|
394
455
|
|
|
395
456
|
@classmethod
|
|
396
457
|
@cache
|
|
@@ -473,6 +534,20 @@ class SmartDetectSettings(ProtectBaseObject):
|
|
|
473
534
|
"autoTrackingObjectTypes": convert_smart_types,
|
|
474
535
|
} | super().unifi_dict_conversions()
|
|
475
536
|
|
|
537
|
+
def unifi_dict(
|
|
538
|
+
self,
|
|
539
|
+
data: dict[str, Any] | None = None,
|
|
540
|
+
exclude: set[str] | None = None,
|
|
541
|
+
) -> dict[str, Any]:
|
|
542
|
+
data = super().unifi_dict(data=data, exclude=exclude)
|
|
543
|
+
if audio_types := data.get("audioTypes"):
|
|
544
|
+
# SMOKE_CMONX is not supported for audio types
|
|
545
|
+
# and should not be sent to the camera
|
|
546
|
+
data["audioTypes"] = [
|
|
547
|
+
t for t in audio_types if t != SmartDetectAudioType.SMOKE_CMONX.value
|
|
548
|
+
]
|
|
549
|
+
return data
|
|
550
|
+
|
|
476
551
|
|
|
477
552
|
class LCDMessage(ProtectBaseObject):
|
|
478
553
|
type: DoorbellMessageType
|
|
@@ -530,10 +605,10 @@ class LCDMessage(ProtectBaseObject):
|
|
|
530
605
|
class TalkbackSettings(ProtectBaseObject):
|
|
531
606
|
type_fmt: AudioCodecs
|
|
532
607
|
type_in: str
|
|
533
|
-
bind_addr: IPv4Address
|
|
608
|
+
bind_addr: IPv4Address | None = None
|
|
534
609
|
bind_port: int
|
|
535
|
-
filter_addr: str | None # can be used to restrict sender address
|
|
536
|
-
filter_port: int | None # can be used to restrict sender port
|
|
610
|
+
filter_addr: str | None = None # can be used to restrict sender address
|
|
611
|
+
filter_port: int | None = None # can be used to restrict sender port
|
|
537
612
|
channels: int # 1 or 2
|
|
538
613
|
sampling_rate: int # 8000, 11025, 22050, 44100, 48000
|
|
539
614
|
bits_per_sample: int
|
|
@@ -541,22 +616,22 @@ class TalkbackSettings(ProtectBaseObject):
|
|
|
541
616
|
|
|
542
617
|
|
|
543
618
|
class WifiStats(ProtectBaseObject):
|
|
544
|
-
channel: int | None
|
|
545
|
-
frequency: int | None
|
|
546
|
-
link_speed_mbps: str | None
|
|
619
|
+
channel: int | None = None
|
|
620
|
+
frequency: int | None = None
|
|
621
|
+
link_speed_mbps: str | None = None
|
|
547
622
|
signal_quality: PercentInt
|
|
548
623
|
signal_strength: int
|
|
549
624
|
|
|
550
625
|
|
|
551
626
|
class VideoStats(ProtectBaseObject):
|
|
552
|
-
recording_start: datetime | None
|
|
553
|
-
recording_end: datetime | None
|
|
554
|
-
recording_start_lq: datetime | None
|
|
555
|
-
recording_end_lq: datetime | None
|
|
556
|
-
timelapse_start: datetime | None
|
|
557
|
-
timelapse_end: datetime | None
|
|
558
|
-
timelapse_start_lq: datetime | None
|
|
559
|
-
timelapse_end_lq: datetime | None
|
|
627
|
+
recording_start: datetime | None = None
|
|
628
|
+
recording_end: datetime | None = None
|
|
629
|
+
recording_start_lq: datetime | None = None
|
|
630
|
+
recording_end_lq: datetime | None = None
|
|
631
|
+
timelapse_start: datetime | None = None
|
|
632
|
+
timelapse_end: datetime | None = None
|
|
633
|
+
timelapse_start_lq: datetime | None = None
|
|
634
|
+
timelapse_end_lq: datetime | None = None
|
|
560
635
|
|
|
561
636
|
@classmethod
|
|
562
637
|
@cache
|
|
@@ -572,24 +647,27 @@ class VideoStats(ProtectBaseObject):
|
|
|
572
647
|
@classmethod
|
|
573
648
|
@cache
|
|
574
649
|
def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
|
|
575
|
-
return
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
650
|
+
return (
|
|
651
|
+
dict.fromkeys(
|
|
652
|
+
(
|
|
653
|
+
"recordingStart",
|
|
654
|
+
"recordingEnd",
|
|
655
|
+
"recordingStartLQ",
|
|
656
|
+
"recordingEndLQ",
|
|
657
|
+
"timelapseStart",
|
|
658
|
+
"timelapseEnd",
|
|
659
|
+
"timelapseStartLQ",
|
|
660
|
+
"timelapseEndLQ",
|
|
661
|
+
),
|
|
662
|
+
convert_to_datetime,
|
|
586
663
|
)
|
|
587
|
-
|
|
664
|
+
| super().unifi_dict_conversions()
|
|
665
|
+
)
|
|
588
666
|
|
|
589
667
|
|
|
590
668
|
class StorageStats(ProtectBaseObject):
|
|
591
|
-
used: int | None # bytes
|
|
592
|
-
rate: float | None # bytes / millisecond
|
|
669
|
+
used: int | None = None # bytes
|
|
670
|
+
rate: float | None = None # bytes / millisecond
|
|
593
671
|
|
|
594
672
|
@property
|
|
595
673
|
def rate_per_second(self) -> float | None:
|
|
@@ -611,10 +689,7 @@ class StorageStats(ProtectBaseObject):
|
|
|
611
689
|
exclude: set[str] | None = None,
|
|
612
690
|
) -> dict[str, Any]:
|
|
613
691
|
data = super().unifi_dict(data=data, exclude=exclude)
|
|
614
|
-
|
|
615
|
-
if "rate" in data and data["rate"] is None:
|
|
616
|
-
del data["rate"]
|
|
617
|
-
|
|
692
|
+
pop_dict_set_if_none(data, {"rate"})
|
|
618
693
|
return data
|
|
619
694
|
|
|
620
695
|
|
|
@@ -623,7 +698,7 @@ class CameraStats(ProtectBaseObject):
|
|
|
623
698
|
tx_bytes: int
|
|
624
699
|
wifi: WifiStats
|
|
625
700
|
video: VideoStats
|
|
626
|
-
storage: StorageStats | None
|
|
701
|
+
storage: StorageStats | None = None
|
|
627
702
|
wifi_quality: PercentInt
|
|
628
703
|
wifi_strength: int
|
|
629
704
|
|
|
@@ -670,6 +745,10 @@ class CameraZone(ProtectBaseObject):
|
|
|
670
745
|
if "points" in data:
|
|
671
746
|
data["points"] = [serialize_point(p) for p in data["points"]]
|
|
672
747
|
|
|
748
|
+
if "color" in data and isinstance(data["color"], dict):
|
|
749
|
+
# Serialize Color object to hex string to avoid Pydantic serialization warnings
|
|
750
|
+
data["color"] = self.color.as_hex().upper()
|
|
751
|
+
|
|
673
752
|
return data
|
|
674
753
|
|
|
675
754
|
@staticmethod
|
|
@@ -698,7 +777,7 @@ class SmartMotionZone(MotionZone):
|
|
|
698
777
|
|
|
699
778
|
|
|
700
779
|
class PrivacyMaskCapability(ProtectBaseObject):
|
|
701
|
-
max_masks: int | None
|
|
780
|
+
max_masks: int | None = None
|
|
702
781
|
rectangle_only: bool
|
|
703
782
|
|
|
704
783
|
|
|
@@ -725,9 +804,9 @@ class Hotplug(ProtectBaseObject):
|
|
|
725
804
|
|
|
726
805
|
|
|
727
806
|
class PTZRangeSingle(ProtectBaseObject):
|
|
728
|
-
max: float | None
|
|
729
|
-
min: float | None
|
|
730
|
-
step: float | None
|
|
807
|
+
max: float | None = None
|
|
808
|
+
min: float | None = None
|
|
809
|
+
step: float | None = None
|
|
731
810
|
|
|
732
811
|
|
|
733
812
|
class PTZRange(ProtectBaseObject):
|
|
@@ -761,7 +840,7 @@ class PTZRange(ProtectBaseObject):
|
|
|
761
840
|
|
|
762
841
|
|
|
763
842
|
class PTZZoomRange(PTZRange):
|
|
764
|
-
ratio:
|
|
843
|
+
ratio: int
|
|
765
844
|
|
|
766
845
|
def to_native_value(self, zoom_value: float, is_relative: bool = False) -> float:
|
|
767
846
|
"""Convert zoom values to step values."""
|
|
@@ -838,6 +917,11 @@ class CameraFeatureFlags(ProtectBaseObject):
|
|
|
838
917
|
has_vertical_flip: bool | None = None
|
|
839
918
|
# 3.0.22+
|
|
840
919
|
flash_range: Any | None = None
|
|
920
|
+
# 4.73.71+
|
|
921
|
+
support_nfc: bool | None = None
|
|
922
|
+
has_fingerprint_sensor: bool | None = None
|
|
923
|
+
# 6.0.0+
|
|
924
|
+
support_full_hd_snapshot: bool | None = None
|
|
841
925
|
|
|
842
926
|
focus: PTZRange
|
|
843
927
|
pan: PTZRange
|
|
@@ -891,6 +975,15 @@ class CameraAudioSettings(ProtectBaseObject):
|
|
|
891
975
|
style: list[AudioStyle]
|
|
892
976
|
|
|
893
977
|
|
|
978
|
+
@lru_cache
|
|
979
|
+
def _chime_type_from_total_seconds(total_seconds: float) -> ChimeType:
|
|
980
|
+
if total_seconds == 0.3:
|
|
981
|
+
return ChimeType.MECHANICAL
|
|
982
|
+
if total_seconds > 0.3:
|
|
983
|
+
return ChimeType.DIGITAL
|
|
984
|
+
return ChimeType.NONE
|
|
985
|
+
|
|
986
|
+
|
|
894
987
|
class Camera(ProtectMotionDeviceModel):
|
|
895
988
|
is_deleting: bool
|
|
896
989
|
# Microphone Sensitivity
|
|
@@ -899,13 +992,13 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
899
992
|
is_recording: bool
|
|
900
993
|
is_motion_detected: bool
|
|
901
994
|
is_smart_detected: bool
|
|
902
|
-
phy_rate: float | None
|
|
995
|
+
phy_rate: float | None = None
|
|
903
996
|
hdr_mode: bool
|
|
904
997
|
# Recording Quality -> High Frame
|
|
905
998
|
video_mode: VideoMode
|
|
906
999
|
is_probing_for_wifi: bool
|
|
907
1000
|
chime_duration: timedelta
|
|
908
|
-
last_ring: datetime | None
|
|
1001
|
+
last_ring: datetime | None = None
|
|
909
1002
|
is_live_heatmap_enabled: bool
|
|
910
1003
|
video_reconfiguration_in_progress: bool
|
|
911
1004
|
channels: list[CameraChannel]
|
|
@@ -921,18 +1014,18 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
921
1014
|
smart_detect_zones: list[SmartMotionZone]
|
|
922
1015
|
stats: CameraStats
|
|
923
1016
|
feature_flags: CameraFeatureFlags
|
|
924
|
-
lcd_message: LCDMessage | None
|
|
1017
|
+
lcd_message: LCDMessage | None = None
|
|
925
1018
|
lenses: list[CameraLenses]
|
|
926
|
-
platform: str
|
|
1019
|
+
platform: str | None = None
|
|
927
1020
|
has_speaker: bool
|
|
928
1021
|
has_wifi: bool
|
|
929
1022
|
audio_bitrate: int
|
|
930
1023
|
can_manage: bool
|
|
931
1024
|
is_managed: bool
|
|
932
|
-
voltage: float | None
|
|
1025
|
+
voltage: float | None = None
|
|
933
1026
|
# requires 1.21+
|
|
934
|
-
is_poor_network: bool | None
|
|
935
|
-
is_wireless_uplink_enabled: bool | None
|
|
1027
|
+
is_poor_network: bool | None = None
|
|
1028
|
+
is_wireless_uplink_enabled: bool | None = None
|
|
936
1029
|
# requires 2.6.13+
|
|
937
1030
|
homekit_settings: CameraHomekitSettings | None = None
|
|
938
1031
|
# requires 2.6.17+
|
|
@@ -952,7 +1045,12 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
952
1045
|
is_ptz: bool | None = None
|
|
953
1046
|
# requires 2.11.13+
|
|
954
1047
|
audio_settings: CameraAudioSettings | None = None
|
|
955
|
-
|
|
1048
|
+
# requires 5.0.33+
|
|
1049
|
+
is_third_party_camera: bool | None = None
|
|
1050
|
+
# requires 5.1.78+
|
|
1051
|
+
is_paired_with_ai_port: bool | None = None
|
|
1052
|
+
# requires 5.2.39+
|
|
1053
|
+
is_adopted_by_access_app: bool | None = None
|
|
956
1054
|
# TODO: used for adopting
|
|
957
1055
|
# apMac read only
|
|
958
1056
|
# apRssi read only
|
|
@@ -968,6 +1066,10 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
968
1066
|
|
|
969
1067
|
# not directly from UniFi
|
|
970
1068
|
last_ring_event_id: str | None = None
|
|
1069
|
+
last_nfc_card_scanned_event_id: str | None = None
|
|
1070
|
+
last_nfc_card_scanned: datetime | None = None
|
|
1071
|
+
last_fingerprint_identified_event_id: str | None = None
|
|
1072
|
+
last_fingerprint_identified: datetime | None = None
|
|
971
1073
|
last_smart_detect: datetime | None = None
|
|
972
1074
|
last_smart_audio_detect: datetime | None = None
|
|
973
1075
|
last_smart_detect_event_id: str | None = None
|
|
@@ -977,7 +1079,6 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
977
1079
|
last_smart_detect_event_ids: dict[SmartDetectObjectType, str] = {}
|
|
978
1080
|
last_smart_audio_detect_event_ids: dict[SmartDetectAudioType, str] = {}
|
|
979
1081
|
talkback_stream: TalkbackStream | None = None
|
|
980
|
-
_last_ring_timeout: datetime | None = PrivateAttr(None)
|
|
981
1082
|
|
|
982
1083
|
@classmethod
|
|
983
1084
|
@cache
|
|
@@ -989,6 +1090,10 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
989
1090
|
def _get_excluded_changed_fields(cls) -> set[str]:
|
|
990
1091
|
return super()._get_excluded_changed_fields() | {
|
|
991
1092
|
"last_ring_event_id",
|
|
1093
|
+
"last_nfc_card_scanned",
|
|
1094
|
+
"last_nfc_card_scanned_event_id",
|
|
1095
|
+
"last_fingerprint_identified",
|
|
1096
|
+
"last_fingerprint_identified_event_id",
|
|
992
1097
|
"last_smart_detect",
|
|
993
1098
|
"last_smart_audio_detect",
|
|
994
1099
|
"last_smart_detect_event_id",
|
|
@@ -1026,6 +1131,13 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
1026
1131
|
"chimeDuration": lambda x: timedelta(milliseconds=x),
|
|
1027
1132
|
} | super().unifi_dict_conversions()
|
|
1028
1133
|
|
|
1134
|
+
@model_validator(mode="after")
|
|
1135
|
+
def _set_channel_parents(self) -> Camera:
|
|
1136
|
+
"""Set parent camera reference in channels after initialization."""
|
|
1137
|
+
for channel in self.channels:
|
|
1138
|
+
channel._parent = self
|
|
1139
|
+
return self
|
|
1140
|
+
|
|
1029
1141
|
@classmethod
|
|
1030
1142
|
def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
|
|
1031
1143
|
# LCD messages comes back as empty dict {}
|
|
@@ -1054,21 +1166,25 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
1054
1166
|
]
|
|
1055
1167
|
|
|
1056
1168
|
data = super().unifi_dict(data=data, exclude=exclude)
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1169
|
+
pop_dict_tuple(
|
|
1170
|
+
data,
|
|
1171
|
+
(
|
|
1172
|
+
"lastFingerprintIdentified",
|
|
1173
|
+
"lastFingerprintIdentifiedEventId",
|
|
1174
|
+
"lastNfcCardScanned",
|
|
1175
|
+
"lastNfcCardScannedEventId",
|
|
1176
|
+
"lastRingEventId",
|
|
1177
|
+
"lastSmartDetect",
|
|
1178
|
+
"lastSmartAudioDetect",
|
|
1179
|
+
"lastSmartDetectEventId",
|
|
1180
|
+
"lastSmartAudioDetectEventId",
|
|
1181
|
+
"lastSmartDetects",
|
|
1182
|
+
"lastSmartAudioDetects",
|
|
1183
|
+
"lastSmartDetectEventIds",
|
|
1184
|
+
"lastSmartAudioDetectEventIds",
|
|
1185
|
+
"talkbackStream",
|
|
1186
|
+
),
|
|
1187
|
+
)
|
|
1072
1188
|
if "lcdMessage" in data and data["lcdMessage"] is None:
|
|
1073
1189
|
data["lcdMessage"] = {}
|
|
1074
1190
|
|
|
@@ -1084,7 +1200,7 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
1084
1200
|
updated["lcd_message"] = {"reset_at": utc_now() - timedelta(seconds=10)}
|
|
1085
1201
|
# otherwise, pass full LCD message to prevent issues
|
|
1086
1202
|
elif self.lcd_message is not None:
|
|
1087
|
-
updated["lcd_message"] = self.lcd_message.
|
|
1203
|
+
updated["lcd_message"] = self.lcd_message.model_dump()
|
|
1088
1204
|
|
|
1089
1205
|
# if reset_at is not passed in, it will default to reset in 1 minute
|
|
1090
1206
|
if lcd_message is not None and "reset_at" not in lcd_message:
|
|
@@ -1096,12 +1212,14 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
1096
1212
|
return updated
|
|
1097
1213
|
|
|
1098
1214
|
def update_from_dict(self, data: dict[str, Any]) -> Camera:
|
|
1099
|
-
# a message in the past is actually a
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1215
|
+
# a message in the past is actually a signal to wipe the message
|
|
1216
|
+
if (
|
|
1217
|
+
reset_at := data.get("lcd_message", {}).get("reset_at")
|
|
1218
|
+
) is not None and utc_now() > from_js_time(reset_at):
|
|
1219
|
+
# Important: Make a copy of the data before modifying it
|
|
1220
|
+
# since unifi_dict_to_dict will otherwise report incorrect changes
|
|
1221
|
+
data = data.copy()
|
|
1222
|
+
data["lcd_message"] = None
|
|
1105
1223
|
|
|
1106
1224
|
return super().update_from_dict(data)
|
|
1107
1225
|
|
|
@@ -1118,6 +1236,23 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
1118
1236
|
return None
|
|
1119
1237
|
return self._api.bootstrap.events.get(last_smart_detect_event_id)
|
|
1120
1238
|
|
|
1239
|
+
@property
|
|
1240
|
+
def last_nfc_card_scanned_event(self) -> Event | None:
|
|
1241
|
+
if (
|
|
1242
|
+
last_nfc_card_scanned_event_id := self.last_nfc_card_scanned_event_id
|
|
1243
|
+
) is None:
|
|
1244
|
+
return None
|
|
1245
|
+
return self._api.bootstrap.events.get(last_nfc_card_scanned_event_id)
|
|
1246
|
+
|
|
1247
|
+
@property
|
|
1248
|
+
def last_fingerprint_identified_event(self) -> Event | None:
|
|
1249
|
+
if (
|
|
1250
|
+
last_fingerprint_identified_event_id
|
|
1251
|
+
:= self.last_fingerprint_identified_event_id
|
|
1252
|
+
) is None:
|
|
1253
|
+
return None
|
|
1254
|
+
return self._api.bootstrap.events.get(last_fingerprint_identified_event_id)
|
|
1255
|
+
|
|
1121
1256
|
@property
|
|
1122
1257
|
def hdr_mode_display(self) -> Literal["auto", "off", "always"]:
|
|
1123
1258
|
"""Get HDR mode similar to how Protect interface works."""
|
|
@@ -1140,11 +1275,9 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
1140
1275
|
smart_type: SmartDetectObjectType,
|
|
1141
1276
|
) -> Event | None:
|
|
1142
1277
|
"""Get the last smart detect event for given type."""
|
|
1143
|
-
event_id
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
return self._api.bootstrap.events.get(event_id)
|
|
1278
|
+
if event_id := self.last_smart_detect_event_ids.get(smart_type):
|
|
1279
|
+
return self._api.bootstrap.events.get(event_id)
|
|
1280
|
+
return None
|
|
1148
1281
|
|
|
1149
1282
|
@property
|
|
1150
1283
|
def last_smart_audio_detect_event(self) -> Event | None:
|
|
@@ -1228,19 +1361,20 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
1228
1361
|
"""Get active smart detection types."""
|
|
1229
1362
|
if self.use_global:
|
|
1230
1363
|
return set(self.smart_detect_settings.object_types).intersection(
|
|
1231
|
-
self.feature_flags.smart_detect_types
|
|
1364
|
+
self.feature_flags.smart_detect_types
|
|
1232
1365
|
)
|
|
1233
1366
|
return set(self.smart_detect_settings.object_types)
|
|
1234
1367
|
|
|
1235
1368
|
@property
|
|
1236
1369
|
def active_audio_detect_types(self) -> set[SmartDetectAudioType]:
|
|
1237
1370
|
"""Get active audio detection types."""
|
|
1371
|
+
if not (enabled_audio_types := self.smart_detect_settings.audio_types):
|
|
1372
|
+
return set()
|
|
1238
1373
|
if self.use_global:
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
)
|
|
1242
|
-
|
|
1243
|
-
return set(self.smart_detect_settings.audio_types or [])
|
|
1374
|
+
if not (feature_audio_types := self.feature_flags.smart_detect_audio_types):
|
|
1375
|
+
return set()
|
|
1376
|
+
return set(feature_audio_types).intersection(enabled_audio_types)
|
|
1377
|
+
return set(enabled_audio_types)
|
|
1244
1378
|
|
|
1245
1379
|
@property
|
|
1246
1380
|
def is_motion_detection_on(self) -> bool:
|
|
@@ -1382,6 +1516,40 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
1382
1516
|
"""Toggles vehicle smart detection. Requires camera to have smart detection"""
|
|
1383
1517
|
return await self._set_object_detect(SmartDetectObjectType.VEHICLE, enabled)
|
|
1384
1518
|
|
|
1519
|
+
# endregion
|
|
1520
|
+
# region Face
|
|
1521
|
+
|
|
1522
|
+
@property
|
|
1523
|
+
def can_detect_face(self) -> bool:
|
|
1524
|
+
return SmartDetectObjectType.FACE in self.feature_flags.smart_detect_types
|
|
1525
|
+
|
|
1526
|
+
@property
|
|
1527
|
+
def is_face_detection_on(self) -> bool:
|
|
1528
|
+
"""Is Face Detection available and enabled?"""
|
|
1529
|
+
return self._is_smart_enabled(SmartDetectObjectType.FACE)
|
|
1530
|
+
|
|
1531
|
+
@property
|
|
1532
|
+
def last_face_detect_event(self) -> Event | None:
|
|
1533
|
+
"""Get the last face smart detection event."""
|
|
1534
|
+
return self.get_last_smart_detect_event(SmartDetectObjectType.FACE)
|
|
1535
|
+
|
|
1536
|
+
@property
|
|
1537
|
+
def last_face_detect(self) -> datetime | None:
|
|
1538
|
+
"""Get the last face smart detection event."""
|
|
1539
|
+
return self.last_smart_detects.get(SmartDetectObjectType.FACE)
|
|
1540
|
+
|
|
1541
|
+
@property
|
|
1542
|
+
def is_face_currently_detected(self) -> bool:
|
|
1543
|
+
"""Is face currently being detected"""
|
|
1544
|
+
return self._is_smart_detected(SmartDetectObjectType.FACE)
|
|
1545
|
+
|
|
1546
|
+
async def set_face_detection(self, enabled: bool) -> None:
|
|
1547
|
+
"""Toggles face smart detection. Requires camera to have smart detection"""
|
|
1548
|
+
return await self._set_object_detect(
|
|
1549
|
+
SmartDetectObjectType.FACE,
|
|
1550
|
+
enabled,
|
|
1551
|
+
)
|
|
1552
|
+
|
|
1385
1553
|
# endregion
|
|
1386
1554
|
# region License Plate
|
|
1387
1555
|
|
|
@@ -1846,18 +2014,14 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
1846
2014
|
# endregion
|
|
1847
2015
|
|
|
1848
2016
|
@property
|
|
1849
|
-
def
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
2017
|
+
def chime_type(self) -> ChimeType:
|
|
2018
|
+
return _chime_type_from_total_seconds(
|
|
2019
|
+
timedelta_total_seconds(self.chime_duration)
|
|
2020
|
+
)
|
|
1853
2021
|
|
|
1854
2022
|
@property
|
|
1855
|
-
def
|
|
1856
|
-
|
|
1857
|
-
return ChimeType.MECHANICAL
|
|
1858
|
-
if self.chime_duration.total_seconds() > 0.3:
|
|
1859
|
-
return ChimeType.DIGITAL
|
|
1860
|
-
return ChimeType.NONE
|
|
2023
|
+
def chime_duration_seconds(self) -> float:
|
|
2024
|
+
return timedelta_total_seconds(self.chime_duration)
|
|
1861
2025
|
|
|
1862
2026
|
@property
|
|
1863
2027
|
def is_digital_chime(self) -> bool:
|
|
@@ -1935,10 +2099,6 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
1935
2099
|
|
|
1936
2100
|
return False
|
|
1937
2101
|
|
|
1938
|
-
def set_ring_timeout(self) -> None:
|
|
1939
|
-
self._last_ring_timeout = utc_now() + EVENT_PING_INTERVAL
|
|
1940
|
-
self._event_callback_ping()
|
|
1941
|
-
|
|
1942
2102
|
def get_privacy_zone(self) -> tuple[int | None, CameraZone | None]:
|
|
1943
2103
|
for index, zone in enumerate(self.privacy_zones):
|
|
1944
2104
|
if zone.name == PRIVACY_ZONE_NAME:
|
|
@@ -1971,13 +2131,19 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
1971
2131
|
|
|
1972
2132
|
Datetime of screenshot is approximate. It may be +/- a few seconds.
|
|
1973
2133
|
"""
|
|
1974
|
-
if
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2134
|
+
# Use READ_LIVE if dt is None, otherwise READ_MEDIA
|
|
2135
|
+
auth_user = self._api.bootstrap.auth_user
|
|
2136
|
+
if dt is None:
|
|
2137
|
+
if not (
|
|
2138
|
+
auth_user.can(ModelType.CAMERA, PermissionNode.READ_LIVE, self)
|
|
2139
|
+
or auth_user.can(ModelType.CAMERA, PermissionNode.READ_MEDIA, self)
|
|
2140
|
+
):
|
|
2141
|
+
raise NotAuthorized(
|
|
2142
|
+
f"Do not have permission to read live or media for camera: {self.id}"
|
|
2143
|
+
)
|
|
2144
|
+
elif not auth_user.can(ModelType.CAMERA, PermissionNode.READ_MEDIA, self):
|
|
1979
2145
|
raise NotAuthorized(
|
|
1980
|
-
f"Do not have permission to read media for camera: {self.id}"
|
|
2146
|
+
f"Do not have permission to read media for camera: {self.id}"
|
|
1981
2147
|
)
|
|
1982
2148
|
|
|
1983
2149
|
if height is None and width is None and self.high_camera_channel is not None:
|
|
@@ -1985,6 +2151,43 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
1985
2151
|
|
|
1986
2152
|
return await self._api.get_camera_snapshot(self.id, width, height, dt=dt)
|
|
1987
2153
|
|
|
2154
|
+
async def get_public_api_snapshot(
|
|
2155
|
+
self, high_quality: bool | None = None
|
|
2156
|
+
) -> bytes | None:
|
|
2157
|
+
"""Gets snapshot for camera using public API."""
|
|
2158
|
+
if self._api._api_key is None:
|
|
2159
|
+
raise NotAuthorized("Cannot get public API snapshot without an API key.")
|
|
2160
|
+
|
|
2161
|
+
if high_quality is None:
|
|
2162
|
+
high_quality = self.feature_flags.support_full_hd_snapshot or False
|
|
2163
|
+
|
|
2164
|
+
return await self._api.get_public_api_camera_snapshot(
|
|
2165
|
+
camera_id=self.id, high_quality=high_quality
|
|
2166
|
+
)
|
|
2167
|
+
|
|
2168
|
+
async def create_rtsps_streams(
|
|
2169
|
+
self, qualities: list[str] | str
|
|
2170
|
+
) -> RTSPSStreams | None:
|
|
2171
|
+
"""Creates RTSPS streams for camera using public API."""
|
|
2172
|
+
if self._api._api_key is None:
|
|
2173
|
+
raise NotAuthorized("Cannot create RTSPS streams without an API key.")
|
|
2174
|
+
|
|
2175
|
+
return await self._api.create_camera_rtsps_streams(self.id, qualities)
|
|
2176
|
+
|
|
2177
|
+
async def get_rtsps_streams(self) -> RTSPSStreams | None:
|
|
2178
|
+
"""Gets existing RTSPS streams for camera using public API."""
|
|
2179
|
+
if self._api._api_key is None:
|
|
2180
|
+
raise NotAuthorized("Cannot get RTSPS streams without an API key.")
|
|
2181
|
+
|
|
2182
|
+
return await self._api.get_camera_rtsps_streams(self.id)
|
|
2183
|
+
|
|
2184
|
+
async def delete_rtsps_streams(self, qualities: list[str] | str) -> bool:
|
|
2185
|
+
"""Deletes RTSPS streams for camera using public API."""
|
|
2186
|
+
if self._api._api_key is None:
|
|
2187
|
+
raise NotAuthorized("Cannot delete RTSPS streams without an API key.")
|
|
2188
|
+
|
|
2189
|
+
return await self._api.delete_camera_rtsps_streams(self.id, qualities)
|
|
2190
|
+
|
|
1988
2191
|
async def get_package_snapshot(
|
|
1989
2192
|
self,
|
|
1990
2193
|
width: int | None = None,
|
|
@@ -1999,13 +2202,19 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
1999
2202
|
if not self.feature_flags.has_package_camera:
|
|
2000
2203
|
raise BadRequest("Device does not have package camera")
|
|
2001
2204
|
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2205
|
+
auth_user = self._api.bootstrap.auth_user
|
|
2206
|
+
# Use READ_LIVE if dt is None, otherwise READ_MEDIA
|
|
2207
|
+
if dt is None:
|
|
2208
|
+
if not (
|
|
2209
|
+
auth_user.can(ModelType.CAMERA, PermissionNode.READ_LIVE, self)
|
|
2210
|
+
or auth_user.can(ModelType.CAMERA, PermissionNode.READ_MEDIA, self)
|
|
2211
|
+
):
|
|
2212
|
+
raise NotAuthorized(
|
|
2213
|
+
f"Do not have permission to read live or media for camera: {self.id}"
|
|
2214
|
+
)
|
|
2215
|
+
elif not auth_user.can(ModelType.CAMERA, PermissionNode.READ_MEDIA, self):
|
|
2007
2216
|
raise NotAuthorized(
|
|
2008
|
-
f"Do not have permission to read media for camera: {self.id}"
|
|
2217
|
+
f"Do not have permission to read media for camera: {self.id}"
|
|
2009
2218
|
)
|
|
2010
2219
|
|
|
2011
2220
|
if height is None and width is None and self.package_camera_channel is not None:
|
|
@@ -2111,7 +2320,9 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
2111
2320
|
|
|
2112
2321
|
def callback() -> None:
|
|
2113
2322
|
self.led_settings.is_enabled = enabled
|
|
2114
|
-
|
|
2323
|
+
# blink_rate was removed in Protect 6.x
|
|
2324
|
+
if self.led_settings.blink_rate is not None:
|
|
2325
|
+
self.led_settings.blink_rate = 0
|
|
2115
2326
|
|
|
2116
2327
|
await self.queue_update(callback)
|
|
2117
2328
|
|
|
@@ -2201,7 +2412,17 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
2201
2412
|
await self.queue_update(callback)
|
|
2202
2413
|
|
|
2203
2414
|
async def set_speaker_volume(self, level: int) -> None:
|
|
2204
|
-
"""Sets the speaker
|
|
2415
|
+
"""Sets the speaker output volume on camera. Requires camera to have speakers"""
|
|
2416
|
+
if not self.feature_flags.has_speaker:
|
|
2417
|
+
raise BadRequest("Camera does not have speaker")
|
|
2418
|
+
|
|
2419
|
+
def callback() -> None:
|
|
2420
|
+
self.speaker_settings.speaker_volume = PercentInt(level)
|
|
2421
|
+
|
|
2422
|
+
await self.queue_update(callback)
|
|
2423
|
+
|
|
2424
|
+
async def set_volume(self, level: int) -> None:
|
|
2425
|
+
"""Sets the general volume level on camera. Requires camera to have speakers"""
|
|
2205
2426
|
if not self.feature_flags.has_speaker:
|
|
2206
2427
|
raise BadRequest("Camera does not have speaker")
|
|
2207
2428
|
|
|
@@ -2210,6 +2431,16 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
2210
2431
|
|
|
2211
2432
|
await self.queue_update(callback)
|
|
2212
2433
|
|
|
2434
|
+
async def set_ring_volume(self, level: int) -> None:
|
|
2435
|
+
"""Sets the doorbell ring volume. Requires camera to be a doorbell"""
|
|
2436
|
+
if not self.feature_flags.is_doorbell:
|
|
2437
|
+
raise BadRequest("Camera is not a doorbell")
|
|
2438
|
+
|
|
2439
|
+
def callback() -> None:
|
|
2440
|
+
self.speaker_settings.ring_volume = PercentInt(level)
|
|
2441
|
+
|
|
2442
|
+
await self.queue_update(callback)
|
|
2443
|
+
|
|
2213
2444
|
async def set_chime_type(self, chime_type: ChimeType) -> None:
|
|
2214
2445
|
"""Sets chime type for doorbell. Requires camera to be a doorbell"""
|
|
2215
2446
|
await self.set_chime_duration(timedelta(milliseconds=chime_type.value))
|
|
@@ -2716,8 +2947,8 @@ class SensorThresholdSettings(SensorSettingsBase):
|
|
|
2716
2947
|
margin: float # read only
|
|
2717
2948
|
# "safe" thresholds for alerting
|
|
2718
2949
|
# anything below/above will trigger alert
|
|
2719
|
-
low_threshold: float | None
|
|
2720
|
-
high_threshold: float | None
|
|
2950
|
+
low_threshold: float | None = None
|
|
2951
|
+
high_threshold: float | None = None
|
|
2721
2952
|
|
|
2722
2953
|
|
|
2723
2954
|
class SensorSensitivitySettings(SensorSettingsBase):
|
|
@@ -2725,12 +2956,12 @@ class SensorSensitivitySettings(SensorSettingsBase):
|
|
|
2725
2956
|
|
|
2726
2957
|
|
|
2727
2958
|
class SensorBatteryStatus(ProtectBaseObject):
|
|
2728
|
-
percentage: PercentInt | None
|
|
2959
|
+
percentage: PercentInt | None = None
|
|
2729
2960
|
is_low: bool
|
|
2730
2961
|
|
|
2731
2962
|
|
|
2732
2963
|
class SensorStat(ProtectBaseObject):
|
|
2733
|
-
value: float | None
|
|
2964
|
+
value: float | None = None
|
|
2734
2965
|
status: SensorStatusType
|
|
2735
2966
|
|
|
2736
2967
|
|
|
@@ -2742,20 +2973,20 @@ class SensorStats(ProtectBaseObject):
|
|
|
2742
2973
|
|
|
2743
2974
|
class Sensor(ProtectAdoptableDeviceModel):
|
|
2744
2975
|
alarm_settings: SensorSettingsBase
|
|
2745
|
-
alarm_triggered_at: datetime | None
|
|
2976
|
+
alarm_triggered_at: datetime | None = None
|
|
2746
2977
|
battery_status: SensorBatteryStatus
|
|
2747
|
-
camera_id: str | None
|
|
2978
|
+
camera_id: str | None = None
|
|
2748
2979
|
humidity_settings: SensorThresholdSettings
|
|
2749
2980
|
is_motion_detected: bool
|
|
2750
2981
|
is_opened: bool
|
|
2751
|
-
leak_detected_at: datetime | None
|
|
2752
|
-
led_settings:
|
|
2982
|
+
leak_detected_at: datetime | None = None
|
|
2983
|
+
led_settings: LEDSettings
|
|
2753
2984
|
light_settings: SensorThresholdSettings
|
|
2754
|
-
motion_detected_at: datetime | None
|
|
2985
|
+
motion_detected_at: datetime | None = None
|
|
2755
2986
|
motion_settings: SensorSensitivitySettings
|
|
2756
|
-
open_status_changed_at: datetime | None
|
|
2987
|
+
open_status_changed_at: datetime | None = None
|
|
2757
2988
|
stats: SensorStats
|
|
2758
|
-
tampering_detected_at: datetime | None
|
|
2989
|
+
tampering_detected_at: datetime | None = None
|
|
2759
2990
|
temperature_settings: SensorThresholdSettings
|
|
2760
2991
|
mount_type: MountType
|
|
2761
2992
|
|
|
@@ -2794,15 +3025,16 @@ class Sensor(ProtectAdoptableDeviceModel):
|
|
|
2794
3025
|
exclude: set[str] | None = None,
|
|
2795
3026
|
) -> dict[str, Any]:
|
|
2796
3027
|
data = super().unifi_dict(data=data, exclude=exclude)
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
3028
|
+
pop_dict_tuple(
|
|
3029
|
+
data,
|
|
3030
|
+
(
|
|
3031
|
+
"lastMotionEventId",
|
|
3032
|
+
"lastContactEventId",
|
|
3033
|
+
"lastValueEventId",
|
|
3034
|
+
"lastAlarmEventId",
|
|
3035
|
+
"extremeValueDetectedAt",
|
|
3036
|
+
),
|
|
3037
|
+
)
|
|
2806
3038
|
return data
|
|
2807
3039
|
|
|
2808
3040
|
@property
|
|
@@ -3048,13 +3280,13 @@ class Sensor(ProtectAdoptableDeviceModel):
|
|
|
3048
3280
|
|
|
3049
3281
|
|
|
3050
3282
|
class Doorlock(ProtectAdoptableDeviceModel):
|
|
3051
|
-
credentials: str | None
|
|
3283
|
+
credentials: str | None = None
|
|
3052
3284
|
lock_status: LockStatusType
|
|
3053
3285
|
enable_homekit: bool
|
|
3054
3286
|
auto_close_time: timedelta
|
|
3055
3287
|
led_settings: SensorSettingsBase
|
|
3056
3288
|
battery_status: SensorBatteryStatus
|
|
3057
|
-
camera_id: str | None
|
|
3289
|
+
camera_id: str | None = None
|
|
3058
3290
|
has_homekit: bool
|
|
3059
3291
|
private_token: str
|
|
3060
3292
|
|
|
@@ -3191,7 +3423,7 @@ class ChimeTrack(ProtectBaseObject):
|
|
|
3191
3423
|
class Chime(ProtectAdoptableDeviceModel):
|
|
3192
3424
|
volume: PercentInt
|
|
3193
3425
|
is_probing_for_wifi: bool
|
|
3194
|
-
last_ring: datetime | None
|
|
3426
|
+
last_ring: datetime | None = None
|
|
3195
3427
|
is_wireless_uplink_enabled: bool
|
|
3196
3428
|
camera_ids: list[str]
|
|
3197
3429
|
# requires 2.6.17+
|
|
@@ -3326,3 +3558,16 @@ class Chime(ProtectAdoptableDeviceModel):
|
|
|
3326
3558
|
raise BadRequest("Camera %s is not paired with chime", camera.id)
|
|
3327
3559
|
|
|
3328
3560
|
await self.queue_update(callback)
|
|
3561
|
+
|
|
3562
|
+
|
|
3563
|
+
class AiPort(Camera):
|
|
3564
|
+
paired_cameras: list[str]
|
|
3565
|
+
|
|
3566
|
+
|
|
3567
|
+
class Ringtone(ProtectBaseObject):
|
|
3568
|
+
id: str
|
|
3569
|
+
name: str
|
|
3570
|
+
size: int
|
|
3571
|
+
is_default: bool
|
|
3572
|
+
nvr_mac: str
|
|
3573
|
+
model_key: str
|