uiprotect 1.0.0__py3-none-any.whl → 1.1.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
@@ -65,6 +65,13 @@ from .utils import (
65
65
  )
66
66
  from .websocket import Websocket
67
67
 
68
+ if sys.version_info[:2] < (3, 13):
69
+ from http import cookies
70
+
71
+ # See: https://github.com/python/cpython/issues/112713
72
+ cookies.Morsel._reserved["partitioned"] = "partitioned" # type: ignore[attr-defined]
73
+ cookies.Morsel._flags.add("partitioned") # type: ignore[attr-defined]
74
+
68
75
  TOKEN_COOKIE_MAX_EXP_SECONDS = 60
69
76
 
70
77
  NEVER_RAN = -1000
@@ -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.0
3
+ Version: 1.1.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=EgqupTzTKsbSNck-h6vYpptibYtAHhFcxG6WviX5R8w,66173
3
+ uiprotect/api.py,sha256=hN1PvNhOTFIfOmBtnUCfpujIXBEBYIKttaYnqlS_2es,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,7 +15,7 @@ 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=p75RcPjVTmHEk6BVWApougBC2RBwUdFq8x_gL4zr8xc,37313
18
- uiprotect/data/bootstrap.py,sha256=lxijgYGOEhGzbgnBaJdmCJcmzEKIJnGbIBx_LQzNEMU,21547
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.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
34
- uiprotect-1.0.0.dist-info/METADATA,sha256=TkJEgMl3hpJViK8WveWGO2O2OvPbUihSdiCEecfOcWc,10984
35
- uiprotect-1.0.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
36
- uiprotect-1.0.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
37
- uiprotect-1.0.0.dist-info/RECORD,,
33
+ uiprotect-1.1.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
34
+ uiprotect-1.1.0.dist-info/METADATA,sha256=--s4fFpQ0KbuDx6VYuPnkko1w4_f2hRpGR-IJOvZrt4,10984
35
+ uiprotect-1.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
36
+ uiprotect-1.1.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
37
+ uiprotect-1.1.0.dist-info/RECORD,,