uiprotect 1.3.0__py3-none-any.whl → 1.4.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.

uiprotect/api.py CHANGED
@@ -1146,7 +1146,7 @@ class ProtectApiClient(BaseApiClient):
1146
1146
 
1147
1147
  async def get_devices_raw(self, model_type: ModelType) -> list[dict[str, Any]]:
1148
1148
  """Gets a raw device list given a model_type"""
1149
- return await self.api_request_list(f"{model_type.value}s")
1149
+ return await self.api_request_list(model_type.devices_key)
1150
1150
 
1151
1151
  async def get_devices(
1152
1152
  self,
@@ -1703,7 +1703,7 @@ class ProtectApiClient(BaseApiClient):
1703
1703
 
1704
1704
  async def adopt_device(self, model_type: ModelType, device_id: str) -> None:
1705
1705
  """Adopts a device"""
1706
- key = f"{model_type.value}s"
1706
+ key = model_type.devices_key
1707
1707
  data = await self.api_request_obj(
1708
1708
  "devices/adopt",
1709
1709
  method="post",
@@ -197,8 +197,8 @@ class Bootstrap(ProtectBaseObject):
197
197
  )
198
198
  data["macLookup"] = {}
199
199
  data["idLookup"] = {}
200
- for model_type in ModelType.bootstrap_models():
201
- key = f"{model_type}s"
200
+ for model_type in ModelType.bootstrap_models_types_set():
201
+ key = model_type.devices_key
202
202
  items: dict[str, ProtectModel] = {}
203
203
  for item in data[key]:
204
204
  if (
@@ -234,8 +234,8 @@ class Bootstrap(ProtectBaseObject):
234
234
  if "idLookup" in data:
235
235
  del data["idLookup"]
236
236
 
237
- for model_type in ModelType.bootstrap_models():
238
- attr = f"{model_type}s"
237
+ for model_type in ModelType.bootstrap_models_types_set():
238
+ attr = model_type.devices_key
239
239
  if attr in data and isinstance(data[attr], dict):
240
240
  data[attr] = list(data[attr].values())
241
241
 
@@ -302,7 +302,7 @@ class Bootstrap(ProtectBaseObject):
302
302
  if ref is None:
303
303
  return None
304
304
 
305
- devices: dict[str, ProtectModelWithId] = getattr(self, f"{ref.model.value}s")
305
+ devices: dict[str, ProtectModelWithId] = getattr(self, ref.model.devices_key)
306
306
  return cast(ProtectAdoptableDeviceModel, devices.get(ref.id))
307
307
 
308
308
  def get_device_from_id(self, device_id: str) -> ProtectAdoptableDeviceModel | None:
@@ -310,7 +310,7 @@ class Bootstrap(ProtectBaseObject):
310
310
  ref = self.id_lookup.get(device_id)
311
311
  if ref is None:
312
312
  return None
313
- devices: dict[str, ProtectModelWithId] = getattr(self, f"{ref.model.value}s")
313
+ devices: dict[str, ProtectModelWithId] = getattr(self, ref.model.devices_key)
314
314
  return cast(ProtectAdoptableDeviceModel, devices.get(ref.id))
315
315
 
316
316
  def process_event(self, event: Event) -> None:
@@ -344,21 +344,24 @@ class Bootstrap(ProtectBaseObject):
344
344
 
345
345
  def _process_add_packet(
346
346
  self,
347
+ model_type: ModelType,
347
348
  packet: WSPacket,
348
349
  data: dict[str, Any],
349
350
  ) -> WSSubscriptionMessage | None:
350
- obj = create_from_unifi_dict(data, api=self._api)
351
-
352
- if isinstance(obj, Event):
351
+ obj = create_from_unifi_dict(data, api=self._api, model_type=model_type)
352
+ if model_type is ModelType.EVENT:
353
+ if TYPE_CHECKING:
354
+ assert isinstance(obj, Event)
353
355
  self.process_event(obj)
354
- elif isinstance(obj, NVR):
356
+ if model_type is ModelType.NVR:
357
+ if TYPE_CHECKING:
358
+ assert isinstance(obj, NVR)
355
359
  self.nvr = obj
356
- elif (
357
- isinstance(obj, ProtectAdoptableDeviceModel)
358
- and obj.model is not None
359
- and obj.model.value in ModelType.bootstrap_models_set()
360
- ):
361
- key = f"{obj.model.value}s"
360
+ elif model_type in ModelType.bootstrap_models_types_set():
361
+ if TYPE_CHECKING:
362
+ assert isinstance(obj, ProtectAdoptableDeviceModel)
363
+ assert isinstance(obj.model, ModelType)
364
+ key = obj.model.devices_key
362
365
  if not self._api.ignore_unadopted or (
363
366
  obj.is_adopted and not obj.is_adopted_by_other
364
367
  ):
@@ -381,9 +384,12 @@ class Bootstrap(ProtectBaseObject):
381
384
  new_obj=obj,
382
385
  )
383
386
 
384
- def _process_remove_packet(self, packet: WSPacket) -> WSSubscriptionMessage | None:
385
- model: str | None = packet.action_frame.data.get("modelKey")
386
- devices: dict[str, ProtectDeviceModel] | None = getattr(self, f"{model}s", None)
387
+ def _process_remove_packet(
388
+ self, model_type: ModelType, packet: WSPacket
389
+ ) -> WSSubscriptionMessage | None:
390
+ devices: dict[str, ProtectDeviceModel] | None = getattr(
391
+ self, model_type.devices_key, None
392
+ )
387
393
 
388
394
  if devices is None:
389
395
  return None
@@ -443,12 +449,12 @@ class Bootstrap(ProtectBaseObject):
443
449
 
444
450
  def _process_device_update(
445
451
  self,
452
+ model_type: ModelType,
446
453
  packet: WSPacket,
447
454
  action: dict[str, Any],
448
455
  data: dict[str, Any],
449
456
  ignore_stats: bool,
450
457
  ) -> WSSubscriptionMessage | None:
451
- model_type = action["modelKey"]
452
458
  remove_keys = (
453
459
  STATS_AND_IGNORE_DEVICE_KEYS if ignore_stats else IGNORE_DEVICE_KEYS
454
460
  )
@@ -456,33 +462,31 @@ class Bootstrap(ProtectBaseObject):
456
462
  del data[key]
457
463
  # `last_motion` from cameras update every 100 milliseconds when a motion event is active
458
464
  # this overrides the behavior to only update `last_motion` when a new event starts
459
- if model_type == "camera" and "lastMotion" in data:
465
+ if model_type is ModelType.CAMERA and "lastMotion" in data:
460
466
  del data["lastMotion"]
461
467
  # nothing left to process
462
468
  if not data:
463
469
  self._create_stat(packet, None, True)
464
470
  return None
465
471
 
466
- key = f"{model_type}s"
467
- devices: dict[str, ProtectModelWithId] = getattr(self, key)
472
+ devices: dict[str, ProtectModelWithId] = getattr(self, model_type.devices_key)
468
473
  action_id: str = action["id"]
469
474
  if action_id not in devices:
470
475
  # ignore updates to events that phase out
471
- if model_type != _ModelType_Event_value:
476
+ if model_type is not ModelType.EVENT:
472
477
  _LOGGER.debug("Unexpected %s: %s", key, action_id)
473
478
  return None
474
479
 
475
480
  obj = devices[action_id]
476
- model = obj.model
477
481
  data = obj.unifi_dict_to_dict(data)
478
482
  old_obj = obj.copy()
479
483
  obj = obj.update_from_dict(deepcopy(data))
480
484
 
481
- if model is ModelType.EVENT:
485
+ if model_type is ModelType.EVENT:
482
486
  if TYPE_CHECKING:
483
487
  assert isinstance(obj, Event)
484
488
  self.process_event(obj)
485
- elif model is ModelType.CAMERA:
489
+ elif model_type is ModelType.CAMERA:
486
490
  if TYPE_CHECKING:
487
491
  assert isinstance(obj, Camera)
488
492
  if "last_ring" in data and obj.last_ring:
@@ -490,7 +494,7 @@ class Bootstrap(ProtectBaseObject):
490
494
  _LOGGER.debug("last_ring for %s (%s)", obj.id, is_recent)
491
495
  if is_recent:
492
496
  obj.set_ring_timeout()
493
- elif model is ModelType.SENSOR:
497
+ elif model_type is ModelType.SENSOR:
494
498
  if TYPE_CHECKING:
495
499
  assert isinstance(obj, Sensor)
496
500
  if "alarm_triggered_at" in data and obj.alarm_triggered_at:
@@ -532,13 +536,14 @@ class Bootstrap(ProtectBaseObject):
532
536
  self._create_stat(packet, None, True)
533
537
  return None
534
538
 
535
- if models and ModelType(model_key) not in models:
539
+ model_type = ModelType.from_string(model_key)
540
+ if models and model_type not in models:
536
541
  self._create_stat(packet, None, True)
537
542
  return None
538
543
 
539
544
  action_action: str = action["action"]
540
545
  if action_action == "remove":
541
- return self._process_remove_packet(packet)
546
+ return self._process_remove_packet(model_type, packet)
542
547
 
543
548
  if not data:
544
549
  self._create_stat(packet, None, True)
@@ -546,16 +551,13 @@ class Bootstrap(ProtectBaseObject):
546
551
 
547
552
  try:
548
553
  if action_action == "add":
549
- return self._process_add_packet(packet, data)
554
+ return self._process_add_packet(model_type, packet, data)
550
555
  if action_action == "update":
551
- if model_key == _ModelType_NVR_value:
556
+ if model_type is ModelType.NVR:
552
557
  return self._process_nvr_update(packet, data, ignore_stats)
553
-
554
- if (
555
- model_key in ModelType.bootstrap_models_set()
556
- or model_key == _ModelType_Event_value
557
- ):
558
+ if model_type in ModelType.bootstrap_models_types_and_event_set():
558
559
  return self._process_device_update(
560
+ model_type,
559
561
  packet,
560
562
  action,
561
563
  data,
@@ -576,7 +578,7 @@ class Bootstrap(ProtectBaseObject):
576
578
  msg = f"Validation error processing event: {action['id']}. Ignoring event."
577
579
  else:
578
580
  try:
579
- model_type = ModelType(action["modelKey"])
581
+ model_type = ModelType.from_string(action["modelKey"])
580
582
  device_id: str = action["id"]
581
583
  task = asyncio.create_task(self.refresh_device(model_type, device_id))
582
584
  self._refresh_tasks.add(task)
@@ -609,7 +611,7 @@ class Bootstrap(ProtectBaseObject):
609
611
  self.nvr = device
610
612
  else:
611
613
  devices: dict[str, ProtectModelWithId] = getattr(
612
- self, f"{model_type.value}s"
614
+ self, model_type.devices_key
613
615
  )
614
616
  devices[device.id] = device
615
617
  _LOGGER.debug("Successfully refresh model: %s %s", model_type, device_id)
uiprotect/data/convert.py CHANGED
@@ -63,6 +63,7 @@ def create_from_unifi_dict(
63
63
  data: dict[str, Any],
64
64
  api: ProtectApiClient | None = None,
65
65
  klass: type[ProtectModel] | None = None,
66
+ model_type: ModelType | None = None,
66
67
  ) -> ProtectModel:
67
68
  """
68
69
  Helper method to read the `modelKey` from a UFP JSON dict and convert to currect Python class.
@@ -71,6 +72,9 @@ def create_from_unifi_dict(
71
72
  if "modelKey" not in data:
72
73
  raise DataDecodeError("No modelKey")
73
74
 
75
+ if model_type is not None and klass is None:
76
+ klass = MODEL_TO_CLASS.get(model_type)
77
+
74
78
  if klass is None:
75
79
  klass = get_klass_from_dict(data)
76
80
 
uiprotect/data/types.py CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import enum
4
4
  from collections.abc import Callable, Coroutine
5
- from functools import cache
5
+ from functools import cache, cached_property
6
6
  from typing import Any, Literal, Optional, TypeVar, Union
7
7
 
8
8
  from packaging.version import Version as BaseVersion
@@ -109,31 +109,62 @@ class ModelType(str, UnknownValuesEnumMixin, enum.Enum):
109
109
  RECORDING_SCHEDULE = "recordingSchedule"
110
110
  UNKNOWN = "unknown"
111
111
 
112
+ @cached_property
113
+ def devices_key(self) -> str:
114
+ """Return the devices key."""
115
+ return f"{self.value}s"
116
+
117
+ @classmethod
118
+ @cache
119
+ def from_string(cls, value: str) -> ModelType:
120
+ return cls(value)
121
+
112
122
  @staticmethod
113
123
  @cache
114
- def bootstrap_models() -> tuple[str, ...]:
124
+ def bootstrap_model_types() -> tuple[ModelType, ...]:
125
+ """Return the bootstrap models as a tuple."""
115
126
  # TODO:
116
127
  # legacyUFV
117
128
  # display
118
-
119
129
  return (
120
- ModelType.CAMERA.value,
121
- ModelType.USER.value,
122
- ModelType.GROUP.value,
123
- ModelType.LIVEVIEW.value,
124
- ModelType.VIEWPORT.value,
125
- ModelType.LIGHT.value,
126
- ModelType.BRIDGE.value,
127
- ModelType.SENSOR.value,
128
- ModelType.DOORLOCK.value,
129
- ModelType.CHIME.value,
130
+ ModelType.CAMERA,
131
+ ModelType.USER,
132
+ ModelType.GROUP,
133
+ ModelType.LIVEVIEW,
134
+ ModelType.VIEWPORT,
135
+ ModelType.LIGHT,
136
+ ModelType.BRIDGE,
137
+ ModelType.SENSOR,
138
+ ModelType.DOORLOCK,
139
+ ModelType.CHIME,
140
+ )
141
+
142
+ @staticmethod
143
+ @cache
144
+ def bootstrap_models() -> tuple[str, ...]:
145
+ """Return the bootstrap models strings as a tuple."""
146
+ return tuple(
147
+ model_type.value for model_type in ModelType.bootstrap_model_types()
130
148
  )
131
149
 
132
150
  @staticmethod
133
151
  @cache
134
152
  def bootstrap_models_set() -> set[str]:
153
+ """Return the set of bootstrap models strings as a set."""
135
154
  return set(ModelType.bootstrap_models())
136
155
 
156
+ @staticmethod
157
+ @cache
158
+ def bootstrap_models_types_set() -> set[ModelType]:
159
+ """Return the set of bootstrap models as a set."""
160
+ return set(ModelType.bootstrap_model_types())
161
+
162
+ @staticmethod
163
+ @cache
164
+ def bootstrap_models_types_and_event_set() -> set[ModelType]:
165
+ """Return the set of bootstrap models and the event model as a set."""
166
+ return ModelType.bootstrap_models_types_set() | {ModelType.EVENT}
167
+
137
168
 
138
169
  @enum.unique
139
170
  class EventType(str, ValuesEnumMixin, enum.Enum):
uiprotect/data/user.py CHANGED
@@ -54,7 +54,7 @@ class Permission(ProtectBaseObject):
54
54
  if self.obj_ids == {"self"} or self.obj_ids is None:
55
55
  return None
56
56
 
57
- devices = getattr(self._api.bootstrap, f"{self.model.value}s")
57
+ devices = getattr(self._api.bootstrap, self.model.devices_key)
58
58
  return [devices[oid] for oid in self.obj_ids]
59
59
 
60
60
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: uiprotect
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: Python API for Unifi Protect (Unofficial)
5
5
  Home-page: https://github.com/uilibs/uiprotect
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  uiprotect/__init__.py,sha256=llnQNtiBfwQG8IkQXovvFz4LZeFjrJx7XdmmUhu3a9E,289
2
2
  uiprotect/__main__.py,sha256=C_bHCOkv5qj6WMy-6ELoY3Y6HDhLxOa1a30CzmbZhsg,462
3
- uiprotect/api.py,sha256=hN1PvNhOTFIfOmBtnUCfpujIXBEBYIKttaYnqlS_2es,66463
3
+ uiprotect/api.py,sha256=t3E1wMja0av6ENtvXJHGv70WC6NCCWbF_wFIJxOLS0I,66463
4
4
  uiprotect/cli/__init__.py,sha256=sSLW9keVQOkgFcMW18HTDjRrt9sJ0KWjn9DJDA6f9Pc,8658
5
5
  uiprotect/cli/backup.py,sha256=ZiS7RZnJGKI8TJKLW2cOUzkRM8nyTvE5Ov_jZZGtvSM,36708
6
6
  uiprotect/cli/base.py,sha256=zpTm2kyJe_GLixnv3Uadke__iRLh64AEwQzp-2hqS7g,7730
@@ -15,12 +15,12 @@ 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=kkPrRhvJV7igGyB3wv1fDvWH26Xzlbz94BjJkDiTvU4,38375
18
- uiprotect/data/bootstrap.py,sha256=D8qyR4fMp9TfVUIYbbn788hTKYxuzifdHax4qRYW1Gw,21185
19
- uiprotect/data/convert.py,sha256=rOQplUMIdTMD2SbAx_iI9BNPDscnhDvyRVLEMDhtADg,2047
18
+ uiprotect/data/bootstrap.py,sha256=INRUEbLWnsEJuzlcoFlm7RAT-WPx6odje3aD092QKGo,21465
19
+ uiprotect/data/convert.py,sha256=8h6Il_DhMkPRDPj9F_rA2UZIlTuchS3BQD24peKpk2A,2185
20
20
  uiprotect/data/devices.py,sha256=Nq3bOko5PFf5LvEBoD4JV8kmbq50laRdh3VHMWX7t-0,111809
21
21
  uiprotect/data/nvr.py,sha256=jwJEl4o2kbvoYFB9wSJPghNj0905847-86ElT51nNs0,47580
22
- uiprotect/data/types.py,sha256=6Z5ZqWTbH4Igy0l4QJShqQZ_zvrJKD0G-hZLjoBNP-U,16193
23
- uiprotect/data/user.py,sha256=Y7DnnxwUOJRJYw5FntztAtwN-o-Fs48SNS3mHi4Y5To,7071
22
+ uiprotect/data/types.py,sha256=1dj9jCk7btAa2hkiEo1Y5NoAFsQtOroCWT6KQKqG5eo,17213
23
+ uiprotect/data/user.py,sha256=Wb-ZWSwIJbyUbfVuENtUYbuW-uftHNDcoMH85dvEjkw,7071
24
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
@@ -30,8 +30,8 @@ uiprotect/test_util/__init__.py,sha256=d2g7afa0LSdixQ0kjEDYwafDFME_UlW2LzxpamZ2B
30
30
  uiprotect/test_util/anonymize.py,sha256=f-8ijU-_y9r-uAbhIPn0f0I6hzJpAkvJzc8UpWihObI,8478
31
31
  uiprotect/utils.py,sha256=6OLY8hNiCzk418PjJJIlFW7jjPzVt1vxBKEzBSqMeTk,18418
32
32
  uiprotect/websocket.py,sha256=IzDPyqbzrkOMREvahN-e2zdvVD0VABSCWy6jSoCwOT0,7299
33
- uiprotect-1.3.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
34
- uiprotect-1.3.0.dist-info/METADATA,sha256=uFhgPqZREr1J8zj0PfD7twZl98ueL46-biEodcR5jZk,10984
35
- uiprotect-1.3.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
36
- uiprotect-1.3.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
37
- uiprotect-1.3.0.dist-info/RECORD,,
33
+ uiprotect-1.4.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
34
+ uiprotect-1.4.0.dist-info/METADATA,sha256=BZe2l7Ns5304fpwkOl7u7HuJA8AjTg9D0ilGahzubEs,10984
35
+ uiprotect-1.4.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
36
+ uiprotect-1.4.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
37
+ uiprotect-1.4.0.dist-info/RECORD,,