uiprotect 6.6.4__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.
- {uiprotect-6.6.4 → uiprotect-6.7.0}/PKG-INFO +1 -1
- {uiprotect-6.6.4 → uiprotect-6.7.0}/pyproject.toml +2 -2
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/api.py +16 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/data/bootstrap.py +67 -9
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/data/convert.py +16 -2
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/data/devices.py +2 -1
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/data/types.py +2 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/data/user.py +18 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/LICENSE +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/README.md +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/__init__.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/__main__.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/_compat.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/cli/__init__.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/cli/backup.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/cli/base.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/cli/cameras.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/cli/chimes.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/cli/doorlocks.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/cli/events.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/cli/lights.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/cli/liveviews.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/cli/nvr.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/cli/sensors.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/cli/viewers.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/data/__init__.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/data/base.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/data/nvr.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/data/websocket.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/exceptions.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/py.typed +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/release_cache.json +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/stream.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/test_util/__init__.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/test_util/anonymize.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/utils.py +0 -0
- {uiprotect-6.6.4 → uiprotect-6.7.0}/src/uiprotect/websocket.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "uiprotect"
|
|
3
|
-
version = "6.
|
|
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 = "
|
|
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
|
|
@@ -974,7 +974,8 @@ class Camera(ProtectMotionDeviceModel):
|
|
|
974
974
|
is_ptz: bool | None = None
|
|
975
975
|
# requires 2.11.13+
|
|
976
976
|
audio_settings: CameraAudioSettings | None = None
|
|
977
|
-
|
|
977
|
+
# requires 5.0.33+
|
|
978
|
+
is_third_party_camera: bool | None = None
|
|
978
979
|
# TODO: used for adopting
|
|
979
980
|
# apMac read only
|
|
980
981
|
# apRssi read only
|
|
@@ -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
|
|
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
|