uiprotect 6.6.5__tar.gz → 6.7.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.

Files changed (37) hide show
  1. {uiprotect-6.6.5 → uiprotect-6.7.0}/PKG-INFO +1 -1
  2. {uiprotect-6.6.5 → uiprotect-6.7.0}/pyproject.toml +2 -2
  3. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/api.py +16 -0
  4. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/data/bootstrap.py +67 -9
  5. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/data/convert.py +16 -2
  6. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/data/types.py +2 -0
  7. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/data/user.py +18 -0
  8. {uiprotect-6.6.5 → uiprotect-6.7.0}/LICENSE +0 -0
  9. {uiprotect-6.6.5 → uiprotect-6.7.0}/README.md +0 -0
  10. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/__init__.py +0 -0
  11. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/__main__.py +0 -0
  12. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/_compat.py +0 -0
  13. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/cli/__init__.py +0 -0
  14. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/cli/backup.py +0 -0
  15. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/cli/base.py +0 -0
  16. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/cli/cameras.py +0 -0
  17. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/cli/chimes.py +0 -0
  18. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/cli/doorlocks.py +0 -0
  19. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/cli/events.py +0 -0
  20. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/cli/lights.py +0 -0
  21. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/cli/liveviews.py +0 -0
  22. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/cli/nvr.py +0 -0
  23. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/cli/sensors.py +0 -0
  24. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/cli/viewers.py +0 -0
  25. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/data/__init__.py +0 -0
  26. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/data/base.py +0 -0
  27. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/data/devices.py +0 -0
  28. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/data/nvr.py +0 -0
  29. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/data/websocket.py +0 -0
  30. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/exceptions.py +0 -0
  31. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/py.typed +0 -0
  32. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/release_cache.json +0 -0
  33. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/stream.py +0 -0
  34. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/test_util/__init__.py +0 -0
  35. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/test_util/anonymize.py +0 -0
  36. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/utils.py +0 -0
  37. {uiprotect-6.6.5 → uiprotect-6.7.0}/src/uiprotect/websocket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: uiprotect
3
- Version: 6.6.5
3
+ Version: 6.7.0
4
4
  Summary: Python API for Unifi Protect (Unofficial)
5
5
  Home-page: https://github.com/uilibs/uiprotect
6
6
  Author: UI Protect Maintainers
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "uiprotect"
3
- version = "6.6.5"
3
+ version = "6.7.0"
4
4
  description = "Python API for Unifi Protect (Unofficial)"
5
5
  authors = ["UI Protect Maintainers <ui@koston.org>"]
6
6
  readme = "README.md"
@@ -52,7 +52,7 @@ propcache = ">=0.0.0"
52
52
  pytest = ">=7,<9"
53
53
  pytest-cov = ">=3,<7"
54
54
  aiosqlite = ">=0.20.0"
55
- asttokens = "^2.4.1"
55
+ asttokens = ">=2.4.1,<4.0.0"
56
56
  pytest-asyncio = ">=0.23.7,<0.25.0"
57
57
  pytest-benchmark = ">=4,<6"
58
58
  pytest-sugar = "^1.0.0"
@@ -27,6 +27,9 @@ from aiohttp import CookieJar, client_exceptions
27
27
  from platformdirs import user_cache_dir, user_config_dir
28
28
  from yarl import URL
29
29
 
30
+ from uiprotect.data.convert import dict_from_unifi_list
31
+ from uiprotect.data.user import Keyring, UlpUser
32
+
30
33
  from ._compat import cached_property
