Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add keyring and ulp-user #299

Merged
merged 52 commits into from
Dec 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
f2b9ca2
feat(keyring): add sample data for testing purposes
RaHehl Nov 30, 2024
e7f1ad9
feat(api): add ulp-user and keyring integration
RaHehl Nov 30, 2024
6bd9761
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Nov 30, 2024
482accb
feat(keyrings): work in progress on adding keyrings functionality
RaHehl Nov 30, 2024
aa1f628
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Nov 30, 2024
90a1248
refactor(api): streamline keyrings and ulpusers handling in ProtectAp…
RaHehl Dec 1, 2024
b0c23bb
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Dec 1, 2024
3070aaf
feat(api): add dict_from_unifi_list function and refactor keyrings an…
RaHehl Dec 6, 2024
096e8ce
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Dec 6, 2024
83c766e
refactor(api): replace get_keyrings and get_ulpusers methods with dir…
RaHehl Dec 6, 2024
49edd9b
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Dec 6, 2024
0652cf6
fix(api): conditionally assign keyrings and ulp_users based on NVR ve…
RaHehl Dec 6, 2024
f9efb5a
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Dec 6, 2024
c44070f
refactor(api): update dict_from_unifi_list to use Any type for return…
RaHehl Dec 6, 2024
232a671
fix(api): convert keys to snake_case in ProtectBaseObject data proces…
RaHehl Dec 6, 2024
d82a0d6
refactor(api): rename keyring and ulp_user update methods for clarity…
RaHehl Dec 6, 2024
a7821ff
test(api): add websocket tests for keyring add, update, and remove ac…
RaHehl Dec 6, 2024
8189ba3
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Dec 6, 2024
b494a08
test(api): add websocket tests for keyring add actions with NFC and f…
RaHehl Dec 6, 2024
f958f7a
feat(api): define NFC fingerprint support version as constant
RaHehl Dec 6, 2024
bbcfff8
test(api): improve formatting in NFC keyring add test
RaHehl Dec 6, 2024
3d40a57
refactor(api): improve bootstrap update pop after keyring ulpusr requ…
RaHehl Dec 6, 2024
c038560
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Dec 6, 2024
ed3b092
refactor(api): remove to_snake_case from update_from_dict
RaHehl Dec 6, 2024
6405e90
refactor(api): typed dict_from_unifi_list
RaHehl Dec 6, 2024
7b37530
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Dec 6, 2024
d9d24dd
refactor(api): update dict_from_unifi_list to use ProtectModelWithId …
RaHehl Dec 6, 2024
456a809
refactor(api): consolidate keyring and ULP user message processing in…
RaHehl Dec 6, 2024
d268d7a
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Dec 6, 2024
96cab1d
refactor(api): update device key retrieval and add ULP user managemen…
RaHehl Dec 6, 2024
cacd825
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Dec 6, 2024
e6f6910
refactor(api): improve object removal and update handling in Bootstra…
RaHehl Dec 6, 2024
61020d8
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Dec 6, 2024
99163a8
refactor(api): streamline action handling in Bootstrap class
RaHehl Dec 6, 2024
c636bfc
Merge branch 'uilibs:main' into add-keyring
RaHehl Dec 6, 2024
426f5ab
refactor(api): remove unused user message processing method in Bootst…
RaHehl Dec 6, 2024
5b5e8b6
refactor(api): move dict_from_unifi_list function to convert module
RaHehl Dec 7, 2024
a3fb886
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Dec 7, 2024
2fcfffa
test(api): add tests for force update with version checks
RaHehl Dec 7, 2024
58cce26
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Dec 7, 2024
f64c321
test(api): remove outdated NFC fingerprint support version tests
RaHehl Dec 7, 2024
c2171fb
test(data): remove additional keys from obj_dict in bootstrap test
RaHehl Dec 7, 2024
8500629
fix(data): enhance type checking for model class in bootstrap and upd…
RaHehl Dec 7, 2024
ae5bb87
fix(data): improve type handling in Bootstrap and convert functions f…
RaHehl Dec 7, 2024
79d6d34
fix(api): enhance type safety by casting keyrings and ulp_users in Pr…
RaHehl Dec 7, 2024
fa5ffbf
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Dec 7, 2024
a7bce24
fix(api): remove type check for ProtectModelWithId and enhance mock d…
RaHehl Dec 7, 2024
fc437bc
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Dec 7, 2024
4c81f0e
fix(api): add debug logging for unexpected websocket actions and enha…
RaHehl Dec 7, 2024
1d23987
chore(pre-commit.ci): auto fixes
pre-commit-ci[bot] Dec 7, 2024
b7bf1b2
fix(data): initialize keyrings and ulp_users as empty dictionaries; u…
RaHehl Dec 7, 2024
1e9e3a7
fix: lint
bdraco Dec 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/uiprotect/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
from platformdirs import user_cache_dir, user_config_dir
from yarl import URL

