uiprotect 1.5.0__tar.gz → 1.7.0__tar.gz

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.

Files changed (36) hide show
  1. {uiprotect-1.5.0 → uiprotect-1.7.0}/PKG-INFO +1 -1
  2. {uiprotect-1.5.0 → uiprotect-1.7.0}/pyproject.toml +1 -1
  3. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/data/base.py +67 -90
  4. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/data/nvr.py +19 -12
  5. {uiprotect-1.5.0 → uiprotect-1.7.0}/LICENSE +0 -0
  6. {uiprotect-1.5.0 → uiprotect-1.7.0}/README.md +0 -0
  7. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/__init__.py +0 -0
  8. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/__main__.py +0 -0
  9. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/api.py +0 -0
  10. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/cli/__init__.py +0 -0
  11. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/cli/backup.py +0 -0
  12. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/cli/base.py +0 -0
  13. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/cli/cameras.py +0 -0
  14. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/cli/chimes.py +0 -0
  15. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/cli/doorlocks.py +0 -0
  16. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/cli/events.py +0 -0
  17. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/cli/lights.py +0 -0
  18. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/cli/liveviews.py +0 -0
  19. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/cli/nvr.py +0 -0
  20. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/cli/sensors.py +0 -0
  21. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/cli/viewers.py +0 -0
  22. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/data/__init__.py +0 -0
  23. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/data/bootstrap.py +0 -0
  24. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/data/convert.py +0 -0
  25. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/data/devices.py +0 -0
  26. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/data/types.py +0 -0
  27. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/data/user.py +0 -0
  28. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/data/websocket.py +0 -0
  29. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/exceptions.py +0 -0
  30. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/py.typed +0 -0
  31. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/release_cache.json +0 -0
  32. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/stream.py +0 -0
  33. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/test_util/__init__.py +0 -0
  34. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/test_util/anonymize.py +0 -0
  35. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/utils.py +0 -0
  36. {uiprotect-1.5.0 → uiprotect-1.7.0}/src/uiprotect/websocket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: uiprotect
3
- Version: 1.5.0
3
+ Version: 1.7.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
  [tool.poetry]
2
2
  name = "uiprotect"
3
- version = "1.5.0"
3
+ version = "1.7.0"
4
4
  description = "Python API for Unifi Protect (Unofficial)"
5
5
  authors = ["UI Protect Maintainers <ui@koston.org>"]
6
6
  license = "MIT"
@@ -139,34 +139,25 @@ class ProtectBaseObject(BaseModel):
139
139
  @classmethod
140
140
  def construct(cls, _fields_set: set[str] | None = None, **values: Any) -> Self:
141
141
  api: ProtectApiClient | None = values.pop("api", None)
142
- values_set = set(values)
143
-
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
- }
142
+ unifi_objs = cls._get_protect_objs()
143
+ has_unifi_objs = bool(unifi_objs)
144
+ unifi_lists = cls._get_protect_lists()
145
+ has_unifi_lists = bool(unifi_lists)
146
+ unifi_dicts = cls._get_protect_dicts()
147
+ has_unifi_dicts = bool(unifi_dicts)
148
+ for key, value in values.items():
149
+ if has_unifi_objs and key in unifi_objs and isinstance(value, dict):
150
+ values[key] = unifi_objs[key].construct(**value)
151
+ elif has_unifi_lists and key in unifi_lists and isinstance(value, list):
152
+ values[key] = [
153
+ unifi_lists[key].construct(**v) if isinstance(v, dict) else v
154
+ for v in value
155
+ ]
156
+ elif has_unifi_dicts and key in unifi_dicts and isinstance(value, dict):
157
+ values[key] = {
158
+ k: unifi_dicts[key].construct(**v) if isinstance(v, dict) else v
159
+ for k, v in value.items()
160
+ }
170
161
 
171
162
  obj = super().construct(_fields_set=_fields_set, **values)
172
163
  if api is not None:
@@ -341,16 +332,16 @@ class ProtectBaseObject(BaseModel):
341
332
  cls._api if isinstance(cls, ProtectBaseObject) else None
342
333
  )
343
334
 
344
- # remap keys that will not be converted correctly by snake_case convert
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)
350
-
335
+ remaps = cls._get_unifi_remaps()
351
336
  # convert to snake_case and remove extra fields
352
337
  _fields = cls.__fields__
353
338
  for key in list(data):
339
+ if key in remaps:
340
+ # remap keys that will not be converted correctly by snake_case convert
341
+ remapped_key = remaps[key]
342
+ data[remapped_key] = data.pop(key)
343
+ key = remapped_key
344
+
354
345
  new_key = to_snake_case(key)
355
346
  data[new_key] = data.pop(key)
356
347
  key = new_key
@@ -363,36 +354,23 @@ class ProtectBaseObject(BaseModel):
363
354
  continue
364
355
  data[key] = convert_unifi_data(data[key], _fields[key])
365
356
 