31
34
  from .data import (
32
35
  NVR,
@@ -90,6 +93,8 @@ If your Protect instance has a lot of events, this request will take much longer
90
93
  _LOGGER = logging.getLogger(__name__)
91
94
  _COOKIE_RE = re.compile(r"^set-cookie: ", re.IGNORECASE)
92
95
 
96
+ NFC_FINGERPRINT_SUPPORT_VERSION = Version("5.1.57")
97
+
93
98
  # TODO: Urls to still support
94
99
  # Backups
95
100
  # * GET /backups - list backends
@@ -823,6 +828,17 @@ class ProtectApiClient(BaseApiClient):
823
828
  """
824
829
  async with self._update_lock:
825
830
  bootstrap = await self.get_bootstrap()
831
+ if bootstrap.nvr.version >= NFC_FINGERPRINT_SUPPORT_VERSION:
832
+ bootstrap.keyrings = cast(
833
+ dict[str, Keyring],
834
+ dict_from_unifi_list(self, await self.api_request_list("keyrings")),
835
+ )
836
+ bootstrap.ulp_users = cast(
837
+ dict[str, UlpUser],
838
+ dict_from_unifi_list(
839
+ self, await self.api_request_list("ulp-users")
840
+ ),
841
+ )
826
842
  self.__dict__.pop("bootstrap", None)
827
843
  self._bootstrap = bootstrap
828
844
  return bootstrap
@@ -6,14 +6,14 @@ import asyncio
6
6
  import logging
7
7
  from dataclasses import dataclass
8
8
  from datetime import datetime
9
- from typing import TYPE_CHECKING, Any
9
+ from typing import TYPE_CHECKING, Any, cast
10
10
 
11
11
  from aiohttp.client_exceptions import ServerDisconnectedError
12
12
  from convertertools import pop_dict_set, pop_dict_tuple
13
13
  from pydantic.v1 import PrivateAttr, ValidationError
14
14
 
15
15
  from ..exceptions import ClientError
16
- from ..utils import normalize_mac, utc_now
16
+ from ..utils import normalize_mac, to_snake_case, utc_now
17
17
  from .base import (
18
18
  RECENT_EVENT_MAX,
19
19
  ProtectBaseObject,
@@ -21,7 +21,7 @@ from .base import (
21
21
  ProtectModel,
22
22
  ProtectModelWithId,
23
23
  )
24
- from .convert import create_from_unifi_dict
24
+ from .convert import MODEL_TO_CLASS, create_from_unifi_dict
25
25
  from .devices import (
26
26
  Bridge,
27
27
  Camera,
@@ -34,7 +34,7 @@ from .devices import (
34
34
  )
35
35
  from .nvr import NVR, Event, Liveview
36
36
  from .types import EventType, FixSizeOrderedDict, ModelType
37
- from .user import Group, User
37
+ from .user import Group, Keyring, UlpUser, User
38
38
  from .websocket import (
39
39
  WSAction,
40
40
  WSPacket,
@@ -188,6 +188,8 @@ class Bootstrap(ProtectBaseObject):
188
188
  # agreements
189
189
 
190
190
  # not directly from UniFi
191
+ keyrings: dict[str, Keyring] = {}
192
+ ulp_users: dict[str, UlpUser] = {}
191
193
  events: dict[str, Event] = FixSizeOrderedDict()
192
194
  capture_ws_stats: bool = False
193
195
  mac_lookup: dict[str, ProtectDeviceRef] = {}
@@ -384,6 +386,59 @@ class Bootstrap(ProtectBaseObject):
384
386
  old_obj=device,
385
387
  )
386
388
 
389
+ def _process_ws_keyring_or_ulp_user_message(
390
+ self,
391
+ action: dict[str, Any],
392
+ data: dict[str, Any],
393
+ model_type: ModelType,
394
+ ) -> WSSubscriptionMessage | None:
395
+ action_id = action["id"]
396
+ dict_from_bootstrap: dict[str, ProtectModelWithId] = getattr(
397
+ self, to_snake_case(model_type.devices_key)
398
+ )
399
+ action_type = action["action"]
400
+ if action_type == "add":
401
+ add_obj = create_from_unifi_dict(data, api=self._api, model_type=model_type)
402
+ if TYPE_CHECKING:
403
+ model_class = MODEL_TO_CLASS.get(model_type)
404
+ assert model_class is not None and isinstance(add_obj, model_class)
405
+ add_obj = cast(ProtectModelWithId, add_obj)
406
+ dict_from_bootstrap[add_obj.id] = add_obj
407
+ return WSSubscriptionMessage(
408
+ action=WSAction.ADD,
409
+ new_update_id=self.last_update_id,
410
+ changed_data=add_obj.dict(),
411
+ new_obj=add_obj,
412
+ )
413
+ elif action_type == "remove":
414
+ removed_obj = dict_from_bootstrap.pop(action_id, None)
415
+ if removed_obj is None:
416
+ return None
417
+ return WSSubscriptionMessage(
418
+ action=WSAction.REMOVE,
419
+ new_update_id=self.last_update_id,
420
+ changed_data={},
421
+ old_obj=removed_obj,
422
+ )
423
+ elif action_type == "update":
424
+ updated_obj = dict_from_bootstrap.get(action_id)
425
+ if updated_obj is None:
426
+ return None
427
+
428
+ old_obj = updated_obj.copy()
429
+ updated_data = {to_snake_case(k): v for k, v in data.items()}
430
+ updated_obj.update_from_dict(updated_data)
431
+
432
+ return WSSubscriptionMessage(
433
+ action=WSAction.UPDATE,
434
+ new_update_id=self.last_update_id,
435
+ changed_data=updated_data,
436
+ new_obj=updated_obj,
437
+ old_obj=old_obj,
438
+ )
439
+ _LOGGER.debug("Unexpected ws action for %s: %s", model_type, action_type)
440
+ return None
441
+
387
442
  def _process_nvr_update(
388
443
  self,
389
444
  action: dict[str, Any],
@@ -540,13 +595,16 @@ class Bootstrap(ProtectBaseObject):
540
595
  return None
541
596
 
542
597
  action_action: str = action["action"]
543
- if action_action == "remove":
544
- return self._process_remove_packet(model_type, action)
545
-
546
- if not data and not is_ping_back:
547
- return None
548
598
 
549
599
  try:
600
+ if model_type in {ModelType.KEYRING, ModelType.ULP_USER}:
601
+ return self._process_ws_keyring_or_ulp_user_message(
602
+ action, data, model_type
603
+ )
604
+ if action_action == "remove":
605
+ return self._process_remove_packet(model_type, action)
606
+ if not data and not is_ping_back:
607
+ return None
550
608
  if action_action == "add":
551
609
  return self._process_add_packet(model_type, data)
552
610
  if action_action == "update":
@@ -2,7 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Any
5
+ from typing import TYPE_CHECKING, Any, cast
6
+
7
+ from uiprotect.data.base import ProtectModelWithId
6
8
 
7
9
  from ..exceptions import DataDecodeError
8
10
  from .devices import (
@@ -16,7 +18,7 @@ from .devices import (
16
18
  )
17
19
  from .nvr import NVR, Event, Liveview
18
20
  from .types import ModelType
19
- from .user import CloudAccount, Group, User, UserLocation
21
+ from .user import CloudAccount, Group, Keyring, UlpUser, User, UserLocation
20
22
 
21
23
  if TYPE_CHECKING:
22
24
  from ..api import ProtectApiClient
@@ -38,6 +40,8 @@ MODEL_TO_CLASS: dict[str, type[ProtectModel]] = {
38
40
  ModelType.SENSOR: Sensor,
39
41
  ModelType.DOORLOCK: Doorlock,
40
42
  ModelType.CHIME: Chime,
43
+ ModelType.KEYRING: Keyring,
44
+ ModelType.ULP_USER: UlpUser,
41
45
  }
42
46
 
43
47
 
@@ -79,3 +83,13 @@ def create_from_unifi_dict(
79
83
  klass = get_klass_from_dict(data)
80
84
 
81
85
  return klass.from_unifi_dict(**data, api=api)
86
+
87
+
88
+ def dict_from_unifi_list(
89
+ api: ProtectApiClient, unifi_list: list[dict[str, ProtectModelWithId]]
90
+ ) -> dict[str, ProtectModelWithId]:
91
+ return_dict: dict[str, ProtectModelWithId] = {}
92
+ for obj_dict in unifi_list:
93
+ obj = cast(ProtectModelWithId, create_from_unifi_dict(obj_dict, api))
94
+ return_dict[obj.id] = obj
95
+ return return_dict
@@ -105,6 +105,8 @@ class ModelType(str, UnknownValuesEnumMixin, enum.Enum):
105
105
  CHIME = "chime"
106
106
  DEVICE_GROUP = "deviceGroup"
107
107
  RECORDING_SCHEDULE = "recordingSchedule"
108
+ ULP_USER = "ulpUser"
109
+ KEYRING = "keyring"
108
110
  UNKNOWN = "unknown"
109
111
 
110
112
  bootstrap_model_types: tuple[ModelType, ...]
@@ -234,3 +234,21 @@ class User(ProtectModelWithId):
234
234
  return True
235
235
  self._perm_cache[perm_str] = False
236
236
  return False
237
+
238
+
239
+ class Keyring(ProtectModelWithId):
240
+ device_type: str
241
+ device_id: str
242
+ registry_type: str
243
+ registry_id: str
244
+ last_activity: datetime | None = None
245
+ ulp_user: str
246
+
247
+
248
+ class UlpUser(ProtectModelWithId):
249
+ ulp_id: str
250
+ first_name: str
251
+ last_name: str
252
+ full_name: str
253
+ avatar: str
254
+ status: str
File without changes
File without changes