uiprotect 1.0.1__py3-none-any.whl → 1.2.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/data/base.py CHANGED
@@ -141,26 +141,32 @@ class ProtectBaseObject(BaseModel):
141
141
  api: ProtectApiClient | None = values.pop("api", None)
142
142
  values_set = set(values)
143
143
 
144
- unifi_objs = cls._get_protect_objs()
145
- for key in cls._get_protect_objs_set().intersection(values_set):
146
- if isinstance(values[key], dict):
147
- values[key] = unifi_objs[key].construct(**values[key])
148
-
149
- unifi_lists = cls._get_protect_lists()
150
- for key in cls._get_protect_lists_set().intersection(values_set):
151
- if isinstance(values[key], list):
152
- values[key] = [
153
- unifi_lists[key].construct(**v) if isinstance(v, dict) else v
154
- for v in values[key]
155
- ]
156
-
157
- unifi_dicts = cls._get_protect_dicts()
158
- for key in cls._get_protect_dicts_set().intersection(values_set):
159
- if isinstance(values[key], dict):
160
- values[key] = {
161
- k: unifi_dicts[key].construct(**v) if isinstance(v, dict) else v
162
- for k, v in values[key].items()
163
- }
144
+ if (unifi_objs := cls._get_protect_objs()) and (
145
+ intersections := cls._get_protect_objs_set().intersection(values_set)
146
+ ):
147
+ for key in intersections:
148
+ if isinstance(values[key], dict):
149
+ values[key] = unifi_objs[key].construct(**values[key])
150
+
151
+ if (unifi_lists := cls._get_protect_lists()) and (
152
+ intersections := cls._get_protect_lists_set().intersection(values_set)
153
+ ):
154
+ for key in intersections:
155
+ if isinstance(values[key], list):
156
+ values[key] = [
157
+ unifi_lists[key].construct(**v) if isinstance(v, dict) else v
158
+ for v in values[key]
159
+ ]
160
+
161
+ if (unifi_dicts := cls._get_protect_dicts()) and (
162
+ intersections := cls._get_protect_dicts_set().intersection(values_set)
163
+ ):
164
+ for key in intersections:
165
+ if isinstance(values[key], dict):
166
+ values[key] = {
167
+ k: unifi_dicts[key].construct(**v) if isinstance(v, dict) else v
168
+ for k, v in values[key].items()
169
+ }
164
170
 
165
171
  obj = super().construct(_fields_set=_fields_set, **values)
166
172
  if api is not None:
@@ -336,11 +342,14 @@ class ProtectBaseObject(BaseModel):
336
342
  )
337
343
 
338
344
  # remap keys that will not be converted correctly by snake_case convert
339
- remaps = cls._get_unifi_remaps()
340
- for from_key in cls._get_unifi_remaps_set().intersection(data):
341
- data[remaps[from_key]] = data.pop(from_key)
345
+ if (remaps := cls._get_unifi_remaps()) and (
346
+ intersections := cls._get_unifi_remaps_set().intersection(data)
347
+ ):
348
+ for from_key in intersections:
349
+ data[remaps[from_key]] = data.pop(from_key)
342
350
 
343
351
  # convert to snake_case and remove extra fields
352
+ _fields = cls.__fields__
344
353
  for key in list(data):
345
354
  new_key = to_snake_case(key)
346
355
  data[new_key] = data.pop(key)
@@ -349,35 +358,41 @@ class ProtectBaseObject(BaseModel):
349
358
  if key == "api":
350
359
  continue
351
360
 
352
- if key not in cls.__fields__:
361
+ if key not in _fields:
353
362
  del data[key]
354
363
  continue
355
- data[key] = convert_unifi_data(data[key], cls.__fields__[key])
364
+ data[key] = convert_unifi_data(data[key], _fields[key])
356
365
 
357
366
  # clean child UFP objs
358
367
  data_set = set(data)
359
368
 
360
- unifi_objs = cls._get_protect_objs()
361
- for key in cls._get_protect_objs_set().intersection(data_set):
362
- data[key] = cls._clean_protect_obj(data[key], unifi_objs[key], api)
363
-
364
- unifi_lists = cls._get_protect_lists()
365
- for key in cls._get_protect_lists_set().intersection(data_set):
366
- if isinstance(data[key], list):
367
- data[key] = cls._clean_protect_obj_list(
368
- data[key],
369
- unifi_lists[key],
370
- api,
371
- )
369
+ if (unifi_objs := cls._get_protect_objs()) and (
370
+ intersections := cls._get_protect_objs_set().intersection(data_set)
371
+ ):
372
+ for key in intersections:
373
+ data[key] = cls._clean_protect_obj(data[key], unifi_objs[key], api)
372
374
 