366
- # clean child UFP objs
367
- data_set = set(data)
357
+ if not data:
358
+ return data
368
359
 
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)
374
-
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
- )
360
+ # clean child UFP objs
361
+ unifi_objs = cls._get_protect_objs()
362
+ has_unifi_objs = bool(unifi_objs)
363
+ unifi_lists = cls._get_protect_lists()
364
+ has_unifi_lists = bool(unifi_lists)
365
+ unifi_dicts = cls._get_protect_dicts()
366
+ has_unifi_dicts = bool(unifi_dicts)
367
+ for key, value in data.items():
368
+ if has_unifi_objs and key in unifi_objs:
369
+ data[key] = cls._clean_protect_obj(value, unifi_objs[key], api)
370
+ elif has_unifi_lists and key in unifi_lists and isinstance(value, list):
371
+ data[key] = cls._clean_protect_obj_list(value, unifi_lists[key], api)
372
+ elif has_unifi_dicts and key in unifi_dicts and isinstance(value, dict):
373
+ data[key] = cls._clean_protect_obj_dict(value, unifi_dicts[key], api)
396
374
 
397
375
  return data
398
376
 
@@ -518,36 +496,27 @@ class ProtectBaseObject(BaseModel):
518
496
  api: ProtectApiClient | None,
519
497
  ) -> dict[str, Any]:
520
498
  data["api"] = api
521
- data_set = set(data)
522
-
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
530
-
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]:
499
+ unifi_objs_sets = self._get_protect_objs_set()
500
+ has_unifi_objs = bool(unifi_objs_sets)
501
+ unifi_lists_sets = self._get_protect_lists_set()
502
+ has_unifi_lists = bool(unifi_lists_sets)
503
+ unifi_dicts_sets = self._get_protect_dicts_set()
504
+ has_unifi_dicts = bool(unifi_dicts_sets)
505
+ for key, value in data.items():
506
+ if has_unifi_objs and key in unifi_objs_sets and isinstance(value, dict):
507
+ value["api"] = api
508
+ elif (
509
+ has_unifi_lists and key in unifi_lists_sets and isinstance(value, list)
510
+ ):
511
+ for item in value:
537
512
  if isinstance(item, dict):
538
513
  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():
514
+ elif (
515
+ has_unifi_dicts and key in unifi_dicts_sets and isinstance(value, dict)
516
+ ):
517
+ for item in value.values():
548
518
  if isinstance(item, dict):
549
519
  item["api"] = api
550
- inner_dict[item_key] = item
551
520
 
552
521
  return data
553
522
 
@@ -776,6 +745,14 @@ class ProtectModelWithId(ProtectModel):
776
745
  revert_on_fail: bool = True,
777
746
  ) -> None:
778
747
  """Saves the current device changes to UFP."""
748
+ _LOGGER.debug(
749
+ "Saving device changes for %s (%s) data_before_changes=%s updated=%s",
750
+ self.id,
751
+ self.model,
752
+ data_before_changes,
753
+ updated,
754
+ )
755
+
779
756
  assert (
780
757
  self._update_lock.locked()
781
758
  ), "save_device_changes should only be called when the update lock is held"
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import logging
7
7
  import zoneinfo
8
+ from collections.abc import Callable
8
9
  from datetime import datetime, timedelta, tzinfo
9
10
  from functools import cache
10
11
  from ipaddress import IPv4Address, IPv6Address
@@ -1169,26 +1170,32 @@ class NVR(ProtectDeviceModel):
1169
1170
  if message in self.doorbell_settings.custom_messages:
1170
1171
  raise BadRequest("Custom doorbell message already exists")
1171
1172
 
1172
- async with self._update_lock:
1173
- await asyncio.sleep(
1174
- 0,
1175
- ) # yield to the event loop once we have the look to ensure websocket updates are processed
1176
- data_before_changes = self.dict_with_excludes()
1177
- self.doorbell_settings.custom_messages.append(DoorbellText(message))
1178
- await self.save_device(data_before_changes)
1179
- self.update_all_messages()
1173
+ await self._update_doorbell_messages(
1174
+ lambda: self.doorbell_settings.custom_messages.append(
1175
+ DoorbellText(message)
1176
+ ),
1177
+ )
1180
1178
 
1181
1179
  async def remove_custom_doorbell_message(self, message: str) -> None:
1182
1180
  """Removes custom doorbell message"""
1183
1181
  if message not in self.doorbell_settings.custom_messages:
1184
1182
  raise BadRequest("Custom doorbell message does not exists")
1185
1183
 
1184
+ await self._update_doorbell_messages(
1185
+ lambda: self.doorbell_settings.custom_messages.remove(
1186
+ DoorbellText(message)
1187
+ ),
1188
+ )
1189
+
1190
+ async def _update_doorbell_messages(
1191
+ self, update_callback: Callable[[], None]
1192
+ ) -> None:
1193
+ """Updates doorbell messages and saves to Protect."""
1186
1194
  async with self._update_lock:
1187
- await asyncio.sleep(
1188
- 0,
1189
- ) # yield to the event loop once we have the look to ensure websocket updates are processed
1195
+ # yield to the event loop once we have the lock to ensure websocket updates are processed
1196
+ await asyncio.sleep(0)
1190
1197
  data_before_changes = self.dict_with_excludes()
1191
- self.doorbell_settings.custom_messages.remove(DoorbellText(message))
1198
+ update_callback()
1192
1199
  await self.save_device(data_before_changes)
1193
1200
  self.update_all_messages()
1194
1201
 
File without changes
File without changes