uiprotect 0.10.1__tar.gz → 0.12.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-0.10.1 → uiprotect-0.12.0}/PKG-INFO +1 -1
- {uiprotect-0.10.1 → uiprotect-0.12.0}/pyproject.toml +1 -1
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/data/bootstrap.py +43 -58
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/data/websocket.py +9 -3
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/utils.py +6 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/LICENSE +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/README.md +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/__init__.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/__main__.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/api.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/cli/__init__.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/cli/backup.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/cli/base.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/cli/cameras.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/cli/chimes.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/cli/doorlocks.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/cli/events.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/cli/lights.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/cli/liveviews.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/cli/nvr.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/cli/sensors.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/cli/viewers.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/data/__init__.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/data/base.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/data/convert.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/data/devices.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/data/nvr.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/data/types.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/data/user.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/exceptions.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/py.typed +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/release_cache.json +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/stream.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/test_util/__init__.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/test_util/anonymize.py +0 -0
- {uiprotect-0.10.1 → uiprotect-0.12.0}/src/uiprotect/websocket.py +0 -0
|
@@ -18,7 +18,7 @@ except ImportError:
|
|
|
18
18
|
from pydantic import PrivateAttr, ValidationError # type: ignore[assignment]
|
|
19
19
|
|
|
20
20
|
from ..exceptions import ClientError
|
|
21
|
-
from ..utils import utc_now
|
|
21
|
+
from ..utils import normalize_mac, utc_now
|
|
22
22
|
from .base import (
|
|
23
23
|
RECENT_EVENT_MAX,
|
|
24
24
|
ProtectBaseObject,
|
|
@@ -41,7 +41,6 @@ from .types import EventType, FixSizeOrderedDict, ModelType
|
|
|
41
41
|
from .user import Group, User
|
|
42
42
|
from .websocket import (
|
|
43
43
|
WSAction,
|
|
44
|
-
WSJSONPacketFrame,
|
|
45
44
|
WSPacket,
|
|
46
45
|
WSSubscriptionMessage,
|
|
47
46
|
)
|
|
@@ -169,6 +168,10 @@ class ProtectDeviceRef(ProtectBaseObject):
|
|
|
169
168
|
id: str
|
|
170
169
|
|
|
171
170
|
|
|
171
|
+
_ModelType_NVR_value = ModelType.NVR.value
|
|
172
|
+
_ModelType_Event_value = ModelType.EVENT.value
|
|
173
|
+
|
|
174
|
+
|
|
172
175
|
class Bootstrap(ProtectBaseObject):
|
|
173
176
|
auth_user_id: str
|
|
174
177
|
access_key: str
|
|
@@ -223,7 +226,7 @@ class Bootstrap(ProtectBaseObject):
|
|
|
223
226
|
items[item["id"]] = item
|
|
224
227
|
data["idLookup"][item["id"]] = ref
|
|
225
228
|
if "mac" in item:
|
|
226
|
-
cleaned_mac = item["mac"]
|
|
229
|
+
cleaned_mac = normalize_mac(item["mac"])
|
|
227
230
|
data["macLookup"][cleaned_mac] = ref
|
|
228
231
|
data[key] = items
|
|
229
232
|
|
|
@@ -309,8 +312,7 @@ class Bootstrap(ProtectBaseObject):
|
|
|
309
312
|
|
|
310
313
|
def get_device_from_mac(self, mac: str) -> ProtectAdoptableDeviceModel | None:
|
|
311
314
|
"""Retrieve a device from MAC address."""
|
|
312
|
-
|
|
313
|
-
ref = self.mac_lookup.get(mac)
|
|
315
|
+
ref = self.mac_lookup.get(normalize_mac(mac))
|
|
314
316
|
if ref is None:
|
|
315
317
|
return None
|
|
316
318
|
|
|
@@ -375,7 +377,7 @@ class Bootstrap(ProtectBaseObject):
|
|
|
375
377
|
elif (
|
|
376
378
|
isinstance(obj, ProtectAdoptableDeviceModel)
|
|
377
379
|
and obj.model is not None
|
|
378
|
-
and obj.model.value in ModelType.
|
|
380
|
+
and obj.model.value in ModelType.bootstrap_models_set()
|
|
379
381
|
):
|
|
380
382
|
key = obj.model.value + "s"
|
|
381
383
|
if not self.api.ignore_unadopted or (
|
|
@@ -384,7 +386,7 @@ class Bootstrap(ProtectBaseObject):
|
|
|
384
386
|
getattr(self, key)[obj.id] = obj
|
|
385
387
|
ref = ProtectDeviceRef(model=obj.model, id=obj.id)
|
|
386
388
|
self.id_lookup[obj.id] = ref
|
|
387
|
-
self.mac_lookup[obj.mac
|
|
389
|
+
self.mac_lookup[normalize_mac(obj.mac)] = ref
|
|
388
390
|
else:
|
|
389
391
|
_LOGGER.debug("Unexpected bootstrap model type for add: %s", obj.model)
|
|
390
392
|
return None
|
|
@@ -400,23 +402,19 @@ class Bootstrap(ProtectBaseObject):
|
|
|
400
402
|
new_obj=obj,
|
|
401
403
|
)
|
|
402
404
|
|
|
403
|
-
def _process_remove_packet(
|
|
404
|
-
|
|
405
|
-
packet: WSPacket,
|
|
406
|
-
data: dict[str, Any] | None,
|
|
407
|
-
) -> WSSubscriptionMessage | None:
|
|
408
|
-
model = packet.action_frame.data.get("modelKey")
|
|
409
|
-
device_id = packet.action_frame.data.get("id")
|
|
405
|
+
def _process_remove_packet(self, packet: WSPacket) -> WSSubscriptionMessage | None:
|
|
406
|
+
model: str | None = packet.action_frame.data.get("modelKey")
|
|
410
407
|
devices = getattr(self, f"{model}s", None)
|
|
411
408
|
|
|
412
409
|
if devices is None:
|
|
413
410
|
return None
|
|
414
411
|
|
|
412
|
+
device_id: str = packet.action_frame.data["id"]
|
|
415
413
|
self.id_lookup.pop(device_id, None)
|
|
416
414
|
device = devices.pop(device_id, None)
|
|
417
415
|
if device is None:
|
|
418
416
|
return None
|
|
419
|
-
self.mac_lookup.pop(device.mac
|
|
417
|
+
self.mac_lookup.pop(normalize_mac(device.mac), None)
|
|
420
418
|
|
|
421
419
|
self._create_stat(packet, None, False)
|
|
422
420
|
return WSSubscriptionMessage(
|
|
@@ -486,12 +484,11 @@ class Bootstrap(ProtectBaseObject):
|
|
|
486
484
|
|
|
487
485
|
key = f"{model_type}s"
|
|
488
486
|
devices = getattr(self, key)
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
obj: ProtectModelWithId = devices[action["id"]]
|
|
487
|
+
action_id: str = action["id"]
|
|
488
|
+
if action_id in devices:
|
|
489
|
+
if action_id not in devices:
|
|
490
|
+
raise ValueError(f"Unknown device update for {model_type}: {action_id}")
|
|
491
|
+
obj: ProtectModelWithId = devices[action_id]
|
|
495
492
|
data = obj.unifi_dict_to_dict(data)
|
|
496
493
|
old_obj = obj.copy()
|
|
497
494
|
obj = obj.update_from_dict(deepcopy(data))
|
|
@@ -514,7 +511,7 @@ class Bootstrap(ProtectBaseObject):
|
|
|
514
511
|
if is_recent:
|
|
515
512
|
obj.set_alarm_timeout()
|
|
516
513
|
|
|
517
|
-
devices[
|
|
514
|
+
devices[action_id] = obj
|
|
518
515
|
|
|
519
516
|
self._create_stat(packet, data, False)
|
|
520
517
|
return WSSubscriptionMessage(
|
|
@@ -526,8 +523,8 @@ class Bootstrap(ProtectBaseObject):
|
|
|
526
523
|
)
|
|
527
524
|
|
|
528
525
|
# ignore updates to events that phase out
|
|
529
|
-
if model_type !=
|
|
530
|
-
_LOGGER.debug("Unexpected %s: %s", key,
|
|
526
|
+
if model_type != _ModelType_Event_value:
|
|
527
|
+
_LOGGER.debug("Unexpected %s: %s", key, action_id)
|
|
531
528
|
return None
|
|
532
529
|
|
|
533
530
|
def process_ws_packet(
|
|
@@ -536,51 +533,40 @@ class Bootstrap(ProtectBaseObject):
|
|
|
536
533
|
models: set[ModelType] | None = None,
|
|
537
534
|
ignore_stats: bool = False,
|
|
538
535
|
) -> WSSubscriptionMessage | None:
|
|
539
|
-
|
|
540
|
-
models = set()
|
|
541
|
-
|
|
542
|
-
if not isinstance(packet.action_frame, WSJSONPacketFrame):
|
|
543
|
-
_LOGGER.debug(
|
|
544
|
-
"Unexpected action frame format: %s",
|
|
545
|
-
packet.action_frame.payload_format,
|
|
546
|
-
)
|
|
547
|
-
|
|
548
|
-
if not isinstance(packet.data_frame, WSJSONPacketFrame):
|
|
549
|
-
_LOGGER.debug(
|
|
550
|
-
"Unexpected data frame format: %s",
|
|
551
|
-
packet.data_frame.payload_format,
|
|
552
|
-
)
|
|
553
|
-
|
|
536
|
+
"""Process a WS packet."""
|
|
554
537
|
action, data = self._get_frame_data(packet)
|
|
555
|
-
|
|
556
|
-
|
|
538
|
+
new_update_id: str = action["newUpdateId"]
|
|
539
|
+
if new_update_id is not None:
|
|
540
|
+
self.last_update_id = new_update_id
|
|
557
541
|
|
|
558
|
-
|
|
559
|
-
|
|
542
|
+
model_key: str = action["modelKey"]
|
|
543
|
+
if model_key not in ModelType.values_set():
|
|
544
|
+
_LOGGER.debug("Unknown model type: %s", model_key)
|
|
560
545
|
self._create_stat(packet, None, True)
|
|
561
546
|
return None
|
|
562
547
|
|
|
563
|
-
if
|
|
548
|
+
if models and ModelType(model_key) not in models:
|
|
564
549
|
self._create_stat(packet, None, True)
|
|
565
550
|
return None
|
|
566
551
|
|
|
567
|
-
|
|
568
|
-
|
|
552
|
+
action_action: str = action["action"]
|
|
553
|
+
if action_action == "remove":
|
|
554
|
+
return self._process_remove_packet(packet)
|
|
569
555
|
|
|
570
|
-
if
|
|
556
|
+
if not data:
|
|
571
557
|
self._create_stat(packet, None, True)
|
|
572
558
|
return None
|
|
573
559
|
|
|
574
560
|
try:
|
|
575
|
-
if
|
|
561
|
+
if action_action == "add":
|
|
576
562
|
return self._process_add_packet(packet, data)
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
if action["modelKey"] == ModelType.NVR.value:
|
|
563
|
+
if action_action == "update":
|
|
564
|
+
if model_key == _ModelType_NVR_value:
|
|
580
565
|
return self._process_nvr_update(packet, data, ignore_stats)
|
|
566
|
+
|
|
581
567
|
if (
|
|
582
|
-
|
|
583
|
-
or
|
|
568
|
+
model_key in ModelType.bootstrap_models_set()
|
|
569
|
+
or model_key == _ModelType_Event_value
|
|
584
570
|
):
|
|
585
571
|
return self._process_device_update(
|
|
586
572
|
packet,
|
|
@@ -588,13 +574,12 @@ class Bootstrap(ProtectBaseObject):
|
|
|
588
574
|
data,
|
|
589
575
|
ignore_stats,
|
|
590
576
|
)
|
|
591
|
-
_LOGGER.debug(
|
|
592
|
-
"Unexpected bootstrap model type deviceadoptedfor update: %s",
|
|
593
|
-
action["modelKey"],
|
|
594
|
-
)
|
|
595
577
|
except (ValidationError, ValueError) as err:
|
|
596
578
|
self._handle_ws_error(action, err)
|
|
597
579
|
|
|
580
|
+
_LOGGER.debug(
|
|
581
|
+
"Unexpected bootstrap model type deviceadoptedfor update: %s", model_key
|
|
582
|
+
)
|
|
598
583
|
self._create_stat(packet, None, True)
|
|
599
584
|
return None
|
|
600
585
|
|
|
@@ -605,7 +590,7 @@ class Bootstrap(ProtectBaseObject):
|
|
|
605
590
|
else:
|
|
606
591
|
try:
|
|
607
592
|
model_type = ModelType(action["modelKey"])
|
|
608
|
-
device_id = action["id"]
|
|
593
|
+
device_id: str = action["id"]
|
|
609
594
|
task = asyncio.create_task(self.refresh_device(model_type, device_id))
|
|
610
595
|
self._refresh_tasks.add(task)
|
|
611
596
|
task.add_done_callback(self._refresh_tasks.discard)
|
|
@@ -7,6 +7,7 @@ import enum
|
|
|
7
7
|
import struct
|
|
8
8
|
import zlib
|
|
9
9
|
from dataclasses import dataclass
|
|
10
|
+
from functools import cache, cached_property
|
|
10
11
|
from typing import TYPE_CHECKING, Any
|
|
11
12
|
|
|
12
13
|
import orjson
|
|
@@ -62,6 +63,7 @@ class BaseWSPacketFrame:
|
|
|
62
63
|
raise NotImplementedError
|
|
63
64
|
|
|
64
65
|
@staticmethod
|
|
66
|
+
@cache
|
|
65
67
|
def klass_from_format(format_raw: int) -> type[BaseWSPacketFrame]:
|
|
66
68
|
payload_format = ProtectWSPayloadFormat(format_raw)
|
|
67
69
|
|
|
@@ -174,13 +176,15 @@ class WSJSONPacketFrame(BaseWSPacketFrame):
|
|
|
174
176
|
|
|
175
177
|
|
|
176
178
|
class WSPacket:
|
|
179
|
+
"""Class to handle a unifi protect websocket packet."""
|
|
180
|
+
|
|
177
181
|
_raw: bytes
|
|
178
182
|
_raw_encoded: str | None = None
|
|
179
183
|
|
|
180
184
|
_action_frame: BaseWSPacketFrame | None = None
|
|
181
185
|
_data_frame: BaseWSPacketFrame | None = None
|
|
182
186
|
|
|
183
|
-
def __init__(self, data: bytes):
|
|
187
|
+
def __init__(self, data: bytes) -> None:
|
|
184
188
|
self._raw = data
|
|
185
189
|
|
|
186
190
|
def decode(self) -> None:
|
|
@@ -190,7 +194,7 @@ class WSPacket:
|
|
|
190
194
|
self._action_frame.length,
|
|
191
195
|
)
|
|
192
196
|
|
|
193
|
-
@
|
|
197
|
+
@cached_property
|
|
194
198
|
def action_frame(self) -> BaseWSPacketFrame:
|
|
195
199
|
if self._action_frame is None:
|
|
196
200
|
self.decode()
|
|
@@ -200,7 +204,7 @@ class WSPacket:
|
|
|
200
204
|
|
|
201
205
|
return self._action_frame
|
|
202
206
|
|
|
203
|
-
@
|
|
207
|
+
@cached_property
|
|
204
208
|
def data_frame(self) -> BaseWSPacketFrame:
|
|
205
209
|
if self._data_frame is None:
|
|
206
210
|
self.decode()
|
|
@@ -220,6 +224,8 @@ class WSPacket:
|
|
|
220
224
|
self._action_frame = None
|
|
221
225
|
self._data_frame = None
|
|
222
226
|
self._raw_encoded = None
|
|
227
|
+
self.__dict__.pop("data_frame", None)
|
|
228
|
+
self.__dict__.pop("action_frame", None)
|
|
223
229
|
|
|
224
230
|
@property
|
|
225
231
|
def raw_base64(self) -> str:
|
|
@@ -624,3 +624,9 @@ def clamp_value(value: float, step_size: float) -> float:
|
|
|
624
624
|
"""Clamps value to multiples of step size."""
|
|
625
625
|
ratio = 1 / step_size
|
|
626
626
|
return int(value * ratio) / ratio
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
@lru_cache(maxsize=1024)
|
|
630
|
+
def normalize_mac(mac: str) -> str:
|
|
631
|
+
"""Normalize MAC address."""
|
|
632
|
+
return mac.lower().replace(":", "").replace("-", "").replace("_", "")
|
|
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
|
|
File without changes
|
|
File without changes
|