373
- unifi_dicts = cls._get_protect_dicts()
374
- for key in cls._get_protect_dicts_set().intersection(data_set):
375
- if isinstance(data[key], dict):
376
- data[key] = cls._clean_protect_obj_dict(
377
- data[key],
378
- unifi_dicts[key],
379
- api,
380
- )
375
+ if (unifi_lists := cls._get_protect_lists()) and (
376
+ intersections := cls._get_protect_lists_set().intersection(data_set)
377
+ ):
378
+ for key in intersections:
379
+ if isinstance(data[key], list):
380
+ data[key] = cls._clean_protect_obj_list(
381
+ data[key],
382
+ unifi_lists[key],
383
+ api,
384
+ )
385
+
386
+ if (unifi_dicts := cls._get_protect_dicts()) and (
387
+ intersections := cls._get_protect_dicts_set().intersection(data_set)
388
+ ):
389
+ for key in intersections:
390
+ if isinstance(data[key], dict):
391
+ data[key] = cls._clean_protect_obj_dict(
392
+ data[key],
393
+ unifi_dicts[key],
394
+ api,
395
+ )
381
396
 
382
397
  return data
383
398
 
@@ -505,24 +520,34 @@ class ProtectBaseObject(BaseModel):
505
520
  data["api"] = api
506
521
  data_set = set(data)
507
522
 
508
- for key in self._get_protect_objs_set().intersection(data_set):
509
- unifi_obj: Any | None = getattr(self, key)
510
- if unifi_obj is not None and isinstance(unifi_obj, dict):
511
- unifi_obj["api"] = api
523
+ if (unifi_objs_sets := self._get_protect_objs_set()) and (
524
+ intersections := unifi_objs_sets.intersection(data_set)
525
+ ):
526
+ for key in intersections:
527
+ unifi_obj: Any | None = getattr(self, key)
528
+ if unifi_obj is not None and isinstance(unifi_obj, dict):
529
+ unifi_obj["api"] = api
512
530
 
513
- for key in self._get_protect_lists_set().intersection(data_set):
514
- new_items = []
515
- for item in data[key]:
516
- if isinstance(item, dict):
517
- item["api"] = api
518
- new_items.append(item)
519
- data[key] = new_items
520
-
521
- for key in self._get_protect_dicts_set().intersection(data_set):
522
- for item_key, item in data[key].items():
523
- if isinstance(item, dict):
524
- item["api"] = api
525
- data[key][item_key] = item
531
+ if (unifi_lists_sets := self._get_protect_lists_set()) and (
532
+ intersections := unifi_lists_sets.intersection(data_set)
533
+ ):
534
+ for key in intersections:
535
+ new_items = []
536
+ for item in data[key]:
537
+ if isinstance(item, dict):
538
+ item["api"] = api
539
+ new_items.append(item)
540
+ data[key] = new_items
541
+
542
+ if (unifi_dicts_sets := self._get_protect_dicts_set()) and (
543
+ intersections := unifi_dicts_sets.intersection(data_set)
544
+ ):
545
+ for key in intersections:
546
+ inner_dict: dict[str, Any] = data[key]
547
+ for item_key, item in inner_dict.items():
548
+ if isinstance(item, dict):
549
+ item["api"] = api
550
+ inner_dict[item_key] = item
526
551
 
527
552
  return data
528
553
 
@@ -22,6 +22,7 @@ from ..utils import normalize_mac, utc_now
22
22
  from .base import (
23
23
  RECENT_EVENT_MAX,
24
24
  ProtectBaseObject,
25
+ ProtectDeviceModel,
25
26
  ProtectModel,
26
27
  ProtectModelWithId,
27
28
  )
@@ -65,6 +66,7 @@ STATS_KEYS = {
65
66
  "recordingSchedules",
66
67
  }
67
68
  IGNORE_DEVICE_KEYS = {"nvrMac", "guid"}
69
+ STATS_AND_IGNORE_DEVICE_KEYS = STATS_KEYS | IGNORE_DEVICE_KEYS
68
70
 
