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/data/nvr.py CHANGED
@@ -15,11 +15,12 @@ from uuid import UUID
15
15
 
16
16
  import aiofiles
17
17
  import orjson
18
- from aiofiles import os as aos
19
- from pydantic.v1.fields import PrivateAttr
18
+ from convertertools import pop_dict_set_if_none, pop_dict_tuple
19
+ from pydantic import ConfigDict
20
+ from pydantic.fields import PrivateAttr
20
21
 
21
22
  from ..exceptions import BadRequest, NotAuthorized
22
- from ..utils import RELEASE_CACHE, convert_to_datetime
23
+ from ..utils import convert_to_datetime
23
24
  from .base import (
24
25
  ProtectBaseObject,
25
26
  ProtectDeviceModel,
@@ -45,7 +46,6 @@ from .types import (
45
46
  ModelType,
46
47
  MountType,
47
48
  PercentFloat,
48
- PercentInt,
49
49
  PermissionNode,
50
50
  ProgressCallback,
51
51
  RecordingMode,
@@ -60,7 +60,7 @@ from .types import (
60
60
  from .user import User, UserLocation
61
61
 
62
62
  if TYPE_CHECKING:
63
- from pydantic.v1.typing import SetStr
63
+ from pydantic.typing import SetStr
64
64
 
65
65
 
66
66
  _LOGGER = logging.getLogger(__name__)
@@ -70,27 +70,46 @@ DELETE_KEYS_THUMB = {"color", "vehicleType"}
70
70
  DELETE_KEYS_EVENT = {"deletedAt", "category", "subCategory"}
71
71
 
72
72
 
73
+ class MetaInfo(ProtectBaseObject):
74
+ applicationVersion: str
75
+
76
+
73
77
  class NVRLocation(UserLocation):
74
78
  is_geofencing_enabled: bool
75
79
  radius: int
76
80
  model: ModelType | None = None
77
81
 
78
82
 
83
+ class SmartDetectItemAttribute(ProtectBaseObject):
84
+ """Attribute value with confidence for smart detect items (e.g., color, vehicle type)."""
85
+
86
+ val: str
87
+ confidence: int
88
+
89
+
79
90
  class SmartDetectItem(ProtectBaseObject):
80
91
  id: str
81
92
  timestamp: datetime
82
- level: PercentInt
83
93
  coord: tuple[int, int, int, int]
84
94
  object_type: SmartDetectObjectType
85
95
  zone_ids: list[int]
86
96
  duration: timedelta
97
+ confidence: int
98
+ first_shown_time_ms: int
99
+ idle_since_time_ms: int
100
+ stationary: bool
101
+ license_plate: str | None = None # only populated for vehicle object_type
102
+ depth: float | None = None
103
+ speed: float | None = None
104
+ attributes: dict[str, SmartDetectItemAttribute] | None = None
105
+ lines: list[int] | None = None
87
106
 
88
107
  @classmethod
89
108
  @cache
90
109
  def _get_unifi_remaps(cls) -> dict[str, str]:
91
110
  return {
92
111
  **super()._get_unifi_remaps(),
93
- "zones": "zoneIds",
112
+ "zones": "zone_ids",
94
113
  }
95
114
 
96
115
  @classmethod
@@ -125,9 +144,22 @@ class SmartDetectTrack(ProtectBaseObject):
125
144
  return self._api.bootstrap.events.get(self.event_id)
126
145
 
127
146
 
128
- class LicensePlateMetadata(ProtectBaseObject):
129
- name: str
130
- confidence_level: int
147
+ class EventThumbnailGroup(ProtectBaseObject):
148
+ """Group information for detected thumbnails (e.g., license plate recognition)."""
149
+
150
+ id: str
151
+ name: str | None = None
152
+ matched_name: str | None = None
153
+ confidence: int
154
+
155
+ def unifi_dict(
156
+ self,
157
+ data: dict[str, Any] | None = None,
158
+ exclude: set[str] | None = None,
159
+ ) -> dict[str, Any]:
160
+ data = super().unifi_dict(data=data, exclude=exclude)
161
+ pop_dict_set_if_none(data, {"matchedName", "name"})
162
+ return data
131
163
 
132
164
 
133
165
  class EventThumbnailAttribute(ProtectBaseObject):
@@ -135,9 +167,75 @@ class EventThumbnailAttribute(ProtectBaseObject):
135
167
  val: str
136
168
 
137
169
 
170
+ class NfcMetadata(ProtectBaseObject):
171
+ nfc_id: str
172
+ user_id: str
173
+
174
+ @classmethod
175
+ @cache
176
+ def _get_unifi_remaps(cls) -> dict[str, str]:
177
+ return {
178
+ **super()._get_unifi_remaps(),
179
+ "nfcId": "nfc_id",
180
+ "userId": "user_id",
181
+ }
182
+
183
+
184
+ class FingerprintMetadata(ProtectBaseObject):
185
+ ulp_id: str | None = None
186
+
187
+ @classmethod
188
+ @cache
189
+ def _get_unifi_remaps(cls) -> dict[str, str]:
190
+ return {
191
+ **super()._get_unifi_remaps(),
192
+ "ulpId": "ulp_id",
193
+ }
194
+
195
+
138
196
  class EventThumbnailAttributes(ProtectBaseObject):
139
- color: EventThumbnailAttribute | None = None
140
- vehicle_type: EventThumbnailAttribute | None = None
197
+ """
198
+ Dynamic attributes for detected thumbnails.
199
+
200
+ All attributes are stored as extra fields for full flexibility.
201
+
202
+ Common attribute types:
203
+ - color, vehicleType, faceMask: EventThumbnailAttribute (with val and confidence)
204
+ - zone: list[int] - Zone IDs where detection occurred
205
+ - trackerId: int - Unique tracker ID for this detection
206
+
207
+ Allows any attributes for forward compatibility with new UFP features.
208
+
209
+ Example usage:
210
+ >>> attrs = thumbnail.attributes
211
+ >>> if attrs:
212
+ ... # Access EventThumbnailAttribute objects
213
+ ... color = attrs.color # EventThumbnailAttribute
214
+ ... if color:
215
+ ... print(f"Color: {color.val} (confidence: {color.confidence})")
216
+ ... # Access primitive types
217
+ ... zones = attrs.zone # list[int] | None
218
+ ... tracker = attrs.trackerId # int | None
219
+ """
220
+
221
+ model_config = ConfigDict(extra="allow")
222
+
223
+ def get_value(self, key: str) -> str | None:
224
+ """Get the string value from an EventThumbnailAttribute field, if it exists."""
225
+ attr = getattr(self, key, None)
226
+ if isinstance(attr, EventThumbnailAttribute):
227
+ return attr.val
228
+ return None
229
+
230
+ @classmethod
231
+ def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
232
+ # Convert nested attribute objects to EventThumbnailAttribute instances
233
+ return {
234
+ key: EventThumbnailAttribute.from_unifi_dict(**value)
235
+ if isinstance(value, dict) and "val" in value and "confidence" in value
236
+ else value
237
+ for key, value in data.items()
238
+ }
141
239
 
142
240
  def unifi_dict(
143
241
  self,
@@ -146,11 +244,8 @@ class EventThumbnailAttributes(ProtectBaseObject):
146
244
  ) -> dict[str, Any]:
147
245
  data = super().unifi_dict(data=data, exclude=exclude)
148
246
 
149
- for key in DELETE_KEYS_THUMB.intersection(data):
150
- if data[key] is None:
151
- del data[key]
152
-
153
- return data
247
+ # Remove None values from extra fields
248
+ return {k: v for k, v in data.items() if v is not None}
154
249
 
155
250
 
156
251
  class EventDetectedThumbnail(ProtectBaseObject):
@@ -158,7 +253,12 @@ class EventDetectedThumbnail(ProtectBaseObject):
158
253
  type: str
159
254
  cropped_id: str
160
255
  attributes: EventThumbnailAttributes | None = None
161
- name: str | None
256
+ name: str | None = None
257
+ coord: list[int] | None = None
258
+ confidence: int | None = None
259
+ # requires 6.0.0+
260
+ group: EventThumbnailGroup | None = None
261
+ object_id: str | None = None
162
262
 
163
263
  @classmethod
164
264
  @cache
@@ -171,36 +271,34 @@ class EventDetectedThumbnail(ProtectBaseObject):
171
271
  exclude: set[str] | None = None,
172
272
  ) -> dict[str, Any]:
173
273
  data = super().unifi_dict(data=data, exclude=exclude)
174
-
175
- if "name" in data and data["name"] is None:
176
- del data["name"]
177
-
274
+ pop_dict_set_if_none(data, {"name", "group", "objectId", "coord", "confidence"})
178
275
  return data
179
276
 
180
277
 
181
278
  class EventMetadata(ProtectBaseObject):
182
- client_platform: str | None
183
- reason: str | None
184
- app_update: str | None
185
- light_id: str | None
186
- light_name: str | None
187
- type: str | None
188
- sensor_id: str | None
189
- sensor_name: str | None
190
- sensor_type: SensorType | None
191
- doorlock_id: str | None
192
- doorlock_name: str | None
193
- from_value: str | None
194
- to_value: str | None
195
- mount_type: MountType | None
196
- status: SensorStatusType | None
197
- alarm_type: str | None
198
- device_id: str | None
199
- mac: str | None
200
- # require 2.7.5+
201
- license_plate: LicensePlateMetadata | None = None
279
+ client_platform: str | None = None
280
+ reason: str | None = None
281
+ app_update: str | None = None
282
+ light_id: str | None = None
283
+ light_name: str | None = None
284
+ type: str | None = None
285
+ sensor_id: str | None = None
286
+ sensor_name: str | None = None
287
+ sensor_type: SensorType | None = None
288
+ doorlock_id: str | None = None
289
+ doorlock_name: str | None = None
290
+ from_value: str | None = None
291
+ to_value: str | None = None
292
+ mount_type: MountType | None = None
293
+ status: SensorStatusType | None = None
294
+ alarm_type: str | None = None
295
+ device_id: str | None = None
296
+ mac: str | None = None
202
297
  # requires 2.11.13+
203
298
  detected_thumbnails: list[EventDetectedThumbnail] | None = None
299
+ # requires 5.1.34+
300
+ nfc: NfcMetadata | None = None
301
+ fingerprint: FingerprintMetadata | None = None
204
302
 
205
303
  _collapse_keys: ClassVar[SetStr] = {
206
304
  "lightId",
@@ -258,22 +356,25 @@ class EventMetadata(ProtectBaseObject):
258
356
  class Event(ProtectModelWithId):
259
357
  type: EventType
260
358
  start: datetime
261
- end: datetime | None
359
+ end: datetime | None = None
262
360
  score: int
263
- heatmap_id: str | None
264
- camera_id: str | None
361
+ heatmap_id: str | None = None
362
+ camera_id: str | None = None
265
363
  smart_detect_types: list[SmartDetectObjectType]
266
364
  smart_detect_event_ids: list[str]
267
- thumbnail_id: str | None
268
- user_id: str | None
269
- timestamp: datetime | None
270
- metadata: EventMetadata | None
365
+ thumbnail_id: str | None = None
366
+ user_id: str | None = None
367
+ timestamp: datetime | None = None
368
+ metadata: EventMetadata | None = None
271
369
  # requires 2.7.5+
272
370
  deleted_at: datetime | None = None
273
371
  deletion_type: Literal["manual", "automatic"] | None = None
274
372
  # only appears if `get_events` is called with category
275
373
  category: EventCategories | None = None
276
374
  sub_category: str | None = None
375
+ # requires 6.0.0+
376
+ is_favorite: bool | None = None
377
+ favorite_object_ids: list[str] | None = None
277
378
 
278
379
  # TODO:
279
380
  # partition
@@ -298,10 +399,12 @@ class Event(ProtectModelWithId):
298
399
  @classmethod
299
400
  @cache
300
401
  def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
301
- return {
302
- key: convert_to_datetime
303
- for key in ("start", "end", "timestamp", "deletedAt")
304
- } | super().unifi_dict_conversions()
402
+ return (
403
+ dict.fromkeys(
404
+ ("start", "end", "timestamp", "deletedAt"), convert_to_datetime
405
+ )
406
+ | super().unifi_dict_conversions()
407
+ )
305
408
 
306
409
  def unifi_dict(
307
410
  self,
@@ -309,11 +412,7 @@ class Event(ProtectModelWithId):
309
412
  exclude: set[str] | None = None,
310
413
  ) -> dict[str, Any]:
311
414
  data = super().unifi_dict(data=data, exclude=exclude)
312
-
313
- for key in DELETE_KEYS_EVENT.intersection(data):
314
- if data[key] is None:
315
- del data[key]
316
-
415
+ pop_dict_set_if_none(data, DELETE_KEYS_EVENT)
317
416
  return data
318
417
 
319
418
  @property
@@ -356,6 +455,45 @@ class Event(ProtectModelWithId):
356
455
  ]
357
456
  return self._smart_detect_events
358
457
 
458
+ def get_detected_thumbnail(self) -> EventDetectedThumbnail | None:
459
+ """
460
+ Gets best detected thumbnail for event (UFP 6.x+).
461
+
462
+ Returns the thumbnail marked with clockBestWall, which indicates
463
+ the optimal frame for this detection (highest confidence, best angle, etc.).
464
+
465
+ Returns:
466
+ EventDetectedThumbnail with the best detection frame, or None if:
467
+ - Event has no metadata
468
+ - No detected thumbnails available
469
+ - No thumbnail has clockBestWall set
470
+
471
+ Example usage:
472
+ >>> # License Plate Recognition
473
+ >>> thumbnail = event.get_detected_thumbnail()
474
+ >>> if thumbnail and thumbnail.group:
475
+ ... plate = thumbnail.group.matched_name # "ABC123"
476
+ ... confidence = thumbnail.group.confidence # 95
477
+ ... if thumbnail.attributes:
478
+ ... color = thumbnail.attributes.get_value("color") # "white"
479
+ ... vehicle = thumbnail.attributes.get_value("vehicleType") # "sedan"
480
+
481
+ >>> # Face Detection
482
+ >>> thumbnail = event.get_detected_thumbnail()
483
+ >>> if thumbnail and thumbnail.group:
484
+ ... face_name = thumbnail.group.matched_name # "John Doe"
485
+ ... confidence = thumbnail.group.confidence # 87
486
+
487
+ """
488
+ if not self.metadata or not self.metadata.detected_thumbnails:
489
+ return None
490
+
491
+ for thumbnail in self.metadata.detected_thumbnails:
492
+ if thumbnail.clock_best_wall:
493
+ return thumbnail
494
+
495
+ return None
496
+
359
497
  async def get_thumbnail(
360
498
  self,
361
499
  width: int | None = None,
@@ -532,9 +670,9 @@ class CPUInfo(ProtectBaseObject):
532
670
 
533
671
 
534
672
  class MemoryInfo(ProtectBaseObject):
535
- available: int | None
536
- free: int | None
537
- total: int | None
673
+ available: int | None = None
674
+ free: int | None = None
675
+ total: int | None = None
538
676
 
539
677
 
540
678
  class StorageDevice(ProtectBaseObject):
@@ -632,30 +770,29 @@ class UOSDisk(ProtectBaseObject):
632
770
  data["estimate"] /= 1000
633
771
 
634
772
  if "state" in data and data["state"] == "nodisk":
635
- delete_keys = [
636
- "action",
637
- "ata",
638
- "bad_sector",
639
- "estimate",
640
- "firmware",
641
- "healthy",
642
- "life_span",
643
- "model",
644
- "poweronhrs",
645
- "progress",
646
- "reason",
647
- "rpm",
648
- "sata",
649
- "serial",
650
- "tempature",
651
- "temperature",
652
- "threshold",
653
- "type",
654
- ]
655
- for key in delete_keys:
656
- if key in data:
657
- del data[key]
658
-
773
+ pop_dict_tuple(
774
+ data,
775
+ (
776
+ "action",
777
+ "ata",
778
+ "bad_sector",
779
+ "estimate",
780
+ "firmware",
781
+ "healthy",
782
+ "life_span",
783
+ "model",
784
+ "poweronhrs",
785
+ "progress",
786
+ "reason",
787
+ "rpm",
788
+ "sata",
789
+ "serial",
790
+ "tempature",
791
+ "temperature",
792
+ "threshold",
793
+ "type",
794
+ ),
795
+ )
659
796
  return data
660
797
 
661
798
  @property
@@ -739,10 +876,7 @@ class SystemInfo(ProtectBaseObject):
739
876
  exclude: set[str] | None = None,
740
877
  ) -> dict[str, Any]:
741
878
  data = super().unifi_dict(data=data, exclude=exclude)
742
-
743
- if data is not None and "ustorage" in data and data["ustorage"] is None:
744
- del data["ustorage"]
745
-
879
+ pop_dict_set_if_none(data, {"ustorage"})
746
880
  return data
747
881
 
748
882
 
@@ -853,8 +987,8 @@ class StorageDistribution(ProtectBaseObject):
853
987
 
854
988
  class StorageStats(ProtectBaseObject):
855
989
  utilization: float
856
- capacity: timedelta | None
857
- remaining_capacity: timedelta | None
990
+ capacity: timedelta | None = None
991
+ remaining_capacity: timedelta | None = None
858
992
  recording_space: StorageSpace
859
993
  storage_distribution: StorageDistribution
860
994
 
@@ -901,7 +1035,7 @@ class NVR(ProtectDeviceModel):
901
1035
  ucore_version: str
902
1036
  hardware_platform: str
903
1037
  ports: PortConfig
904
- last_update_at: datetime | None
1038
+ last_update_at: datetime | None = None
905
1039
  is_station: bool
906
1040
  enable_automatic_backups: bool
907
1041
  enable_stats_reporting: bool
@@ -912,14 +1046,14 @@ class NVR(ProtectDeviceModel):
912
1046
  host_type: int
913
1047
  host_shortname: str
914
1048
  is_hardware: bool
915
- is_wireless_uplink_enabled: bool | None
1049
+ is_wireless_uplink_enabled: bool | None = None
916
1050
  time_format: Literal["12h", "24h"]
917
1051
  temperature_unit: Literal["C", "F"]
918
- recording_retention_duration: timedelta | None
1052
+ recording_retention_duration: timedelta | None = None
919
1053
  enable_crash_reporting: bool
920
1054
  disable_audio: bool
921
1055
  analytics_data: AnalyticsOption
922
- anonymous_device_id: UUID | None
1056
+ anonymous_device_id: UUID | None = None
923
1057
  camera_utilization: int
924
1058
  is_recycling: bool
925
1059
  disable_auto_link: bool
@@ -1196,29 +1330,8 @@ class NVR(ProtectDeviceModel):
1196
1330
  return versions
1197
1331
 
1198
1332
  async def get_is_prerelease(self) -> bool:
1199
- """Get if current version of Protect is a prerelease version."""
1200
- # only EA versions have `-beta` in versions
1201
- if self.version.is_prerelease:
1202
- return True
1203
-
1204
- # 2.6.14 is an EA version that looks like a release version
1205
- cache_file_path = self._api.cache_dir / "release_cache.json"
1206
- versions = await self._read_cache_file(
1207
- cache_file_path,
1208
- ) or await self._read_cache_file(RELEASE_CACHE)
1209
- if versions is None or self.version not in versions:
1210
- versions = await self._api.get_release_versions()
1211
- try:
1212
- _LOGGER.debug("Fetching releases from APT repos...")
1213
- tmp = self._api.cache_dir / "release_cache.tmp.json"
1214
- await aos.makedirs(self._api.cache_dir, exist_ok=True)
1215
- async with aiofiles.open(tmp, "wb") as cache_file:
1216
- await cache_file.write(orjson.dumps([str(v) for v in versions]))
1217
- await aos.rename(tmp, cache_file_path)
1218
- except Exception:
1219
- _LOGGER.warning("Failed write cache file.")
1220
-
1221
- return self.version not in versions
1333
+ """[DEPRECATED] Always returns False. Will be removed after HA 2025.8.0."""
1334
+ return False
1222
1335
 
1223
1336
  async def set_smart_detections(self, value: bool) -> None:
1224
1337
  """Set if smart detections are enabled."""
@@ -1344,6 +1457,11 @@ class NVR(ProtectDeviceModel):
1344
1457
  """
1345
1458
  return self._is_smart_enabled(SmartDetectObjectType.VEHICLE)
1346
1459
 
1460
+ @property
1461
+ def is_global_face_detection_on(self) -> bool:
1462
+ """Is Face Detection available and enabled?"""
1463
+ return self._is_smart_enabled(SmartDetectObjectType.FACE)
1464
+
1347
1465
  @property
1348
1466
  def is_global_license_plate_detection_on(self) -> bool:
1349
1467
  """