uiprotect 1.11.0__tar.gz → 1.12.0__tar.gz
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.
Potentially problematic release.
This version of uiprotect might be problematic. Click here for more details.
- {uiprotect-1.11.0 → uiprotect-1.12.0}/PKG-INFO +1 -1
- {uiprotect-1.11.0 → uiprotect-1.12.0}/pyproject.toml +1 -1
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/data/base.py +1 -2
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/data/bootstrap.py +46 -29
- {uiprotect-1.11.0 → uiprotect-1.12.0}/LICENSE +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/README.md +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/__init__.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/__main__.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/api.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/cli/__init__.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/cli/backup.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/cli/base.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/cli/cameras.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/cli/chimes.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/cli/doorlocks.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/cli/events.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/cli/lights.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/cli/liveviews.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/cli/nvr.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/cli/sensors.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/cli/viewers.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/data/__init__.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/data/convert.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/data/devices.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/data/nvr.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/data/types.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/data/user.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/data/websocket.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/exceptions.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/py.typed +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/release_cache.json +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/stream.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/test_util/__init__.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/test_util/anonymize.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/utils.py +0 -0
- {uiprotect-1.11.0 → uiprotect-1.12.0}/src/uiprotect/websocket.py +0 -0
|
@@ -6,7 +6,7 @@ import asyncio
|
|
|
6
6
|
import logging
|
|
7
7
|
from collections.abc import Callable
|
|
8
8
|
from datetime import datetime, timedelta
|
|
9
|
-
from functools import cache
|
|
9
|
+
from functools import cache
|
|
10
10
|
from ipaddress import IPv4Address
|
|
11
11
|
from typing import TYPE_CHECKING, Any, ClassVar, TypeVar
|
|
12
12
|
from uuid import UUID
|
|
@@ -89,7 +89,6 @@ class ProtectBaseObject(BaseModel):
|
|
|
89
89
|
arbitrary_types_allowed = True
|
|
90
90
|
validate_assignment = True
|
|
91
91
|
copy_on_model_validation = "shallow"
|
|
92
|
-
keep_untouched = (cached_property,)
|
|
93
92
|
|
|
94
93
|
def __init__(self, api: ProtectApiClient | None = None, **data: Any) -> None:
|
|
95
94
|
"""
|
|
@@ -8,7 +8,7 @@ from collections.abc import Iterable
|
|
|
8
8
|
from copy import deepcopy
|
|
9
9
|
from dataclasses import dataclass
|
|
10
10
|
from datetime import datetime
|
|
11
|
-
from functools import cache
|
|
11
|
+
from functools import cache
|
|
12
12
|
from typing import TYPE_CHECKING, Any, cast
|
|
13
13
|
|
|
14
14
|
from aiohttp.client_exceptions import ServerDisconnectedError
|
|
@@ -56,6 +56,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
56
56
|
MAX_SUPPORTED_CAMERAS = 256
|
|
57
57
|
MAX_EVENT_HISTORY_IN_STATE_MACHINE = MAX_SUPPORTED_CAMERAS * 2
|
|
58
58
|
STATS_KEYS = {
|
|
59
|
+
"eventStats",
|
|
59
60
|
"storageStats",
|
|
60
61
|
"stats",
|
|
61
62
|
"systemInfo",
|
|
@@ -181,6 +182,10 @@ class Bootstrap(ProtectBaseObject):
|
|
|
181
182
|
mac_lookup: dict[str, ProtectDeviceRef] = {}
|
|
182
183
|
id_lookup: dict[str, ProtectDeviceRef] = {}
|
|
183
184
|
_ws_stats: list[WSStat] = PrivateAttr([])
|
|
185
|
+
_has_doorbell: bool | None = PrivateAttr(None)
|
|
186
|
+
_has_smart: bool | None = PrivateAttr(None)
|
|
187
|
+
_has_media: bool | None = PrivateAttr(None)
|
|
188
|
+
_recording_start: datetime | None = PrivateAttr(None)
|
|
184
189
|
_refresh_tasks: set[asyncio.Task[None]] = PrivateAttr(set())
|
|
185
190
|
|
|
186
191
|
@classmethod
|
|
@@ -248,33 +253,48 @@ class Bootstrap(ProtectBaseObject):
|
|
|
248
253
|
def auth_user(self) -> User:
|
|
249
254
|
return self._api.bootstrap.users[self.auth_user_id]
|
|
250
255
|
|
|
251
|
-
@
|
|
256
|
+
@property
|
|
252
257
|
def has_doorbell(self) -> bool:
|
|
253
|
-
|
|
258
|
+
if self._has_doorbell is None:
|
|
259
|
+
self._has_doorbell = any(
|
|
260
|
+
c.feature_flags.is_doorbell for c in self.cameras.values()
|
|
261
|
+
)
|
|
254
262
|
|
|
255
|
-
|
|
263
|
+
return self._has_doorbell
|
|
264
|
+
|
|
265
|
+
@property
|
|
256
266
|
def recording_start(self) -> datetime | None:
|
|
257
|
-
"""Get
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
267
|
+
"""Get earilest recording date."""
|
|
268
|
+
if self._recording_start is None:
|
|
269
|
+
try:
|
|
270
|
+
self._recording_start = min(
|
|
271
|
+
c.stats.video.recording_start
|
|
272
|
+
for c in self.cameras.values()
|
|
273
|
+
if c.stats.video.recording_start is not None
|
|
274
|
+
)
|
|
275
|
+
except ValueError:
|
|
276
|
+
return None
|
|
277
|
+
return self._recording_start
|
|
266
278
|
|
|
267
|
-
@
|
|
279
|
+
@property
|
|
268
280
|
def has_smart_detections(self) -> bool:
|
|
269
281
|
"""Check if any camera has smart detections."""
|
|
270
|
-
|
|
282
|
+
if self._has_smart is None:
|
|
283
|
+
self._has_smart = any(
|
|
284
|
+
c.feature_flags.has_smart_detect for c in self.cameras.values()
|
|
285
|
+
)
|
|
286
|
+
return self._has_smart
|
|
271
287
|
|
|
272
|
-
@
|
|
288
|
+
@property
|
|
273
289
|
def has_media(self) -> bool:
|
|
274
290
|
"""Checks if user can read media for any camera."""
|
|
275
|
-
if self.
|
|
276
|
-
|
|
277
|
-
|
|
291
|
+
if self._has_media is None:
|
|
292
|
+
if self.recording_start is None:
|
|
293
|
+
return False
|
|
294
|
+
self._has_media = any(
|
|
295
|
+
c.can_read_media(self.auth_user) for c in self.cameras.values()
|
|
296
|
+
)
|
|
297
|
+
return self._has_media
|
|
278
298
|
|
|
279
299
|
def get_device_from_mac(self, mac: str) -> ProtectAdoptableDeviceModel | None:
|
|
280
300
|
"""Retrieve a device from MAC address."""
|
|
@@ -375,8 +395,7 @@ class Bootstrap(ProtectBaseObject):
|
|
|
375
395
|
|
|
376
396
|
device_id: str = packet.action_frame.data["id"]
|
|
377
397
|
self.id_lookup.pop(device_id, None)
|
|
378
|
-
device
|
|
379
|
-
if device is None:
|
|
398
|
+
if (device := devices.pop(device_id, None)) is None:
|
|
380
399
|
return None
|
|
381
400
|
self.mac_lookup.pop(normalize_mac(device.mac), None)
|
|
382
401
|
|
|
@@ -467,19 +486,17 @@ class Bootstrap(ProtectBaseObject):
|
|
|
467
486
|
elif model_type is ModelType.CAMERA:
|
|
468
487
|
if TYPE_CHECKING:
|
|
469
488
|
assert isinstance(obj, Camera)
|
|
470
|
-
if "last_ring" in data and obj.last_ring:
|
|
471
|
-
is_recent
|
|
472
|
-
_LOGGER.debug("last_ring for %s (%s)", obj.id, is_recent)
|
|
473
|
-
if is_recent:
|
|
489
|
+
if "last_ring" in data and (last_ring := obj.last_ring):
|
|
490
|
+
if is_recent := last_ring + RECENT_EVENT_MAX >= utc_now():
|
|
474
491
|
obj.set_ring_timeout()
|
|
492
|
+
_LOGGER.debug("last_ring for %s (%s)", obj.id, is_recent)
|
|
475
493
|
elif model_type is ModelType.SENSOR:
|
|
476
494
|
if TYPE_CHECKING:
|
|
477
495
|
assert isinstance(obj, Sensor)
|
|
478
|
-
if "alarm_triggered_at" in data and obj.alarm_triggered_at:
|
|
479
|
-
is_recent
|
|
480
|
-
_LOGGER.debug("alarm_triggered_at for %s (%s)", obj.id, is_recent)
|
|
481
|
-
if is_recent:
|
|
496
|
+
if "alarm_triggered_at" in data and (trigged_at := obj.alarm_triggered_at):
|
|
497
|
+
if is_recent := trigged_at + RECENT_EVENT_MAX >= utc_now():
|
|
482
498
|
obj.set_alarm_timeout()
|
|
499
|
+
_LOGGER.debug("alarm_triggered_at for %s (%s)", obj.id, is_recent)
|
|
483
500
|
|
|
484
501
|
devices[action_id] = obj
|
|
485
502
|
self._create_stat(packet, data, False)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|