69
71
  CAMERA_EVENT_ATTR_MAP: dict[EventType, tuple[str, str]] = {
70
72
  EventType.MOTION: ("last_motion", "last_motion_event_id"),
@@ -78,56 +80,36 @@ CAMERA_EVENT_ATTR_MAP: dict[EventType, tuple[str, str]] = {
78
80
  }
79
81
 
80
82
 
81
- def _remove_stats_keys(data: dict[str, Any]) -> None:
82
- for key in STATS_KEYS.intersection(data):
83
- del data[key]
84
-
85
-
86
83
  def _process_light_event(event: Event) -> None:
87
84
  if event.light is None:
88
85
  return
89
86
 
90
- dt = event.light.last_motion
91
- if dt is None or event.start >= dt or (event.end is not None and event.end >= dt):
87
+ if _event_is_in_range(event, event.light.last_motion):
92
88
  event.light.last_motion_event_id = event.id
93
89
 
94
90
 
91
+ def _event_is_in_range(event: Event, dt: datetime | None) -> bool:
92
+ """Check if event is in range of datetime."""
93
+ return (
94
+ dt is None or event.start >= dt or (event.end is not None and event.end >= dt)
95
+ )
96
+
97
+
95
98
  def _process_sensor_event(event: Event) -> None:
96
99
  if event.sensor is None:
97
100
  return
98
-
99
- if event.type == EventType.MOTION_SENSOR:
100
- dt = event.sensor.motion_detected_at
101
- if (
102
- dt is None
103
- or event.start >= dt
104
- or (event.end is not None and event.end >= dt)
105
- ):
101
+ if event.type is EventType.MOTION_SENSOR:
102
+ if _event_is_in_range(event, event.sensor.motion_detected_at):
106
103
  event.sensor.last_motion_event_id = event.id
107
104
  elif event.type in {EventType.SENSOR_CLOSED, EventType.SENSOR_OPENED}:
108
- dt = event.sensor.open_status_changed_at
109
- if (
110
- dt is None
111
- or event.start >= dt
112
- or (event.end is not None and event.end >= dt)
113
- ):
105
+ if _event_is_in_range(event, event.sensor.open_status_changed_at):
114
106
  event.sensor.last_contact_event_id = event.id
115
- elif event.type == EventType.SENSOR_EXTREME_VALUE:
116
- dt = event.sensor.extreme_value_detected_at
117
- if (
118
- dt is None
119
- or event.start >= dt
120
- or (event.end is not None and event.end >= dt)
121
- ):
107
+ elif event.type is EventType.SENSOR_EXTREME_VALUE:
108
+ if _event_is_in_range(event, event.sensor.extreme_value_detected_at):
122
109
  event.sensor.extreme_value_detected_at = event.end
123
110
  event.sensor.last_value_event_id = event.id
124
- elif event.type == EventType.SENSOR_ALARM:
125
- dt = event.sensor.alarm_triggered_at
126
- if (
127
- dt is None
128
- or event.start >= dt
129
- or (event.end is not None and event.end >= dt)
130
- ):
111
+ elif event.type is EventType.SENSOR_ALARM:
112
+ if _event_is_in_range(event, event.sensor.alarm_triggered_at):
131
113
  event.sensor.last_value_event_id = event.id
132
114
 
133
115
 
@@ -144,21 +126,25 @@ def _process_camera_event(event: Event) -> None:
144
126
 
145
127
  event_type = event.type
146
128
  dt_attr, event_attr = CAMERA_EVENT_ATTR_MAP[event_type]
147
- dt = getattr(camera, dt_attr)
148
- if dt is None or event.start >= dt or (event.end is not None and event.end >= dt):
149
- setattr(camera, event_attr, event.id)
150
- setattr(camera, dt_attr, event.start)
151
- if event_type in _CAMERA_SMART_AND_LINE_EVENTS:
152
- for smart_type in event.smart_detect_types:
153
- camera.last_smart_detect_event_ids[smart_type] = event.id
154
- camera.last_smart_detects[smart_type] = event.start
155
- elif event_type is _CAMERA_SMART_AUDIO_EVENT:
156
- for smart_type in event.smart_detect_types:
157
- audio_type = smart_type.audio_type
158
- if audio_type is None:
159
- continue
160
- camera.last_smart_audio_detect_event_ids[audio_type] = event.id
161
- camera.last_smart_audio_detects[audio_type] = event.start
129
+ dt: datetime | None = getattr(camera, dt_attr)
130
+ if not _event_is_in_range(event, dt):
131
+ return
132
+
133
+ event_id = event.id
134
+ event_start = event.start
135
+
136
+ setattr(camera, event_attr, event_id)
137
+ setattr(camera, dt_attr, event_start)
138
+ if event_type in _CAMERA_SMART_AND_LINE_EVENTS:
139
+ for smart_type in event.smart_detect_types:
140
+ camera.last_smart_detect_event_ids[smart_type] = event_id
141
+ camera.last_smart_detects[smart_type] = event_start
142
+ elif event_type is _CAMERA_SMART_AUDIO_EVENT:
143
+ for smart_type in event.smart_detect_types:
144
+ if (audio_type := smart_type.audio_type) is None:
145
+ continue
146
+ camera.last_smart_audio_detect_event_ids[audio_type] = event_id
147
+ camera.last_smart_audio_detects[audio_type] = event_start
162
148
 
163
149
 
164
150
  @dataclass
@@ -324,7 +310,7 @@ class Bootstrap(ProtectBaseObject):
324
310
  if ref is None:
325
311
  return None
326
312
 
327
- devices = getattr(self, f"{ref.model.value}s")
313
+ devices: dict[str, ProtectModelWithId] = getattr(self, f"{ref.model.value}s")
328
314
  return cast(ProtectAdoptableDeviceModel, devices.get(ref.id))
329
315
 
330
316
  def get_device_from_id(self, device_id: str) -> ProtectAdoptableDeviceModel | None:
@@ -332,7 +318,7 @@ class Bootstrap(ProtectBaseObject):
332
318
  ref = self.id_lookup.get(device_id)
333
319
  if ref is None:
334
320
  return None
335
- devices = getattr(self, f"{ref.model.value}s")
321
+ devices: dict[str, ProtectModelWithId] = getattr(self, f"{ref.model.value}s")
336
322
  return cast(ProtectAdoptableDeviceModel, devices.get(ref.id))
337
323
 
338
324
  def process_event(self, event: Event) -> None:
@@ -363,14 +349,6 @@ class Bootstrap(ProtectBaseObject):
363
349
  ),
