uiprotect 3.3.1__tar.gz → 3.4.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-3.3.1 → uiprotect-3.4.0}/PKG-INFO +1 -1
  2. {uiprotect-3.3.1 → uiprotect-3.4.0}/pyproject.toml +1 -1
  3. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/data/base.py +33 -22
  4. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/data/devices.py +74 -74
  5. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/data/nvr.py +45 -62
  6. {uiprotect-3.3.1 → uiprotect-3.4.0}/LICENSE +0 -0
  7. {uiprotect-3.3.1 → uiprotect-3.4.0}/README.md +0 -0
  8. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/__init__.py +0 -0
  9. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/__main__.py +0 -0
  10. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/api.py +0 -0
  11. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/cli/__init__.py +0 -0
  12. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/cli/backup.py +0 -0
  13. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/cli/base.py +0 -0
  14. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/cli/cameras.py +0 -0
  15. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/cli/chimes.py +0 -0
  16. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/cli/doorlocks.py +0 -0
  17. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/cli/events.py +0 -0
  18. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/cli/lights.py +0 -0
  19. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/cli/liveviews.py +0 -0
  20. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/cli/nvr.py +0 -0
  21. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/cli/sensors.py +0 -0
  22. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/cli/viewers.py +0 -0
  23. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/data/__init__.py +0 -0
  24. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/data/bootstrap.py +0 -0
  25. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/data/convert.py +0 -0
  26. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/data/types.py +0 -0
  27. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/data/user.py +0 -0
  28. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/data/websocket.py +0 -0
  29. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/exceptions.py +0 -0
  30. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/py.typed +0 -0
  31. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/release_cache.json +0 -0
  32. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/stream.py +0 -0
  33. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/test_util/__init__.py +0 -0
  34. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/test_util/anonymize.py +0 -0
  35. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/utils.py +0 -0
  36. {uiprotect-3.3.1 → uiprotect-3.4.0}/src/uiprotect/websocket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: uiprotect
3
- Version: 3.3.1
3
+ Version: 3.4.0
4
4
  Summary: Python API for Unifi Protect (Unofficial)
5
5
  Home-page: https://github.com/uilibs/uiprotect
6
6
  Author: UI Protect Maintainers
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "uiprotect"
3
- version = "3.3.1"
3
+ version = "3.4.0"
4
4
  description = "Python API for Unifi Protect (Unofficial)"
5
5
  authors = ["UI Protect Maintainers <ui@koston.org>"]
6
6
  readme = "README.md"
@@ -275,6 +275,19 @@ class ProtectBaseObject(BaseModel):
275
275
  items[key] = cls._clean_protect_obj(value, klass, api)
276
276
  return items
277
277
 
278
+ @classmethod
279
+ @cache
280
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
281
+ """
282
+ Helper method for overriding in child classes for converting UFP JSON data to Python data types.
283
+
284
+ Return format is
285
+ {
286
+ "ufpJsonName": Callable[[Any], Any]
287
+ }
288
+ """
289
+ return {}
290
+
278
291
  @classmethod
279
292
  def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
