uiprotect 1.18.0__py3-none-any.whl → 1.19.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.

@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import logging
7
- from collections.abc import Iterable
8
7
  from copy import deepcopy
9
8
  from dataclasses import dataclass
10
9
  from datetime import datetime
@@ -66,9 +65,34 @@ STATS_KEYS = {
66
65
  "lastSeen",
67
66
  "recordingSchedules",
68
67
  }
68
+
69
69
  IGNORE_DEVICE_KEYS = {"nvrMac", "guid"}
70
70
  STATS_AND_IGNORE_DEVICE_KEYS = STATS_KEYS | IGNORE_DEVICE_KEYS
71
71
 
72
+ _IGNORE_KEYS_BY_MODEL_TYPE = {
73
+ #
74
+ # `lastMotion` from cameras update every 100 milliseconds when a motion event is active
75
+ # this overrides the behavior to only update `lastMotion` when a new event starts
76
+ #
77
+ ModelType.CAMERA: {"lastMotion"},
78
+ #
79
+ # `cameraIds` is updated every 10s, but we don't need to process it since bootstrap
80
+ # is resynced every so often anyways.
81
+ #
82
+ ModelType.CHIME: {"cameraIds"},
83
+ }
84
+
85
+
86
+ IGNORE_DEVICE_KEYS_BY_MODEL_TYPE = {
87
+ model_type: IGNORE_DEVICE_KEYS | keys
88
+ for model_type, keys in _IGNORE_KEYS_BY_MODEL_TYPE.items()
89
+ }
90
+ STATS_AND_IGNORE_DEVICE_KEYS_BY_MODEL_TYPE = {
91
+ model_type: STATS_AND_IGNORE_DEVICE_KEYS | keys
92
+ for model_type, keys in _IGNORE_KEYS_BY_MODEL_TYPE.items()
93
+ }
94
+
95
+
72
96
  CAMERA_EVENT_ATTR_MAP: dict[EventType, tuple[str, str]] = {
73
97
  EventType.MOTION: ("last_motion", "last_motion_event_id"),
74
98
  EventType.SMART_DETECT: ("last_smart_detect", "last_smart_detect_event_id"),
@@ -318,28 +342,9 @@ class Bootstrap(ProtectBaseObject):
318
342
 
319
343
  self.events[event.id] = event
320
344
 
321
- def _create_stat(
322
- self,
323
- packet: WSPacket,
324
- keys_set: Iterable[str] | None,
325
- filtered: bool,
326
- ) -> None:
327
- if self.capture_ws_stats:
328
- self._ws_stats.append(
329
- WSStat(
330
- model=packet.action_frame.data["modelKey"],
331
- action=packet.action_frame.data["action"],
332
- keys=list(packet.data_frame.data),
333
- keys_set=[] if keys_set is None else list(keys_set),
334
- size=len(packet.raw),
335
- filtered=filtered,
336
- ),
337
- )
338
-
339
345
  def _process_add_packet(
340
346
  self,
341
347
  model_type: ModelType,
342
- packet: WSPacket,
343
348
  data: dict[str, Any],
344
349
  ) -> WSSubscriptionMessage | None:
345
350
  obj = create_from_unifi_dict(data, api=self._api, model_type=model_type)
@@ -366,33 +371,26 @@ class Bootstrap(ProtectBaseObject):
366
371
  _LOGGER.debug("Unexpected bootstrap model type for add: %s", model_type)
367
372
  return None
368
373
 
369
- updated = obj.dict()
370
-
371
- self._create_stat(packet, updated, False)
372
-
373
374
  return WSSubscriptionMessage(
374
375
  action=WSAction.ADD,
375
376
  new_update_id=self.last_update_id,
376
- changed_data=updated,
377
+ changed_data=obj.dict(),
377
378
  new_obj=obj,
378
379
  )
379
380
 
380
381
  def _process_remove_packet(
381
- self, model_type: ModelType, packet: WSPacket
382
+ self, model_type: ModelType, action: dict[str, Any]
382
383
  ) -> WSSubscriptionMessage | None:
383
384
  devices_key = model_type.devices_key
384
385
  devices: dict[str, ProtectDeviceModel] | None = getattr(self, devices_key, None)
385
-
386
386
  if devices is None:
387
387
  return None
388
388
 
389
- device_id: str = packet.action_frame.data["id"]
389
+ device_id: str = action["id"]
390
390
  self.id_lookup.pop(device_id, None)
391
391
  if (device := devices.pop(device_id, None)) is None:
392
392
  return None
393
393
  self.mac_lookup.pop(normalize_mac(device.mac), None)
394
-
395
- self._create_stat(packet, None, False)
396
394
  return WSSubscriptionMessage(
397
395
  action=WSAction.REMOVE,
398
396
  new_update_id=self.last_update_id,
@@ -402,7 +400,7 @@ class Bootstrap(ProtectBaseObject):
402
400
 
403
401
  def _process_nvr_update(
404
402
  self,
405
- packet: WSPacket,
403
+ action: dict[str, Any],
406
404
  data: dict[str, Any],
407
405
  ignore_stats: bool,
408
406
  ) -> WSSubscriptionMessage | None:
@@ -411,24 +409,20 @@ class Bootstrap(ProtectBaseObject):
411
409
  del data[key]
412
410
  # nothing left to process
413
411
  if not data:
414
- self._create_stat(packet, None, True)
415
412
  return None
416
413
 
417
414
  # for another NVR in stack
418
- nvr_id: str | None = packet.action_frame.data.get("id")
415
+ nvr_id: str | None = action.get("id")
419
416
  if nvr_id and nvr_id != self.nvr.id:
420
- self._create_stat(packet, None, True)
421
417
  return None
422
418
 
423
419
  # nothing left to process
424
420
  if not (data := self.nvr.unifi_dict_to_dict(data)):
425
- self._create_stat(packet, None, True)
426
421
  return None
427
422
 
428
423
  old_nvr = self.nvr.copy()
429
424
  self.nvr = self.nvr.update_from_dict(deepcopy(data))
430
425
 
431
- self._create_stat(packet, data, False)
432
426
  return WSSubscriptionMessage(
433
427
  action=WSAction.UPDATE,
434
428
  new_update_id=self.last_update_id,
@@ -440,7 +434,6 @@ class Bootstrap(ProtectBaseObject):
440
434
  def _process_device_update(
441
435
  self,
442
436
  model_type: ModelType,
443
- packet: WSPacket,
444
437
  action: dict[str, Any],
445
438
  data: dict[str, Any],
446
439
  ignore_stats: bool,
@@ -453,18 +446,20 @@ class Bootstrap(ProtectBaseObject):
453
446
  that was generated internally as a result of an event
454
447
  that will expire and result in a state change.
455
448
  """
456
- remove_keys = (
457
- STATS_AND_IGNORE_DEVICE_KEYS if ignore_stats else IGNORE_DEVICE_KEYS
458
- )
449
+ if ignore_stats:
450
+ remove_keys = STATS_AND_IGNORE_DEVICE_KEYS_BY_MODEL_TYPE.get(
451
+ model_type, STATS_AND_IGNORE_DEVICE_KEYS
452
+ )
453
+ else:
454
+ remove_keys = IGNORE_DEVICE_KEYS_BY_MODEL_TYPE.get(
455
+ model_type, IGNORE_DEVICE_KEYS
456
+ )
457
+
459
458
  for key in remove_keys.intersection(data):
460
459
  del data[key]
461
- # `last_motion` from cameras update every 100 milliseconds when a motion event is active
462
- # this overrides the behavior to only update `last_motion` when a new event starts
463
- if model_type is ModelType.CAMERA and "lastMotion" in data:
464
- del data["lastMotion"]
460
+
465
461
  # nothing left to process
466
462
  if not data and not is_ping_back:
467
- self._create_stat(packet, None, True)
468
463
  return None
469
464
 
470
465
  devices: dict[str, ProtectModelWithId] = getattr(self, model_type.devices_key)
@@ -480,7 +475,6 @@ class Bootstrap(ProtectBaseObject):
480
475
 
481
476
  if not data and not is_ping_back:
482
477
  # nothing left to process
483
- self._create_stat(packet, None, True)
484
478
  return None
485
479
 
486
480
  old_obj = obj.copy()
@@ -506,7 +500,6 @@ class Bootstrap(ProtectBaseObject):
506
500
  _LOGGER.debug("alarm_triggered_at for %s (%s)", obj.id, is_recent)
507
501
 
508
502
  devices[action_id] = obj
509
- self._create_stat(packet, data, False)
510
503
  return WSSubscriptionMessage(
511
504
  action=WSAction.UPDATE,
512
505
  new_update_id=self.last_update_id,
@@ -523,9 +516,10 @@ class Bootstrap(ProtectBaseObject):
523
516
  is_ping_back: bool = False,
524
517
  ) -> WSSubscriptionMessage | None:
525
518
  """Process a WS packet."""
519
+ capture_ws_stats = self.capture_ws_stats
526
520
  action = packet.action_frame.data
527
521
  data = packet.data_frame.data
528
- if self.capture_ws_stats:
522
+ if capture_ws_stats:
529
523
  action = deepcopy(action)
530
524
  data = deepcopy(data)
531
525
 
@@ -533,33 +527,57 @@ class Bootstrap(ProtectBaseObject):
533
527
  if new_update_id is not None:
534
528
  self.last_update_id = new_update_id
535
529
 
530
+ message = self._make_ws_packet_message(
531
+ action, data, models, ignore_stats, is_ping_back
532
+ )
533
+
534
+ if capture_ws_stats:
535
+ self._ws_stats.append(
536
+ WSStat(
537
+ model=packet.action_frame.data["modelKey"],
538
+ action=packet.action_frame.data["action"],
539
+ keys=list(packet.data_frame.data),
540
+ keys_set=[] if message is None else list(message.changed_data),
541
+ size=len(packet.raw),
542
+ filtered=message is None,
543
+ ),
544
+ )
545
+
546
+ return message
547
+
548
+ def _make_ws_packet_message(
549
+ self,
550
+ action: dict[str, Any],
551
+ data: dict[str, Any],
552
+ models: set[ModelType] | None,
553
+ ignore_stats: bool,
554
+ is_ping_back: bool,
555
+ ) -> WSSubscriptionMessage | None:
556
+ """Process a WS packet."""
536
557
  model_key: str = action["modelKey"]
537
558
  if (model_type := ModelType.from_string(model_key)) is ModelType.UNKNOWN:
538
559
  _LOGGER.debug("Unknown model type: %s", model_key)
539
- self._create_stat(packet, None, True)
540
560
  return None
541
561
 
542
562
  if models and model_type not in models:
543
- self._create_stat(packet, None, True)
544
563
  return None
545
564
 
546
565
  action_action: str = action["action"]
547
566
  if action_action == "remove":
548
- return self._process_remove_packet(model_type, packet)
567
+ return self._process_remove_packet(model_type, action)
549
568
 
550
569
  if not data and not is_ping_back:
551
- self._create_stat(packet, None, True)
552
570
  return None
553
571
 
554
572
  try:
555
573
  if action_action == "add":
556
- return self._process_add_packet(model_type, packet, data)
574
+ return self._process_add_packet(model_type, data)
557
575
  if action_action == "update":
558
576
  if model_type is ModelType.NVR:
559
- return self._process_nvr_update(packet, data, ignore_stats)
577
+ return self._process_nvr_update(action, data, ignore_stats)
560
578
  if model_type in ModelType.bootstrap_models_types_and_event_set:
561
579
  return self._process_device_update(
562
- model_type, packet, action, data, ignore_stats, is_ping_back
580
+ model_type, action, data, ignore_stats, is_ping_back
563
581
  )
564
582
  except (ValidationError, ValueError) as err:
565
583
  self._handle_ws_error(action, err)
@@ -567,7 +585,6 @@ class Bootstrap(ProtectBaseObject):
567
585
  _LOGGER.debug(
568
586
  "Unexpected bootstrap model type deviceadoptedfor update: %s", model_key
569
587
  )
570
- self._create_stat(packet, None, True)
571
588
  return None
572
589
 
573
590
  def _handle_ws_error(self, action: dict[str, Any], err: Exception) -> None:
uiprotect/data/devices.py CHANGED
@@ -474,7 +474,9 @@ class SmartDetectSettings(ProtectBaseObject):
474
474
 
475
475
  @classmethod
476
476
  def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
477
- for key in ("objectTypes", "audioTypes", "autoTrackingObjectTypes"):
477
+ if "audioTypes" in data:
478
+ data["audioTypes"] = convert_smart_audio_types(data["audioTypes"])
479
+ for key in ("objectTypes", "autoTrackingObjectTypes"):
478
480
  if key in data:
479
481
  data[key] = convert_smart_types(data[key])
480
482
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: uiprotect
3
- Version: 1.18.0
3
+ Version: 1.19.0
4
4
  Summary: Python API for Unifi Protect (Unofficial)
5
5
  Home-page: https://github.com/uilibs/uiprotect
6
6
  License: MIT
@@ -15,9 +15,9 @@ 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=q5lpNJ4keAQ4oDjalwg9eL3kaVzEapfwtcTzZZlrN_M,35691
18
- uiprotect/data/bootstrap.py,sha256=epMt3lRNCp4mu_gkyjV12Ec62snB1FUOP_obVIsWp-w,21723
18
+ uiprotect/data/bootstrap.py,sha256=ZieXqE0526F3iRbdJ1pRM6QclSatn_SmecfoOPS3nNg,21922
19
19
  uiprotect/data/convert.py,sha256=8h6Il_DhMkPRDPj9F_rA2UZIlTuchS3BQD24peKpk2A,2185
20
- uiprotect/data/devices.py,sha256=c32wLGkjP-9IiWSlofKqesMVhsyAtrKwsdzwDsncQS0,110387
20
+ uiprotect/data/devices.py,sha256=450T-8qXsFad0xNNnXwm3x53e09NT4dgqlefl7lrc2Q,110485
21
21
  uiprotect/data/nvr.py,sha256=zCEAI-rKLEpp9P63QDvJi0hGRsuv-PWGssgHw1POYeQ,47648
22
22
  uiprotect/data/types.py,sha256=8z8jIoMlfDre7Op1JAN45PLZdAE-4i3gDPSo2uymqu4,17996
23
23
  uiprotect/data/user.py,sha256=Wb-ZWSwIJbyUbfVuENtUYbuW-uftHNDcoMH85dvEjkw,7071
@@ -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=kVRJwvHP683Sjhi2pnxwCwbaRl_uMQ2qFYSvt9kpfoU,18426
32
32
  uiprotect/websocket.py,sha256=JHI_2EZeRPqPyQopsBZS0dr3tu0HaTiqeLazfBXhW_8,7339
33
- uiprotect-1.18.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
34
- uiprotect-1.18.0.dist-info/METADATA,sha256=-XSKOa6e4RDLO-07RvVVy26J7ujyabN9FhC0UqneUa0,10985
35
- uiprotect-1.18.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
36
- uiprotect-1.18.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
37
- uiprotect-1.18.0.dist-info/RECORD,,
33
+ uiprotect-1.19.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
34
+ uiprotect-1.19.0.dist-info/METADATA,sha256=L8ov7HOBW9HUayl354kr_XlTo60zmbCvG0ZsMAAm98M,10985
35
+ uiprotect-1.19.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
36
+ uiprotect-1.19.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
37
+ uiprotect-1.19.0.dist-info/RECORD,,