364
350
  )
365
351
 
366
- def _get_frame_data(
367
- self,
368
- packet: WSPacket,
369
- ) -> tuple[dict[str, Any], dict[str, Any] | None]:
370
- if self.capture_ws_stats:
371
- return deepcopy(packet.action_frame.data), deepcopy(packet.data_frame.data)
372
- return packet.action_frame.data, packet.data_frame.data
373
-
374
352
  def _process_add_packet(
375
353
  self,
376
354
  packet: WSPacket,
@@ -412,7 +390,7 @@ class Bootstrap(ProtectBaseObject):
412
390
 
413
391
  def _process_remove_packet(self, packet: WSPacket) -> WSSubscriptionMessage | None:
414
392
  model: str | None = packet.action_frame.data.get("modelKey")
415
- devices = getattr(self, f"{model}s", None)
393
+ devices: dict[str, ProtectDeviceModel] | None = getattr(self, f"{model}s", None)
416
394
 
417
395
  if devices is None:
418
396
  return None
@@ -439,7 +417,8 @@ class Bootstrap(ProtectBaseObject):
439
417
  ignore_stats: bool,
440
418
  ) -> WSSubscriptionMessage | None:
441
419
  if ignore_stats:
442
- _remove_stats_keys(data)
420
+ for key in STATS_KEYS.intersection(data):
421
+ del data[key]
443
422
  # nothing left to process
444
423
  if not data:
445
424
  self._create_stat(packet, None, True)
@@ -477,9 +456,10 @@ class Bootstrap(ProtectBaseObject):
477
456
  ignore_stats: bool,
478
457
  ) -> WSSubscriptionMessage | None:
479
458
  model_type = action["modelKey"]
480
- if ignore_stats:
481
- _remove_stats_keys(data)
482
- for key in IGNORE_DEVICE_KEYS.intersection(data):
459
+ remove_keys = (
460
+ STATS_AND_IGNORE_DEVICE_KEYS if ignore_stats else IGNORE_DEVICE_KEYS
461
+ )
462
+ for key in remove_keys.intersection(data):
483
463
  del data[key]
484
464
  # `last_motion` from cameras update every 100 milliseconds when a motion event is active
