uiprotect 3.1.5__py3-none-any.whl → 3.1.6__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.
Potentially problematic release.
This version of uiprotect might be problematic. Click here for more details.
- uiprotect/data/base.py +37 -34
- uiprotect/data/devices.py +9 -12
- uiprotect/data/nvr.py +1 -1
- {uiprotect-3.1.5.dist-info → uiprotect-3.1.6.dist-info}/METADATA +1 -1
- {uiprotect-3.1.5.dist-info → uiprotect-3.1.6.dist-info}/RECORD +8 -8
- {uiprotect-3.1.5.dist-info → uiprotect-3.1.6.dist-info}/LICENSE +0 -0
- {uiprotect-3.1.5.dist-info → uiprotect-3.1.6.dist-info}/WHEEL +0 -0
- {uiprotect-3.1.5.dist-info → uiprotect-3.1.6.dist-info}/entry_points.txt +0 -0
uiprotect/data/base.py
CHANGED
|
@@ -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, cached_property
|
|
10
10
|
from ipaddress import IPv4Address
|
|
11
11
|
from typing import TYPE_CHECKING, Any, ClassVar, TypeVar
|
|
12
12
|
from uuid import UUID
|
|
@@ -446,9 +446,6 @@ class ProtectBaseObject(BaseModel):
|
|
|
446
446
|
for to_key in set(new_data).intersection(remaps):
|
|
447
447
|
new_data[remaps[to_key]] = new_data.pop(to_key)
|
|
448
448
|
|
|
449
|
-
if "api" in new_data:
|
|
450
|
-
del new_data["api"]
|
|
451
|
-
|
|
452
449
|
return new_data
|
|
453
450
|
|
|
454
451
|
def update_from_dict(cls: ProtectObject, data: dict[str, Any]) -> ProtectObject:
|
|
@@ -466,8 +463,6 @@ class ProtectBaseObject(BaseModel):
|
|
|
466
463
|
api = cls._api
|
|
467
464
|
_fields = cls.__fields__
|
|
468
465
|
unifi_obj: ProtectBaseObject | None
|
|
469
|
-
if "api" in data:
|
|
470
|
-
del data["api"]
|
|
471
466
|
value: Any
|
|
472
467
|
|
|
473
468
|
for key, item in data.items():
|
|
@@ -536,32 +531,40 @@ class ProtectModel(ProtectBaseObject):
|
|
|
536
531
|
return data
|
|
537
532
|
|
|
538
533
|
|
|
534
|
+
class UpdateSynchronization:
|
|
535
|
+
"""Helper class for managing updates to Protect devices."""
|
|
536
|
+
|
|
537
|
+
@cached_property
|
|
538
|
+
def lock(self) -> asyncio.Lock:
|
|
539
|
+
"""Lock to prevent multiple updates at once."""
|
|
540
|
+
return asyncio.Lock()
|
|
541
|
+
|
|
542
|
+
@cached_property
|
|
543
|
+
def queue(self) -> asyncio.Queue[Callable[[], None]]:
|
|
544
|
+
"""Queue to store device updates."""
|
|
545
|
+
return asyncio.Queue()
|
|
546
|
+
|
|
547
|
+
@cached_property
|
|
548
|
+
def event(self) -> asyncio.Event:
|
|
549
|
+
"""Event to signal when a device update has been queued."""
|
|
550
|
+
return asyncio.Event()
|
|
551
|
+
|
|
552
|
+
|
|
539
553
|
class ProtectModelWithId(ProtectModel):
|
|
540
554
|
id: str
|
|
541
555
|
|
|
542
|
-
|
|
543
|
-
_update_queue: asyncio.Queue[Callable[[], None]] = PrivateAttr(None)
|
|
544
|
-
_update_event: asyncio.Event = PrivateAttr(None)
|
|
556
|
+
_update_sync: UpdateSynchronization = PrivateAttr(None)
|
|
545
557
|
|
|
546
558
|
def __init__(self, **data: Any) -> None:
|
|
547
|
-
|
|
548
|
-
update_queue = data.pop("update_queue", None)
|
|
549
|
-
update_event = data.pop("update_event", None)
|
|
559
|
+
update_sync = data.pop("update_sync", None)
|
|
550
560
|
super().__init__(**data)
|
|
551
|
-
self.
|
|
552
|
-
self._update_queue = update_queue or asyncio.Queue()
|
|
553
|
-
self._update_event = update_event or asyncio.Event()
|
|
561
|
+
self._update_sync = update_sync or UpdateSynchronization()
|
|
554
562
|
|
|
555
563
|
@classmethod
|
|
556
564
|
def construct(cls, _fields_set: set[str] | None = None, **values: Any) -> Self:
|
|
557
|
-
|
|
558
|
-
update_queue = values.pop("update_queue", None)
|
|
559
|
-
update_event = values.pop("update_event", None)
|
|
565
|
+
update_sync = values.pop("update_sync", None)
|
|
560
566
|
obj = super().construct(_fields_set=_fields_set, **values)
|
|
561
|
-
obj.
|
|
562
|
-
obj._update_queue = update_queue or asyncio.Queue()
|
|
563
|
-
obj._update_event = update_event or asyncio.Event()
|
|
564
|
-
|
|
567
|
+
obj._update_sync = update_sync or UpdateSynchronization()
|
|
565
568
|
return obj
|
|
566
569
|
|
|
567
570
|
@classmethod
|
|
@@ -609,28 +612,28 @@ class ProtectModelWithId(ProtectModel):
|
|
|
609
612
|
This allows aggregating devices updates so if multiple ones come in all at once,
|
|
610
613
|
they can be combined in a single PATCH.
|
|
611
614
|
"""
|
|
612
|
-
self.
|
|
615
|
+
self._update_sync.queue.put_nowait(callback)
|
|
613
616
|
|
|
614
|
-
self.
|
|
617
|
+
self._update_sync.event.set()
|
|
615
618
|
await asyncio.sleep(
|
|
616
619
|
0.001,
|
|
617
620
|
) # release execution so other `queue_update` calls can abort
|
|
618
|
-
self.
|
|
621
|
+
self._update_sync.event.clear()
|
|
619
622
|
|
|
620
623
|
try:
|
|
621
624
|
async with asyncio_timeout(0.05):
|
|
622
|
-
await self.
|
|
623
|
-
self.
|
|
625
|
+
await self._update_sync.event.wait()
|
|
626
|
+
self._update_sync.event.clear()
|
|
624
627
|
return
|
|
625
628
|
except (TimeoutError, asyncio.TimeoutError, asyncio.CancelledError):
|
|
626
|
-
async with self.
|
|
629
|
+
async with self._update_sync.lock:
|
|
627
630
|
# Important! Now that we have the lock, we yield to the event loop so any
|
|
628
631
|
# updates from the websocket are processed before we generate the diff
|
|
629
632
|
await asyncio.sleep(0)
|
|
630
633
|
# Save the initial data before we generate the diff
|
|
631
634
|
data_before_changes = self.dict_with_excludes()
|
|
632
|
-
while not self.
|
|
633
|
-
callback = self.
|
|
635
|
+
while not self._update_sync.queue.empty():
|
|
636
|
+
callback = self._update_sync.queue.get_nowait()
|
|
634
637
|
callback()
|
|
635
638
|
# Important, do not yield to the event loop before generating the diff
|
|
636
639
|
# otherwise we may miss updates from the websocket
|
|
@@ -660,8 +663,8 @@ class ProtectModelWithId(ProtectModel):
|
|
|
660
663
|
"""
|
|
661
664
|
# do not allow multiple save_device calls at once
|
|
662
665
|
release_lock = False
|
|
663
|
-
if not self.
|
|
664
|
-
await self.
|
|
666
|
+
if not self._update_sync.lock.locked():
|
|
667
|
+
await self._update_sync.lock.acquire()
|
|
665
668
|
release_lock = True
|
|
666
669
|
|
|
667
670
|
try:
|
|
@@ -673,7 +676,7 @@ class ProtectModelWithId(ProtectModel):
|
|
|
673
676
|
)
|
|
674
677
|
finally:
|
|
675
678
|
if release_lock:
|
|
676
|
-
self.
|
|
679
|
+
self._update_sync.lock.release()
|
|
677
680
|
|
|
678
681
|
async def _save_device_changes(
|
|
679
682
|
self,
|
|
@@ -692,7 +695,7 @@ class ProtectModelWithId(ProtectModel):
|
|
|
692
695
|
)
|
|
693
696
|
|
|
694
697
|
assert (
|
|
695
|
-
self.
|
|
698
|
+
self._update_sync.lock.locked()
|
|
696
699
|
), "save_device_changes should only be called when the update lock is held"
|
|
697
700
|
read_only_fields = self.__class__._get_read_only_fields()
|
|
698
701
|
|
uiprotect/data/devices.py
CHANGED
|
@@ -159,10 +159,9 @@ class Light(ProtectMotionDeviceModel):
|
|
|
159
159
|
|
|
160
160
|
async def set_paired_camera(self, camera: Camera | None) -> None:
|
|
161
161
|
"""Sets the camera paired with the light"""
|
|
162
|
-
async with self.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
) # yield to the event loop once we have the lock to process any pending updates
|
|
162
|
+
async with self._update_sync.lock:
|
|
163
|
+
# yield to the event loop once we have the lock to process any pending updates
|
|
164
|
+
await asyncio.sleep(0)
|
|
166
165
|
data_before_changes = self.dict_with_excludes()
|
|
167
166
|
if camera is None:
|
|
168
167
|
self.camera_id = None
|
|
@@ -2378,10 +2377,9 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
2378
2377
|
raise BadRequest("Camera does not have an LCD screen")
|
|
2379
2378
|
|
|
2380
2379
|
if text_type is None:
|
|
2381
|
-
async with self.
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
) # yield to the event loop once we have the lock to process any pending updates
|
|
2380
|
+
async with self._update_sync.lock:
|
|
2381
|
+
# yield to the event loop once we have the lock to process any pending updates
|
|
2382
|
+
await asyncio.sleep(0)
|
|
2385
2383
|
data_before_changes = self.dict_with_excludes()
|
|
2386
2384
|
self.lcd_message = None
|
|
2387
2385
|
# UniFi Protect bug: clearing LCD text message does _not_ emit a WS message
|
|
@@ -2704,10 +2702,9 @@ class Viewer(ProtectAdoptableDeviceModel):
|
|
|
2704
2702
|
if self._api is not None and liveview.id not in self._api.bootstrap.liveviews:
|
|
2705
2703
|
raise BadRequest("Unknown liveview")
|
|
2706
2704
|
|
|
2707
|
-
async with self.
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
) # yield to the event loop once we have the lock to process any pending updates
|
|
2705
|
+
async with self._update_sync.lock:
|
|
2706
|
+
# yield to the event loop once we have the lock to process any pending updates
|
|
2707
|
+
await asyncio.sleep(0)
|
|
2711
2708
|
data_before_changes = self.dict_with_excludes()
|
|
2712
2709
|
self.liveview_id = liveview.id
|
|
2713
2710
|
# UniFi Protect bug: changing the liveview does _not_ emit a WS message
|
uiprotect/data/nvr.py
CHANGED
|
@@ -1184,7 +1184,7 @@ class NVR(ProtectDeviceModel):
|
|
|
1184
1184
|
self, update_callback: Callable[[], None]
|
|
1185
1185
|
) -> None:
|
|
1186
1186
|
"""Updates doorbell messages and saves to Protect."""
|
|
1187
|
-
async with self.
|
|
1187
|
+
async with self._update_sync.lock:
|
|
1188
1188
|
# yield to the event loop once we have the lock to ensure websocket updates are processed
|
|
1189
1189
|
await asyncio.sleep(0)
|
|
1190
1190
|
data_before_changes = self.dict_with_excludes()
|
|
@@ -14,11 +14,11 @@ uiprotect/cli/nvr.py,sha256=TwxEg2XT8jXAbOqv6gc7KFXELKadeItEDYweSL4_-e8,4260
|
|
|
14
14
|
uiprotect/cli/sensors.py,sha256=fQtcDJCVxs4VbAqcavgBy2ABiVxAW3GXtna6_XFBp2k,8153
|
|
15
15
|
uiprotect/cli/viewers.py,sha256=2cyrp104ffIvgT0wYGIO0G35QMkEbFe7fSVqLwDXQYQ,2171
|
|
16
16
|
uiprotect/data/__init__.py,sha256=OcfuJl2qXfHcj_mdnrHhzZ5tEIZrw8auziX5IE7dn-I,2938
|
|
17
|
-
uiprotect/data/base.py,sha256=
|
|
17
|
+
uiprotect/data/base.py,sha256=0dfm5qLHpDJYTO04tYpEB869DkVoT0uev9BmgPR9HrA,35436
|
|
18
18
|
uiprotect/data/bootstrap.py,sha256=aF6BJ8qgE7sEDsTKPdRXsrMsL_CTmo8Xr-TUNZjfU6I,21810
|
|
19
19
|
uiprotect/data/convert.py,sha256=8h6Il_DhMkPRDPj9F_rA2UZIlTuchS3BQD24peKpk2A,2185
|
|
20
|
-
uiprotect/data/devices.py,sha256=
|
|
21
|
-
uiprotect/data/nvr.py,sha256=
|
|
20
|
+
uiprotect/data/devices.py,sha256=n2TVY0J-xhO7fXgrvC0hqsHjNc-B_DdySDKHOIKPeqs,110363
|
|
21
|
+
uiprotect/data/nvr.py,sha256=4EzeRFogmdT96VJ9yY17rqpUMx7NuKBRnUrgRbQ5YJs,47461
|
|
22
22
|
uiprotect/data/types.py,sha256=3CocULpkdTgF4is1nIEDYIlwf2EOkNNM7L4kJ7NkAwM,17654
|
|
23
23
|
uiprotect/data/user.py,sha256=YvgXJKV4_y-bm0eySWz9f_ie9aR5lpVn17t9H0Pix8I,6998
|
|
24
24
|
uiprotect/data/websocket.py,sha256=vnn3FUc1H3e4dvA3INGob_asbmVCaA99H1DnwDEmpdw,6709
|
|
@@ -30,8 +30,8 @@ uiprotect/test_util/__init__.py,sha256=whiOUb5LfDLNT3AQG6ISiKtAqO2JnhCIdFavhWDK4
|
|
|
30
30
|
uiprotect/test_util/anonymize.py,sha256=f-8ijU-_y9r-uAbhIPn0f0I6hzJpAkvJzc8UpWihObI,8478
|
|
31
31
|
uiprotect/utils.py,sha256=3SJFF8qs1Jz8t3mD8qwc1hFSocolFjdXI_v4yVlC7o4,20088
|
|
32
32
|
uiprotect/websocket.py,sha256=D5DZrMzo434ecp8toNxOB5HM193kVwYw42yEcg99yMw,8029
|
|
33
|
-
uiprotect-3.1.
|
|
34
|
-
uiprotect-3.1.
|
|
35
|
-
uiprotect-3.1.
|
|
36
|
-
uiprotect-3.1.
|
|
37
|
-
uiprotect-3.1.
|
|
33
|
+
uiprotect-3.1.6.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
|
|
34
|
+
uiprotect-3.1.6.dist-info/METADATA,sha256=kvjDUNT1Q8B75cIVkrbdYm4em87HseeDcyJgQXlSttI,10982
|
|
35
|
+
uiprotect-3.1.6.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
36
|
+
uiprotect-3.1.6.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
|
|
37
|
+
uiprotect-3.1.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|