uiprotect 0.10.1__py3-none-any.whl → 0.12.0__py3-none-any.whl

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.

@@ -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"].lower().replace(":", "")
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
- mac = mac.lower().replace(":", "").replace("-", "").replace("_", "")
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.bootstrap_models()
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.lower().replace(":", "")] = ref
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
- self,
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.lower().replace(":", ""), None)
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
- if action["id"] in devices:
490
- if action["id"] not in devices:
491
- raise ValueError(
492
- f"Unknown device update for {model_type}: { action['id']}",
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[action["id"]] = obj
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 != ModelType.EVENT.value:
530
- _LOGGER.debug("Unexpected %s: %s", key, action["id"])
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
- if models is None:
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
- if action["newUpdateId"] is not None:
556
- self.last_update_id = action["newUpdateId"]
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
- if action["modelKey"] not in ModelType.values_set():
559
- _LOGGER.debug("Unknown model type: %s", action["modelKey"])
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 len(models) > 0 and ModelType(action["modelKey"]) not in models:
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
- if action["action"] == "remove":
568
- return self._process_remove_packet(packet, data)
552
+ action_action: str = action["action"]
553
+ if action_action == "remove":
554
+ return self._process_remove_packet(packet)
569
555
 
570
- if data is None or len(data) == 0:
556
+ if not data:
571
557
  self._create_stat(packet, None, True)
572
558
  return None
573
559
 
574
560
  try:
575
- if action["action"] == "add":
561
+ if action_action == "add":
576
562
  return self._process_add_packet(packet, data)
577
-
578
- if action["action"] == "update":
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
- action["modelKey"] in ModelType.bootstrap_models_set()
583
- or action["modelKey"] == ModelType.EVENT.value
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
- @property
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
- @property
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:
uiprotect/utils.py CHANGED
@@ -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("_", "")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: uiprotect
3
- Version: 0.10.1
3
+ Version: 0.12.0
4
4
  Summary: Python API for Unifi Protect (Unofficial)
5
5
  Home-page: https://github.com/uilibs/uiprotect
6
6
  License: MIT
@@ -15,23 +15,23 @@ uiprotect/cli/sensors.py,sha256=fQtcDJCVxs4VbAqcavgBy2ABiVxAW3GXtna6_XFBp2k,8153
15
15
  uiprotect/cli/viewers.py,sha256=2cyrp104ffIvgT0wYGIO0G35QMkEbFe7fSVqLwDXQYQ,2171
16
16
  uiprotect/data/__init__.py,sha256=OcfuJl2qXfHcj_mdnrHhzZ5tEIZrw8auziX5IE7dn-I,2938
17
17
  uiprotect/data/base.py,sha256=ex-UC9CJUtzxMFqtYokSiXM8pNHVBqCzq7r8WrEf1Mw,37178
18
- uiprotect/data/bootstrap.py,sha256=TzrwRfI4Jt0zDx_tKxGS6AM3xRixB8iHOVY7CgeM7Qg,21952
18
+ uiprotect/data/bootstrap.py,sha256=RcqIiIk8z5QKDMnrWB7ZZDlBouesx2xQGN02wEl-B9s,21416
19
19
  uiprotect/data/convert.py,sha256=rOQplUMIdTMD2SbAx_iI9BNPDscnhDvyRVLEMDhtADg,2047
20
20
  uiprotect/data/devices.py,sha256=LHVBT8ihMAZen7gIlQNbiYxukRrBpi_TNKNmV_5R6Xc,111705
21
21
  uiprotect/data/nvr.py,sha256=OJso6oewA_jY7ovKbD2U2Onp1GqheT5bCW0F6dC53DQ,47570
22
22
  uiprotect/data/types.py,sha256=6Z5ZqWTbH4Igy0l4QJShqQZ_zvrJKD0G-hZLjoBNP-U,16193
23
23
  uiprotect/data/user.py,sha256=yBnUQ3qpHL745hLhR41WjWv_Yx51RlmfHapgvK0KSgM,7067
24
- uiprotect/data/websocket.py,sha256=lkdobRh5SPu7YzLHyhZVe7qlh5W3L8LKzS63Md-4DOk,6048
24
+ uiprotect/data/websocket.py,sha256=WZJVA7EfYuKYMv-9jmvGgMWXKzE9ES25SKv1NQ2eHjc,6281
25
25
  uiprotect/exceptions.py,sha256=kgn0cRM6lTtgLza09SDa3ZiX6ue1QqHCOogQ4qu6KTQ,965
26
26
  uiprotect/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  uiprotect/release_cache.json,sha256=NamnSFy78hOWY0DPO87J9ELFCAN6NnVquv8gQO75ZG4,386
28
28
  uiprotect/stream.py,sha256=McV3XymKyjn-1uV5jdQHcpaDjqLS4zWyMASQ8ubcyb4,4924
29
29
  uiprotect/test_util/__init__.py,sha256=d2g7afa0LSdixQ0kjEDYwafDFME_UlW2LzxpamZ2BC0,18556
30
30
  uiprotect/test_util/anonymize.py,sha256=f-8ijU-_y9r-uAbhIPn0f0I6hzJpAkvJzc8UpWihObI,8478
31
- uiprotect/utils.py,sha256=kXEr1xEoPAwUYuVPd6QoVnTf8MQwzQXQQ4JLyJsiRfY,18219
31
+ uiprotect/utils.py,sha256=ItYUrIhCbvdvIO3Q_MI9jQFnFs5OouqfsO_SXWeQZPM,18389
32
32
  uiprotect/websocket.py,sha256=iMTdchymaCgVHsmY1bRbxkcymqt6WQircIHYNxCu178,7289
33
- uiprotect-0.10.1.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
34
- uiprotect-0.10.1.dist-info/METADATA,sha256=XM-hvlQWsrZSuAVzlVtridjkbMY2Bh18YmSfrNhwiKc,10985
35
- uiprotect-0.10.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
36
- uiprotect-0.10.1.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
37
- uiprotect-0.10.1.dist-info/RECORD,,
33
+ uiprotect-0.12.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
34
+ uiprotect-0.12.0.dist-info/METADATA,sha256=13ADAWhiHnb4kCgVFsioRdR_pYLaGG4vDYQzdDfIcU0,10985
35
+ uiprotect-0.12.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
36
+ uiprotect-0.12.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
37
+ uiprotect-0.12.0.dist-info/RECORD,,