485
465
  # this overrides the behavior to only update `last_motion` when a new event starts
@@ -491,12 +471,12 @@ class Bootstrap(ProtectBaseObject):
491
471
  return None
492
472
 
493
473
  key = f"{model_type}s"
494
- devices = getattr(self, key)
474
+ devices: dict[str, ProtectModelWithId] = getattr(self, key)
495
475
  action_id: str = action["id"]
496
476
  if action_id in devices:
497
477
  if action_id not in devices:
498
478
  raise ValueError(f"Unknown device update for {model_type}: {action_id}")
499
- obj: ProtectModelWithId = devices[action_id]
479
+ obj = devices[action_id]
500
480
  data = obj.unifi_dict_to_dict(data)
501
481
  old_obj = obj.copy()
502
482
  obj = obj.update_from_dict(deepcopy(data))
@@ -542,7 +522,12 @@ class Bootstrap(ProtectBaseObject):
542
522
  ignore_stats: bool = False,
543
523
  ) -> WSSubscriptionMessage | None:
544
524
  """Process a WS packet."""
545
- action, data = self._get_frame_data(packet)
525
+ action = packet.action_frame.data
526
+ data = packet.data_frame.data
527
+ if self.capture_ws_stats:
528
+ action = deepcopy(action)
529
+ data = deepcopy(data)
530
+
546
531
  new_update_id: str = action["newUpdateId"]
547
532
  if new_update_id is not None:
548
533
  self.last_update_id = new_update_id
@@ -629,7 +614,9 @@ class Bootstrap(ProtectBaseObject):
629
614
  if isinstance(device, NVR):
630
615
  self.nvr = device
631
616
  else:
632
- devices = getattr(self, f"{model_type.value}s")
617
+ devices: dict[str, ProtectModelWithId] = getattr(
618
+ self, f"{model_type.value}s"
619
+ )
633
620
  devices[device.id] = device
634
621
  _LOGGER.debug("Successfully refresh model: %s %s", model_type, device_id)
635
622
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: uiprotect
3
- Version: 1.0.1
3
+ Version: 1.2.0
4
4
  Summary: Python API for Unifi Protect (Unofficial)
5
5
  Home-page: https://github.com/uilibs/uiprotect
6
6
  License: MIT
@@ -14,8 +14,8 @@ uiprotect/cli/nvr.py,sha256=TwxEg2XT8jXAbOqv6gc7KFXELKadeItEDYweSL4_-e8,4260
14
14
  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
- uiprotect/data/base.py,sha256=p75RcPjVTmHEk6BVWApougBC2RBwUdFq8x_gL4zr8xc,37313
18
- uiprotect/data/bootstrap.py,sha256=lxijgYGOEhGzbgnBaJdmCJcmzEKIJnGbIBx_LQzNEMU,21547
17
+ uiprotect/data/base.py,sha256=kkPrRhvJV7igGyB3wv1fDvWH26Xzlbz94BjJkDiTvU4,38375
18
+ uiprotect/data/bootstrap.py,sha256=NH4jSEm44M0aijmViLHip2PKhVchuZJEeqWI3bcZwq0,21285
19
19
  uiprotect/data/convert.py,sha256=rOQplUMIdTMD2SbAx_iI9BNPDscnhDvyRVLEMDhtADg,2047
20
20
  uiprotect/data/devices.py,sha256=Nq3bOko5PFf5LvEBoD4JV8kmbq50laRdh3VHMWX7t-0,111809
21
21
  uiprotect/data/nvr.py,sha256=aIi4d0KCJ55r1EP-33sBhvEa8lx-41jDGrDpAq5VnOE,47598
@@ -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.0.1.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
34
- uiprotect-1.0.1.dist-info/METADATA,sha256=Fuv9_e3d1EdTq9JNQSCU2qTWAuBogazwWwdphvEHtZg,10984
35
- uiprotect-1.0.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
36
- uiprotect-1.0.1.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
37
- uiprotect-1.0.1.dist-info/RECORD,,
33
+ uiprotect-1.2.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
34
+ uiprotect-1.2.0.dist-info/METADATA,sha256=N0K6ocu5Xe5y1qvGrBIqu00xfu0ZseOmTMTXccG3cnQ,10984
35
+ uiprotect-1.2.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
36
+ uiprotect-1.2.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
37
+ uiprotect-1.2.0.dist-info/RECORD,,