280
293
  """
@@ -295,6 +308,11 @@ class ProtectBaseObject(BaseModel):
295
308
  cls._api if isinstance(cls, ProtectBaseObject) else None
296
309
  )
297
310
 
311
+ conversions = cls.unifi_dict_conversions()
312
+ for key, convert in conversions.items():
313
+ if (val := data.get(key)) is not None:
314
+ data[key] = convert(val) # type: ignore[operator]
315
+
298
316
  remaps = cls._get_unifi_remaps()
299
317
  # convert to snake_case and remove extra fields
300
318
  _fields = cls.__fields__
@@ -810,23 +828,16 @@ class ProtectDeviceModel(ProtectModelWithId):
810
828
  }
811
829
 
812
830
  @classmethod
813
- def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
814
- if "lastSeen" in data:
815
- data["lastSeen"] = convert_to_datetime(data["lastSeen"])
816
- if "upSince" in data and data["upSince"] is not None:
817
- data["upSince"] = convert_to_datetime(data["upSince"])
818
- if (
819
- "uptime" in data
820
- and data["uptime"] is not None
821
- and not isinstance(data["uptime"], timedelta)
822
- ):
823
- data["uptime"] = timedelta(milliseconds=int(data["uptime"]))
824
- # hardware revisions for all devices are not simple numbers
825
- # so cast them all to str to be consistent
826
- if "hardwareRevision" in data and data["hardwareRevision"] is not None:
827
- data["hardwareRevision"] = str(data["hardwareRevision"])
828
-
829
- return super().unifi_dict_to_dict(data)
831
+ @cache
832
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
833
+ return {
834
+ "upSince": convert_to_datetime,
835
+ "uptime": lambda x: timedelta(milliseconds=int(x)),
836
+ "lastSeen": convert_to_datetime,
837
+ # hardware revisions for all devices are not simple numbers
838
+ # so cast them all to str to be consistent
839
+ "hardwareRevision": str,
840
+ } | super().unifi_dict_conversions()
830
841
 
831
842
  def _event_callback_ping(self) -> None:
832
843
  _LOGGER.debug("Event ping timer started for %s", self.id)
@@ -958,11 +969,11 @@ class ProtectAdoptableDeviceModel(ProtectDeviceModel):
958
969
  return data
959
970
 
960
971
  @classmethod
961
- def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
962
- if "lastDisconnect" in data and data["lastDisconnect"] is not None:
963
- data["lastDisconnect"] = convert_to_datetime(data["lastDisconnect"])
964
-
965
- return super().unifi_dict_to_dict(data)
972
+ @cache
973
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
974
+ return {
975
+ "lastDisconnect": convert_to_datetime,
976
+ } | super().unifi_dict_conversions()
966
977
 
967
978
  @property
968
979
  def display_name(self) -> str:
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import logging
7
7
  import warnings
8
- from collections.abc import Iterable
8
+ from collections.abc import Callable
9
9
  from datetime import datetime, timedelta
10
10
  from functools import cache
11
11
  from ipaddress import IPv4Address
@@ -107,11 +107,11 @@ class LightDeviceSettings(ProtectBaseObject):
107
107
  pir_sensitivity: PercentInt
108
108
 
109
109
  @classmethod
110
- def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
111
- if "pirDuration" in data and not isinstance(data["pirDuration"], timedelta):
112
- data["pirDuration"] = timedelta(milliseconds=data["pirDuration"])
113
-
114
- return super().unifi_dict_to_dict(data)
110
+ @cache
111
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
112
+ return {
113
+ "pirDuration": lambda x: timedelta(milliseconds=x)
114
+ } | super().unifi_dict_conversions()
115
115
 
116
116
 
117
117
  class LightOnSettings(ProtectBaseObject):
@@ -400,6 +400,14 @@ class RecordingSettings(ProtectBaseObject):
400
400
  "retentionDurationMs": "retentionDuration",
401
401
  }
402
402
 
403
+ @classmethod
404
+ @cache
405
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
406
+ return {
407
+ "minMotionEventTrigger": lambda x: timedelta(seconds=x),
408
+ "endMotionEventDelay": lambda x: timedelta(seconds=x),
409
+ } | super().unifi_dict_conversions()
410
+
403
411
  @classmethod
404
412
  def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
405
413
  if "prePaddingSecs" in data:
@@ -414,18 +422,6 @@ class RecordingSettings(ProtectBaseObject):
414
422
  data["smartDetectPostPadding"] = timedelta(
415
423
  seconds=data.pop("smartDetectPostPaddingSecs"),
416
424
  )
417
- if "minMotionEventTrigger" in data and not isinstance(
418
- data["minMotionEventTrigger"],
419
- timedelta,
420
- ):
421
- data["minMotionEventTrigger"] = timedelta(
422
- seconds=data["minMotionEventTrigger"],
423
- )
424
- if "endMotionEventDelay" in data and not isinstance(
425
- data["endMotionEventDelay"],
426
- timedelta,
427
- ):
428
- data["endMotionEventDelay"] = timedelta(seconds=data["endMotionEventDelay"])
429
425
 
430
426
  return super().unifi_dict_to_dict(data)
431
427
 
@@ -469,14 +465,13 @@ class SmartDetectSettings(ProtectBaseObject):
469
465
  auto_tracking_object_types: list[SmartDetectObjectType] | None = None
470
466
 
471
467
  @classmethod
472
- def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
473
- if "audioTypes" in data:
474
- data["audioTypes"] = convert_smart_audio_types(data["audioTypes"])
475
- for key in ("objectTypes", "autoTrackingObjectTypes"):
476
- if key in data:
477
- data[key] = convert_smart_types(data[key])
478
-
479
- return super().unifi_dict_to_dict(data)
468
+ @cache
469
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
470
+ return {
471
+ "audioTypes": convert_smart_audio_types,
472
+ "objectTypes": convert_smart_types,
473
+ "autoTrackingObjectTypes": convert_smart_types,
474
+ } | super().unifi_dict_conversions()
480
475
 
481
476
 
482
477
  class LCDMessage(ProtectBaseObject):
@@ -484,10 +479,15 @@ class LCDMessage(ProtectBaseObject):
484
479
  text: str
485
480
  reset_at: datetime | None = None
486
481
 
482
+ @classmethod
483
+ @cache
484
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
485
+ return {
486
+ "resetAt": convert_to_datetime,
487
+ } | super().unifi_dict_conversions()
488
+
487
489
  @classmethod
488
490
  def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
489
- if "resetAt" in data:
490
- data["resetAt"] = convert_to_datetime(data["resetAt"])
491
491
  if "text" in data:
492
492
  # UniFi Protect bug: some times LCD messages can get into a bad state where message = DEFAULT MESSAGE, but no type
493
493
  if "type" not in data:
@@ -570,21 +570,21 @@ class VideoStats(ProtectBaseObject):
570
570
  }
571
571
 
572
572
  @classmethod
573
- def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
574
- for key in (
575
- "recordingStart",
576
- "recordingEnd",
577
- "recordingStartLQ",
578
- "recordingEndLQ",
579
- "timelapseStart",
580
- "timelapseEnd",
581
- "timelapseStartLQ",
582
- "timelapseEndLQ",
583
- ):
584
- if key in data:
585
- data[key] = convert_to_datetime(data[key])
586
-
587
- return super().unifi_dict_to_dict(data)
573
+ @cache
574
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
575
+ return {
576
+ key: convert_to_datetime
577
+ for key in (
578
+ "recordingStart",
579
+ "recordingEnd",
580
+ "recordingStartLQ",
581
+ "recordingEndLQ",
582
+ "timelapseStart",
583
+ "timelapseEnd",
584
+ "timelapseStartLQ",
585
+ "timelapseEndLQ",
586
+ )
587
+ } | super().unifi_dict_conversions()
588
588
 
589
589
 
590
590
  class StorageStats(ProtectBaseObject):
@@ -654,12 +654,11 @@ class CameraZone(ProtectBaseObject):
654
654
  points: list[tuple[Percent, Percent]]
655
655
 
656
656
  @classmethod
657
- def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
658
- data = super().unifi_dict_to_dict(data)
659
- if "points" in data and isinstance(data["points"], Iterable):
660
- data["points"] = [(p[0], p[1]) for p in data["points"]]
661
-
662
- return data
657
+ @cache
658
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
659
+ return {
660
+ "points": lambda x: [(p[0], p[1]) for p in x],
661
+ } | super().unifi_dict_conversions()
663
662
 
664
663
  def unifi_dict(
665
664
  self,
@@ -691,11 +690,11 @@ class SmartMotionZone(MotionZone):
691
690
  object_types: list[SmartDetectObjectType]
692
691
 
693
692
  @classmethod
694
- def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
695
- if "objectTypes" in data:
696
- data["objectTypes"] = convert_smart_types(data.pop("objectTypes"))
697
-
698
- return super().unifi_dict_to_dict(data)
693
+ @cache
694
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
695
+ return {
696
+ "objectTypes": convert_smart_types,
697
+ } | super().unifi_dict_conversions()
699
698
 
700
699
 
701
700
  class PrivacyMaskCapability(ProtectBaseObject):
@@ -846,16 +845,16 @@ class CameraFeatureFlags(ProtectBaseObject):
846
845
  zoom: PTZZoomRange
847
846
 
848
847
  @classmethod
849
- def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
850
- if "smartDetectTypes" in data:
851
- data["smartDetectTypes"] = convert_smart_types(data.pop("smartDetectTypes"))
852
- if "smartDetectAudioTypes" in data:
853
- data["smartDetectAudioTypes"] = convert_smart_audio_types(
854
- data.pop("smartDetectAudioTypes"),
855
- )
856
- if "videoModes" in data:
857
- data["videoModes"] = convert_video_modes(data.pop("videoModes"))
848
+ @cache
849
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
850
+ return {
851
+ "smartDetectTypes": convert_smart_types,
852
+ "smartDetectAudioTypes": convert_smart_audio_types,
853
+ "videoModes": convert_video_modes,
854
+ } | super().unifi_dict_conversions()
858
855
 
856
+ @classmethod
857
+ def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
859
858
  # backport support for `is_doorbell` to older versions of Protect
860
859
  if "hasChime" in data and "isDoorbell" not in data:
861
860
  data["isDoorbell"] = data["hasChime"]
@@ -1020,14 +1019,18 @@ class Camera(ProtectMotionDeviceModel):
1020
1019
  "featureFlags",
1021
1020
  }
1022
1021
 
1022
+ @classmethod
1023
+ @cache
1024
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
1025
+ return {
1026
+ "chimeDuration": lambda x: timedelta(milliseconds=x),
1027
+ } | super().unifi_dict_conversions()
1028
+
1023
1029
  @classmethod
1024
1030
  def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
1025
1031
  # LCD messages comes back as empty dict {}
1026
1032
  if "lcdMessage" in data and len(data["lcdMessage"]) == 0:
1027
1033
  del data["lcdMessage"]
1028
- if "chimeDuration" in data and not isinstance(data["chimeDuration"], timedelta):
1029
- data["chimeDuration"] = timedelta(milliseconds=data["chimeDuration"])
1030
-
1031
1034
  return super().unifi_dict_to_dict(data)
1032
1035
 
1033
1036
  def unifi_dict(
@@ -3086,14 +3089,11 @@ class Doorlock(ProtectAdoptableDeviceModel):
3086
3089
  }
3087
3090
 
3088
3091
  @classmethod
3089
- def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
3090
- if "autoCloseTimeMs" in data and not isinstance(
3091
- data["autoCloseTimeMs"],
3092
- timedelta,
3093
- ):
3094
- data["autoCloseTimeMs"] = timedelta(milliseconds=data["autoCloseTimeMs"])
3095
-
3096
- return super().unifi_dict_to_dict(data)
3092
+ @cache
3093
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
3094
+ return {
3095
+ "autoCloseTimeMs": lambda x: timedelta(milliseconds=x)
3096
+ } | super().unifi_dict_conversions()
3097
3097
 
3098
3098
  @property
3099
3099
  def camera(self) -> Camera | None:
@@ -94,11 +94,11 @@ class SmartDetectItem(ProtectBaseObject):
94
94
  }
95
95
 
96
96
  @classmethod
97
- def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
98
- if "duration" in data:
99
- data["duration"] = timedelta(milliseconds=data["duration"])
100
-
101
- return super().unifi_dict_to_dict(data)
97
+ @cache
98
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
99
+ return {
100
+ "duration": lambda x: timedelta(milliseconds=x),
101
+ } | super().unifi_dict_conversions()
102
102
 
103
103
 
104
104
  class SmartDetectTrack(ProtectBaseObject):
@@ -161,14 +161,9 @@ class EventDetectedThumbnail(ProtectBaseObject):
161
161
  name: str | None
162
162
 
163
163
  @classmethod
164
- def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
165
- if "clockBestWall" in data:
166
- if data["clockBestWall"]:
167
- data["clockBestWall"] = convert_to_datetime(data["clockBestWall"])
168
- else:
169
- del data["clockBestWall"]
170
-
171
- return super().unifi_dict_to_dict(data)
164
+ @cache
165
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
166
+ return {"clockBestWall": convert_to_datetime} | super().unifi_dict_conversions()
172
167
 
173
168
  def unifi_dict(
174
169
  self,
@@ -301,11 +296,12 @@ class Event(ProtectModelWithId):
301
296
  }
302
297
 
303
298
  @classmethod
304
- def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
305
- for key in ("start", "end", "timestamp", "deletedAt"):
306
- if key in data:
307
- data[key] = convert_to_datetime(data[key])
308
- return super().unifi_dict_to_dict(data)
299
+ @cache
300
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
301
+ return {
302
+ key: convert_to_datetime
303
+ for key in ("start", "end", "timestamp", "deletedAt")
304
+ } | super().unifi_dict_conversions()
309
305
 
310
306
  def unifi_dict(
311
307
  self,
@@ -618,11 +614,11 @@ class UOSDisk(ProtectBaseObject):
618
614
  }
619
615
 
620
616
  @classmethod
621
- def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
622
- if "estimate" in data and data["estimate"] is not None:
623
- data["estimate"] = timedelta(seconds=data.pop("estimate"))
624
-
625
- return super().unifi_dict_to_dict(data)
617
+ @cache
618
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
619
+ return {
620
+ "estimate": lambda x: timedelta(seconds=x)
621
+ } | super().unifi_dict_conversions()
626
622
 
627
623
  def unifi_dict(
628
624
  self,
@@ -702,11 +698,11 @@ class UOSSpace(ProtectBaseObject):
702
698
  }
703
699
 
704
700
  @classmethod
705
- def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
706
- if "estimate" in data and data["estimate"] is not None:
707
- data["estimate"] = timedelta(seconds=data.pop("estimate"))
708
-
709
- return super().unifi_dict_to_dict(data)
701
+ @cache
702
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
703
+ return {
704
+ "estimate": lambda x: timedelta(seconds=x)
705
+ } | super().unifi_dict_conversions()
710
706
 
711
707
  def unifi_dict(
712
708
  self,
@@ -770,13 +766,12 @@ class DoorbellSettings(ProtectBaseObject):
770
766
  }
771
767
 
772
768
  @classmethod
773
- def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
774
- if "defaultMessageResetTimeoutMs" in data:
775
- data["defaultMessageResetTimeout"] = timedelta(
776
- milliseconds=data.pop("defaultMessageResetTimeoutMs"),
777
- )
778
-
779
- return super().unifi_dict_to_dict(data)
769
+ @cache
770
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
771
+ return {
772
+ # defaultMessageResetTimeoutMs is remapped to defaultMessageResetTimeout
773
+ "defaultMessageResetTimeoutMs": lambda x: timedelta(milliseconds=x),
774
+ } | super().unifi_dict_conversions()
780
775
 
781
776
 
782
777
  class RecordingTypeDistribution(ProtectBaseObject):
@@ -864,15 +859,12 @@ class StorageStats(ProtectBaseObject):
864
859
  storage_distribution: StorageDistribution
865
860
 
866
861
  @classmethod
867
- def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
868
- if "capacity" in data and data["capacity"] is not None:
869
- data["capacity"] = timedelta(milliseconds=data.pop("capacity"))
870
- if "remainingCapacity" in data and data["remainingCapacity"] is not None:
871
- data["remainingCapacity"] = timedelta(
872
- milliseconds=data.pop("remainingCapacity"),
873
- )
874
-
875
- return super().unifi_dict_to_dict(data)
862
+ @cache
863
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
864
+ return {
865
+ "capacity": lambda x: timedelta(milliseconds=x),
866
+ "remainingCapacity": lambda x: timedelta(milliseconds=x),
867
+ } | super().unifi_dict_conversions()
876
868
 
877
869
 
878
870
  class NVRFeatureFlags(ProtectBaseObject):
@@ -1017,24 +1009,15 @@ class NVR(ProtectDeviceModel):
1017
1009
  }
1018
1010
 
1019
1011
  @classmethod
1020
- def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
1021
- if "lastUpdateAt" in data:
1022
- data["lastUpdateAt"] = convert_to_datetime(data["lastUpdateAt"])
1023
- if "lastDeviceFwUpdatesCheckedAt" in data:
1024
- data["lastDeviceFwUpdatesCheckedAt"] = convert_to_datetime(
1025
- data["lastDeviceFwUpdatesCheckedAt"]
1026
- )
1027
- if (
1028
- "recordingRetentionDurationMs" in data
1029
- and data["recordingRetentionDurationMs"] is not None
1030
- ):
1031
- data["recordingRetentionDuration"] = timedelta(
1032
- milliseconds=data.pop("recordingRetentionDurationMs"),
1033
- )
1034
- if "timezone" in data and not isinstance(data["timezone"], tzinfo):
1035
- data["timezone"] = zoneinfo.ZoneInfo(data["timezone"])
1036
-
1037
- return super().unifi_dict_to_dict(data)
1012
+ @cache
1013
+ def unifi_dict_conversions(cls) -> dict[str, object | Callable[[Any], Any]]:
1014
+ return {
1015
+ "lastUpdateAt": convert_to_datetime,
1016
+ "lastDeviceFwUpdatesCheckedAt": convert_to_datetime,
1017
+ "timezone": zoneinfo.ZoneInfo,
1018
+ # recordingRetentionDurationMs is remapped to recordingRetentionDuration
1019
+ "recordingRetentionDurationMs": lambda x: timedelta(milliseconds=x),
1020
+ } | super().unifi_dict_conversions()
1038
1021
 
1039
1022
  async def _api_update(self, data: dict[str, Any]) -> None:
1040
1023
  return await self._api.update_nvr(data)
File without changes
File without changes