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/nvr.py
CHANGED
|
@@ -15,11 +15,12 @@ from uuid import UUID
|
|
|
15
15
|
|
|
16
16
|
import aiofiles
|
|
17
17
|
import orjson
|
|
18
|
-
from
|
|
19
|
-
from pydantic
|
|
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
|
|
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.
|
|
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": "
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
"""
|
|
1200
|
-
|
|
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
|
"""
|