from uiprotect.data.convert import dict_from_unifi_list
from uiprotect.data.user import Keyring, UlpUser

from ._compat import cached_property
from .data import (
NVR,
Expand Down Expand Up @@ -90,6 +93,8 @@
_LOGGER = logging.getLogger(__name__)
_COOKIE_RE = re.compile(r"^set-cookie: ", re.IGNORECASE)

NFC_FINGERPRINT_SUPPORT_VERSION = Version("5.1.57")

# TODO: Urls to still support
# Backups
# * GET /backups - list backends
Expand Down Expand Up @@ -823,6 +828,17 @@ async def update(self) -> Bootstrap:
"""
async with self._update_lock:
bootstrap = await self.get_bootstrap()
if bootstrap.nvr.version >= NFC_FINGERPRINT_SUPPORT_VERSION:
bootstrap.keyrings = cast(
dict[str, Keyring],
dict_from_unifi_list(self, await self.api_request_list("keyrings")),
)
bootstrap.ulp_users = cast(
dict[str, UlpUser],
dict_from_unifi_list(
self, await self.api_request_list("ulp-users")
),
)
RaHehl marked this conversation as resolved.
Show resolved Hide resolved
self.__dict__.pop("bootstrap", None)
self._bootstrap = bootstrap
return bootstrap
Expand Down
76 changes: 67 additions & 9 deletions src/uiprotect/data/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@
import logging
from dataclasses import dataclass
from datetime import datetime
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast

from aiohttp.client_exceptions import ServerDisconnectedError
from convertertools import pop_dict_set, pop_dict_tuple
from pydantic.v1 import PrivateAttr, ValidationError

from ..exceptions import ClientError
from ..utils import normalize_mac, utc_now
from ..utils import normalize_mac, to_snake_case, utc_now
from .base import (
RECENT_EVENT_MAX,
ProtectBaseObject,
ProtectDeviceModel,
ProtectModel,
ProtectModelWithId,
)
from .convert import create_from_unifi_dict
from .convert import MODEL_TO_CLASS, create_from_unifi_dict
from .devices import (
Bridge,
Camera,
Expand All @@ -34,7 +34,7 @@
)
from .nvr import NVR, Event, Liveview
from .types import EventType, FixSizeOrderedDict, ModelType
from .user import Group, User
from .user import Group, Keyring, UlpUser, User
from .websocket import (
WSAction,
WSPacket,
Expand Down Expand Up @@ -188,6 +188,8 @@
# agreements

# not directly from UniFi
keyrings: dict[str, Keyring] = {}
ulp_users: dict[str, UlpUser] = {}
events: dict[str, Event] = FixSizeOrderedDict()
capture_ws_stats: bool = False
mac_lookup: dict[str, ProtectDeviceRef] = {}
Expand Down Expand Up @@ -384,6 +386,59 @@
old_obj=device,
)

def _process_ws_keyring_or_ulp_user_message(
self,
action: dict[str, Any],
data: dict[str, Any],
model_type: ModelType,
) -> WSSubscriptionMessage | None:
action_id = action["id"]
dict_from_bootstrap: dict[str, ProtectModelWithId] = getattr(
self, to_snake_case(model_type.devices_key)
)
action_type = action["action"]
if action_type == "add":
add_obj = create_from_unifi_dict(data, api=self._api, model_type=model_type)
if TYPE_CHECKING:
model_class = MODEL_TO_CLASS.get(model_type)
assert model_class is not None and isinstance(add_obj, model_class)
add_obj = cast(ProtectModelWithId, add_obj)
dict_from_bootstrap[add_obj.id] = add_obj
return WSSubscriptionMessage(
action=WSAction.ADD,
new_update_id=self.last_update_id,
changed_data=add_obj.dict(),
new_obj=add_obj,
)
elif action_type == "remove":
removed_obj = dict_from_bootstrap.pop(action_id, None)
if removed_obj is None:
return None
RaHehl marked this conversation as resolved.
Show resolved Hide resolved
return WSSubscriptionMessage(
action=WSAction.REMOVE,
new_update_id=self.last_update_id,
changed_data={},
old_obj=removed_obj,
)
elif action_type == "update":
updated_obj = dict_from_bootstrap.get(action_id)
if updated_obj is None:
return None
RaHehl marked this conversation as resolved.
Show resolved Hide resolved

old_obj = updated_obj.copy()
updated_data = {to_snake_case(k): v for k, v in data.items()}
updated_obj.update_from_dict(updated_data)

return WSSubscriptionMessage(
action=WSAction.UPDATE,
new_update_id=self.last_update_id,
changed_data=updated_data,
new_obj=updated_obj,
old_obj=old_obj,
)
_LOGGER.debug("Unexpected ws action for %s: %s", model_type, action_type)
return None
RaHehl marked this conversation as resolved.
Show resolved Hide resolved

def _process_nvr_update(
self,
action: dict[str, Any],
Expand Down Expand Up @@ -540,13 +595,16 @@
return None

action_action: str = action["action"]
if action_action == "remove":
return self._process_remove_packet(model_type, action)

if not data and not is_ping_back:
return None

try:
if model_type in {ModelType.KEYRING, ModelType.ULP_USER}:
return self._process_ws_keyring_or_ulp_user_message(
action, data, model_type
)
if action_action == "remove":
return self._process_remove_packet(model_type, action)

Check warning on line 605 in src/uiprotect/data/bootstrap.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/data/bootstrap.py#L605

Added line #L605 was not covered by tests
RaHehl marked this conversation as resolved.
Show resolved Hide resolved
if not data and not is_ping_back:
return None

Check warning on line 607 in src/uiprotect/data/bootstrap.py

View check run for this annotation

Codecov / codecov/patch

src/uiprotect/data/bootstrap.py#L607

Added line #L607 was not covered by tests
RaHehl marked this conversation as resolved.
Show resolved Hide resolved
if action_action == "add":
return self._process_add_packet(model_type, data)
if action_action == "update":
Expand Down
18 changes: 16 additions & 2 deletions src/uiprotect/data/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

from __future__ import annotations

from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast

from uiprotect.data.base import ProtectModelWithId

from ..exceptions import DataDecodeError
from .devices import (
Expand All @@ -16,7 +18,7 @@
)
from .nvr import NVR, Event, Liveview
from .types import ModelType
from .user import CloudAccount, Group, User, UserLocation
from .user import CloudAccount, Group, Keyring, UlpUser, User, UserLocation

if TYPE_CHECKING:
from ..api import ProtectApiClient
Expand All @@ -38,6 +40,8 @@
ModelType.SENSOR: Sensor,
ModelType.DOORLOCK: Doorlock,
ModelType.CHIME: Chime,
ModelType.KEYRING: Keyring,
ModelType.ULP_USER: UlpUser,
}


Expand Down Expand Up @@ -79,3 +83,13 @@ def create_from_unifi_dict(
klass = get_klass_from_dict(data)

return klass.from_unifi_dict(**data, api=api)


def dict_from_unifi_list(
api: ProtectApiClient, unifi_list: list[dict[str, ProtectModelWithId]]
RaHehl marked this conversation as resolved.
Show resolved Hide resolved
) -> dict[str, ProtectModelWithId]:
return_dict: dict[str, ProtectModelWithId] = {}
for obj_dict in unifi_list:
obj = cast(ProtectModelWithId, create_from_unifi_dict(obj_dict, api))
return_dict[obj.id] = obj
return return_dict
2 changes: 2 additions & 0 deletions src/uiprotect/data/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ class ModelType(str, UnknownValuesEnumMixin, enum.Enum):
CHIME = "chime"
DEVICE_GROUP = "deviceGroup"
RECORDING_SCHEDULE = "recordingSchedule"
ULP_USER = "ulpUser"
KEYRING = "keyring"
UNKNOWN = "unknown"

bootstrap_model_types: tuple[ModelType, ...]
Expand Down
18 changes: 18 additions & 0 deletions src/uiprotect/data/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,21 @@ def can(
return True
self._perm_cache[perm_str] = False
return False


class Keyring(ProtectModelWithId):
device_type: str
device_id: str
registry_type: str
registry_id: str
last_activity: datetime | None = None
ulp_user: str


class UlpUser(ProtectModelWithId):
ulp_id: str
first_name: str
last_name: str
full_name: str
avatar: str
status: str
4 changes: 4 additions & 0 deletions tests/data/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,10 @@ def test_bootstrap(bootstrap: dict[str, Any]):
if "deviceGroups" in bootstrap:
del bootstrap["deviceGroups"]

# Remove additional keys from obj_dict
obj_dict.pop("keyrings", None)
obj_dict.pop("ulpUsers", None)

for model_type in ModelType.bootstrap_models:
key = model_type + "s"
expected_data = bootstrap.pop(key)
Expand Down
22 changes: 22 additions & 0 deletions tests/sample_data/sample_keyrings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[
{
"deviceType": "camera",
"deviceId": "1c9a2db4df6efda47a3509be",
"registryType": "nfc",
"registryId": "64B2A621",
"lastActivity": 1732904108638,
"ulpUser": "73791632-9805-419c-8351-f3afaab8f064",
"id": "672b573764f79603e400031d",
"modelKey": "keyring"
},
{
"deviceType": "camera",
"deviceId": "1c9a2db4df6efda47a3509be",
"registryType": "fingerprint",
"registryId": "1",
"lastActivity": 1732904119477,
"ulpUser": "0ef32f28-f654-404d-ab34-30e373e66436",
"id": "672b573764f79603e44871d",
"modelKey": "keyring"
}
]
32 changes: 32 additions & 0 deletions tests/sample_data/sample_ulp_users.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[
{
"ulpId": "73791632-9805-419c-8351-f3afaab8f064",
"firstName": "John Doe",
"lastName": "",
"fullName": "John Doe",
"avatar": "",
"status": "ACTIVE",
"id": "73791632-9805-419c-8351-f3afaab8f064",
"modelKey": "ulpUser"
},
{
"ulpId": "ddec43ea-1845-4a50-bdab-83bcd4b3c81d",
"firstName": "Jane Doe",
"lastName": "",
"fullName": "Jane Doe",
"avatar": "",
"status": "ACTIVE",
"id": "ddec43ea-1845-4a50-bdab-83bcd4b3c81d",
"modelKey": "ulpUser"
},
{
"ulpId": "0ef32f28-f654-404d-ab34-30e373e66436",
"firstName": "You Know Who",
"lastName": "",
"fullName": "You Know Who",
"avatar": "/proxy/users/public/avatar/1732954155_589f14b3-b137-4487-9823-db62bb793c19.jpg",
"status": "DEACTIVATED",
"id": "0ef32f28-f654-404d-ab34-30e373e66436",
"modelKey": "ulpUser"
}
]
Loading
Loading