uiprotect 3.1.5__tar.gz → 3.1.6__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.

Files changed (36) hide show
  1. {uiprotect-3.1.5 → uiprotect-3.1.6}/PKG-INFO +1 -1
  2. {uiprotect-3.1.5 → uiprotect-3.1.6}/pyproject.toml +1 -1
  3. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/data/base.py +37 -34
  4. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/data/devices.py +9 -12
  5. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/data/nvr.py +1 -1
  6. {uiprotect-3.1.5 → uiprotect-3.1.6}/LICENSE +0 -0
  7. {uiprotect-3.1.5 → uiprotect-3.1.6}/README.md +0 -0
  8. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/__init__.py +0 -0
  9. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/__main__.py +0 -0
  10. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/api.py +0 -0
  11. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/cli/__init__.py +0 -0
  12. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/cli/backup.py +0 -0
  13. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/cli/base.py +0 -0
  14. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/cli/cameras.py +0 -0
  15. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/cli/chimes.py +0 -0
  16. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/cli/doorlocks.py +0 -0
  17. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/cli/events.py +0 -0
  18. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/cli/lights.py +0 -0
  19. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/cli/liveviews.py +0 -0
  20. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/cli/nvr.py +0 -0
  21. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/cli/sensors.py +0 -0
  22. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/cli/viewers.py +0 -0
  23. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/data/__init__.py +0 -0
  24. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/data/bootstrap.py +0 -0
  25. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/data/convert.py +0 -0
  26. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/data/types.py +0 -0
  27. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/data/user.py +0 -0
  28. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/data/websocket.py +0 -0
  29. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/exceptions.py +0 -0
  30. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/py.typed +0 -0
  31. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/release_cache.json +0 -0
  32. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/stream.py +0 -0
  33. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/test_util/__init__.py +0 -0
  34. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/test_util/anonymize.py +0 -0
  35. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/utils.py +0 -0
  36. {uiprotect-3.1.5 → uiprotect-3.1.6}/src/uiprotect/websocket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: uiprotect
3
- Version: 3.1.5
3
+ Version: 3.1.6
4
4
  Summary: Python API for Unifi Protect (Unofficial)
5
5
  Home-page: https://github.com/uilibs/uiprotect
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "uiprotect"
3
- version = "3.1.5"
3
+ version = "3.1.6"
4
4
  description = "Python API for Unifi Protect (Unofficial)"
5
5
  authors = ["UI Protect Maintainers <ui@koston.org>"]
6
6
  license = "MIT"
@@ -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
- _update_lock: asyncio.Lock = PrivateAttr(None)
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
- update_lock = data.pop("update_lock", None)
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._update_lock = update_lock or asyncio.Lock()
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
- update_lock = values.pop("update_lock", None)
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._update_lock = update_lock or asyncio.Lock()
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._update_queue.put_nowait(callback)
615
+ self._update_sync.queue.put_nowait(callback)
613
616
 
614
- self._update_event.set()
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._update_event.clear()
621
+ self._update_sync.event.clear()
619
622
 
620
623
  try:
621
624
  async with asyncio_timeout(0.05):
622
- await self._update_event.wait()
623
- self._update_event.clear()
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._update_lock:
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._update_queue.empty():
633
- callback = self._update_queue.get_nowait()
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._update_lock.locked():
664
- await self._update_lock.acquire()
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._update_lock.release()
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._update_lock.locked()
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
 
@@ -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._update_lock:
163
- await asyncio.sleep(
164
- 0,
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._update_lock:
2382
- await asyncio.sleep(
2383
- 0,
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._update_lock:
2708
- await asyncio.sleep(
2709
- 0,
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
@@ -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._update_lock:
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()
File without changes
File without changes