uiprotect 6.8.0__py3-none-any.whl → 7.0.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/cli/base.py CHANGED
@@ -7,7 +7,7 @@ from typing import Any, Optional, TypeVar
7
7
 
8
8
  import orjson
9
9
  import typer
10
- from pydantic.v1 import ValidationError
10
+ from pydantic import ValidationError
11
11
 
12
12
  from ..api import ProtectApiClient
13
13
  from ..data import NVR, ProtectAdoptableDeviceModel, ProtectBaseObject
uiprotect/data/base.py CHANGED
@@ -12,8 +12,8 @@ from typing import TYPE_CHECKING, Any, NamedTuple, TypeVar
12
12
  from uuid import UUID
13
13
 
14
14
  from convertertools import pop_dict_set_if_none, pop_dict_tuple
15
- from pydantic.v1 import BaseModel
16
- from pydantic.v1.fields import SHAPE_DICT, SHAPE_LIST, PrivateAttr
15
+ from pydantic import BaseModel, ConfigDict
16
+ from pydantic.fields import PrivateAttr
17
17
 
18
18
  from .._compat import cached_property
19
19
  from ..exceptions import BadRequest, ClientError, NotAuthorized
@@ -27,11 +27,14 @@ from ..utils import (
27
27
  to_snake_case,
28
28
  )
29
29
  from .types import (
30
+ SHAPE_DICT_V1,
31
+ SHAPE_LIST_V1,
30
32
  ModelType,
31
33
  PercentFloat,
32
34
  PermissionNode,
33
35
  ProtectWSPayloadFormat,
34
36
  StateType,
37
+ extract_type_shape,
35
38
  )
36
39
  from .websocket import (
37
40
  WSJSONPacketFrame,
@@ -62,7 +65,7 @@ _LOGGER = logging.getLogger(__name__)
62
65
 
63
66
 
64
67
  @cache
65
- def _is_protect_base_object(cls: type) -> bool:
68
+ def _is_protect_base_object(cls: type[Any]) -> bool:
66
69
  """A cached version of `issubclass(cls, ProtectBaseObject)` to speed up the check."""
67
70
  return issubclass(cls, ProtectBaseObject)
68
71
 
@@ -93,12 +96,8 @@ class ProtectBaseObject(BaseModel):
93
96
  * Provides `.unifi_dict` to convert object back into UFP JSON
94
97
  """
95
98
 
96
- _api: ProtectApiClient = PrivateAttr(None)
97
-
98
- class Config:
99
- arbitrary_types_allowed = True
100
- validate_assignment = True
101
- copy_on_model_validation = "shallow"
99
+ _api: ProtectApiClient = PrivateAttr(None) # type: ignore[assignment]
100
+ model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True)
102
101
 
103
102
  def __init__(self, api: ProtectApiClient | None = None, **data: Any) -> None:
104
103
  """
@@ -219,15 +218,16 @@ class ProtectBaseObject(BaseModel):
219
218
  lists: dict[str, type[ProtectBaseObject]] = {}
220
219
  dicts: dict[str, type[ProtectBaseObject]] = {}
221
220
 
222
- for name, field in cls.__fields__.items():
221
+ for name, field in cls.model_fields.items():
223
222
  try:
224
- if _is_protect_base_object(field.type_):
225
- if field.shape == SHAPE_LIST:
226
- lists[name] = field.type_
227
- elif field.shape == SHAPE_DICT:
228
- dicts[name] = field.type_
223
+ type_, shape = extract_type_shape(field.annotation) # type: ignore[arg-type]
224
+ if _is_protect_base_object(type_):
225
+ if shape == SHAPE_LIST_V1:
226
+ lists[name] = type_
227
+ elif shape == SHAPE_DICT_V1:
228
+ dicts[name] = type_
229
229
  else:
230
- objs[name] = field.type_
230
+ objs[name] = type_
231
231
  except TypeError:
232
232
  pass
233
233
 
@@ -313,8 +313,8 @@ class ProtectBaseObject(BaseModel):
313
313
 
314
314
  remaps = cls._get_unifi_remaps()
315
315
  # convert to snake_case and remove extra fields
316
- _fields = cls.__fields__
317
- for key in list(data):
316
+ _fields = cls.model_fields
317
+ for key in data.copy():
318
318
  if key in remaps:
319
319
  # remap keys that will not be converted correctly by snake_case convert
320
320
  remapped_key = remaps[key]
@@ -437,7 +437,7 @@ class ProtectBaseObject(BaseModel):
437
437
  excluded_fields = self._get_excluded_fields()
438
438
  if exclude is not None:
439
439
  excluded_fields = excluded_fields.copy() | exclude
440
- data = self.dict(exclude=excluded_fields)
440
+ data = self.model_dump(exclude=excluded_fields)
441
441
  use_obj = True
442
442
 
443
443
  (
@@ -490,7 +490,7 @@ class ProtectBaseObject(BaseModel):
490
490
  has_unifi_dicts,
491
491
  ) = cls._get_protect_model()
492
492
  api = cls._api
493
- _fields = cls.__fields__
493
+ _fields = cls.model_fields
494
494
  unifi_obj: ProtectBaseObject | None
495
495
  value: Any
496
496
 
@@ -517,10 +517,10 @@ class ProtectBaseObject(BaseModel):
517
517
  def dict_with_excludes(self) -> dict[str, Any]:
518
518
  """Returns a dict of the current object without any UFP objects converted to dicts."""
519
519
  excludes = self.__class__._get_excluded_changed_fields()
520
- return self.dict(exclude=excludes)
520
+ return self.model_dump(exclude=excludes)
521
521
 
522
522
  def get_changed(self, data_before_changes: dict[str, Any]) -> dict[str, Any]:
523
- return dict_diff(data_before_changes, self.dict())
523
+ return dict_diff(data_before_changes, self.model_dump())
524
524
 
525
525
  @property
526
526
  def api(self) -> ProtectApiClient:
@@ -540,7 +540,7 @@ class ProtectModel(ProtectBaseObject):
540
540
  automatically decoding a `modelKey` object into the correct UFP object and type
541
541
  """
542
542
 
543
- model: ModelType | None
543
+ model: ModelType | None = None
544
544
 
545
545
  @classmethod
546
546
  @cache
@@ -579,7 +579,7 @@ class UpdateSynchronization:
579
579
  class ProtectModelWithId(ProtectModel):
580
580
  id: str
581
581
 
582
- _update_sync: UpdateSynchronization = PrivateAttr(None)
582
+ _update_sync: UpdateSynchronization = PrivateAttr(None) # type: ignore[assignment]
583
583
 
584
584
  def __init__(self, **data: Any) -> None:
585
585
  update_sync = data.pop("update_sync", None)
@@ -794,15 +794,15 @@ class ProtectModelWithId(ProtectModel):
794
794
 
795
795
 
796
796
  class ProtectDeviceModel(ProtectModelWithId):
797
- name: str | None
797
+ name: str | None = None
798
798
  type: str
799
799
  mac: str
800
- host: IPv4Address | str | None
801
- up_since: datetime | None
802
- uptime: timedelta | None
803
- last_seen: datetime | None
804
- hardware_revision: str | None
805
- firmware_version: str | None
800
+ host: IPv4Address | str | None = None
801
+ up_since: datetime | None = None
802
+ uptime: timedelta | None = None
803
+ last_seen: datetime | None = None
804
+ hardware_revision: str | None = None
805
+ firmware_version: str | None = None
806
806
  is_updating: bool
807
807
  is_ssh_enabled: bool
808
808
 
@@ -853,12 +853,12 @@ class ProtectDeviceModel(ProtectModelWithId):
853
853
 
854
854
 
855
855
  class WiredConnectionState(ProtectBaseObject):
856
- phy_rate: float | None
856
+ phy_rate: int | None = None
857
857
 
858
858
 
859
859
  class WirelessConnectionState(ProtectBaseObject):
860
- signal_quality: int | None
861
- signal_strength: int | None
860
+ signal_quality: int | None = None
861
+ signal_strength: int | None = None
862
862
 
863
863
 
864
864
  class BluetoothConnectionState(WirelessConnectionState):
@@ -866,10 +866,10 @@ class BluetoothConnectionState(WirelessConnectionState):
866
866
 
867
867
 
868
868
  class WifiConnectionState(WirelessConnectionState):
869
- phy_rate: float | None
870
- channel: int | None
871
- frequency: int | None
872
- ssid: str | None
869
+ phy_rate: int | None = None
870
+ channel: int | None = None
871
+ frequency: int | None = None
872
+ ssid: str | None = None
873
873
  bssid: str | None = None
874
874
  tx_rate: float | None = None
875
875
  # requires 2.7.5+
@@ -881,10 +881,10 @@ class WifiConnectionState(WirelessConnectionState):
881
881
 
882
882
  class ProtectAdoptableDeviceModel(ProtectDeviceModel):
883
883
  state: StateType
884
- connection_host: IPv4Address | str | None
885
- connected_since: datetime | None
886
- latest_firmware_version: str | None
887
- firmware_build: str | None
884
+ connection_host: IPv4Address | str | None = None
885
+ connected_since: datetime | None = None
886
+ latest_firmware_version: str | None = None
887
+ firmware_build: str | None = None
888
888
  is_adopting: bool
889
889
  is_adopted: bool
890
890
  is_adopted_by_other: bool
@@ -894,7 +894,7 @@ class ProtectAdoptableDeviceModel(ProtectDeviceModel):
894
894
  is_attempting_to_connect: bool
895
895
  is_connected: bool
896
896
  # requires 1.21+
897
- market_name: str | None
897
+ market_name: str | None = None
898
898
  # requires 2.7.5+
899
899
  fw_update_state: str | None = None
900
900
  # requires 2.8.14+
@@ -909,8 +909,8 @@ class ProtectAdoptableDeviceModel(ProtectDeviceModel):
909
909
  wired_connection_state: WiredConnectionState | None = None
910
910
  wifi_connection_state: WifiConnectionState | None = None
911
911
  bluetooth_connection_state: BluetoothConnectionState | None = None
912
- bridge_id: str | None
913
- is_downloading_firmware: bool | None
912
+ bridge_id: str | None = None
913
+ is_downloading_firmware: bool | None = None
914
914
 
915
915
  # TODO:
916
916
  # bridgeCandidates
@@ -1051,7 +1051,7 @@ class ProtectAdoptableDeviceModel(ProtectDeviceModel):
1051
1051
 
1052
1052
 
1053
1053
  class ProtectMotionDeviceModel(ProtectAdoptableDeviceModel):
1054
- last_motion: datetime | None
1054
+ last_motion: datetime | None = None
1055
1055
  is_dark: bool
1056
1056
 
1057
1057
  # not directly from UniFi
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, cast
10
10
 
11
11
  from aiohttp.client_exceptions import ServerDisconnectedError
12
12
  from convertertools import pop_dict_set, pop_dict_tuple
13
- from pydantic.v1 import PrivateAttr, ValidationError
13
+ from pydantic import PrivateAttr, ValidationError
14
14
 
15
15
  from ..exceptions import ClientError
16
16
  from ..utils import normalize_mac, to_snake_case, utc_now
@@ -362,7 +362,7 @@ class Bootstrap(ProtectBaseObject):
362
362
  return WSSubscriptionMessage(
363
363
  action=WSAction.ADD,
364
364
  new_update_id=self.last_update_id,
365
- changed_data=obj.dict(),
365
+ changed_data=obj.model_dump(),
366
366
  new_obj=obj,
367
367
  )
368
368
 
@@ -407,7 +407,7 @@ class Bootstrap(ProtectBaseObject):
407
407
  return WSSubscriptionMessage(
408
408
  action=WSAction.ADD,
409
409
  new_update_id=self.last_update_id,
410
- changed_data=add_obj.dict(),
410
+ changed_data=add_obj.model_dump(),
411
411
  new_obj=add_obj,
412
412
  )
413
413
  elif action_type == "remove":
uiprotect/data/devices.py CHANGED
@@ -13,7 +13,7 @@ from pathlib import Path
13
13
  from typing import TYPE_CHECKING, Any, Literal, cast
14
14
 
15
15
  from convertertools import pop_dict_set_if_none, pop_dict_tuple
16
- from pydantic.v1.fields import PrivateAttr
16
+ from pydantic.fields import PrivateAttr
17
17
 
18
18
  from ..exceptions import BadRequest, NotAuthorized, StreamError
19
19
  from ..stream import TalkbackStream
@@ -123,7 +123,7 @@ class Light(ProtectMotionDeviceModel):
123
123
  light_device_settings: LightDeviceSettings
124
124
  light_on_settings: LightOnSettings
125
125
  light_mode_settings: LightModeSettings
126
- camera_id: str | None
126
+ camera_id: str | None = None
127
127
  is_camera_paired: bool
128
128
 
129
129
  @classmethod
@@ -245,15 +245,15 @@ class CameraChannel(ProtectBaseObject):
245
245
  name: str # read only
246
246
  enabled: bool # read only
247
247
  is_rtsp_enabled: bool
248
- rtsp_alias: str | None # read only
248
+ rtsp_alias: str | None = None # read only
249
249
  width: int
250
250
  height: int
251
251
  fps: int
252
252
  bitrate: int
253
253
  min_bitrate: int # read only
254
254
  max_bitrate: int # read only
255
- min_client_adaptive_bit_rate: int | None # read only
256
- min_motion_adaptive_bit_rate: int | None # read only
255
+ min_client_adaptive_bit_rate: int | None = None # read only
256
+ min_motion_adaptive_bit_rate: int | None = None # read only
257
257
  fps_values: list[int] # read only
258
258
  idr_interval: int
259
259
  # 3.0.22+
@@ -325,8 +325,8 @@ class ISPSettings(ProtectBaseObject):
325
325
  d_zoom_stream_id: int
326
326
  focus_mode: FocusMode | None = None
327
327
  focus_position: int
328
- touch_focus_x: int | None
329
- touch_focus_y: int | None
328
+ touch_focus_x: int | None = None
329
+ touch_focus_y: int | None = None
330
330
  zoom_position: PercentInt
331
331
  mount_position: MountPosition | None = None
332
332
  # requires 2.8.14+
@@ -545,8 +545,8 @@ class TalkbackSettings(ProtectBaseObject):
545
545
  type_in: str
546
546
  bind_addr: IPv4Address
547
547
  bind_port: int
548
- filter_addr: str | None # can be used to restrict sender address
549
- filter_port: int | None # can be used to restrict sender port
548
+ filter_addr: str | None = None # can be used to restrict sender address
549
+ filter_port: int | None = None # can be used to restrict sender port
550
550
  channels: int # 1 or 2
551
551
  sampling_rate: int # 8000, 11025, 22050, 44100, 48000
552
552
  bits_per_sample: int
@@ -554,22 +554,22 @@ class TalkbackSettings(ProtectBaseObject):
554
554
 
555
555
 
556
556
  class WifiStats(ProtectBaseObject):
557
- channel: int | None
558
- frequency: int | None
559
- link_speed_mbps: str | None
557
+ channel: int | None = None
558
+ frequency: int | None = None
559
+ link_speed_mbps: str | None = None
560
560
  signal_quality: PercentInt
561
561
  signal_strength: int
562
562
 
563
563
 
564
564
  class VideoStats(ProtectBaseObject):
565
- recording_start: datetime | None
566
- recording_end: datetime | None
567
- recording_start_lq: datetime | None
568
- recording_end_lq: datetime | None
569
- timelapse_start: datetime | None
570
- timelapse_end: datetime | None
571
- timelapse_start_lq: datetime | None
572
- timelapse_end_lq: datetime | None
565
+ recording_start: datetime | None = None
566
+ recording_end: datetime | None = None
567
+ recording_start_lq: datetime | None = None
568
+ recording_end_lq: datetime | None = None
569
+ timelapse_start: datetime | None = None
570
+ timelapse_end: datetime | None = None
571
+ timelapse_start_lq: datetime | None = None
572
+ timelapse_end_lq: datetime | None = None
573
573
 
574
574
  @classmethod
575
575
  @cache
@@ -601,8 +601,8 @@ class VideoStats(ProtectBaseObject):
601
601
 
602
602
 
603
603
  class StorageStats(ProtectBaseObject):
604
- used: int | None # bytes
605
- rate: float | None # bytes / millisecond
604
+ used: int | None = None # bytes
605
+ rate: float | None = None # bytes / millisecond
606
606
 
607
607
  @property
608
608
  def rate_per_second(self) -> float | None:
@@ -633,7 +633,7 @@ class CameraStats(ProtectBaseObject):
633
633
  tx_bytes: int
634
634
  wifi: WifiStats
635
635
  video: VideoStats
636
- storage: StorageStats | None
636
+ storage: StorageStats | None = None
637
637
  wifi_quality: PercentInt
638
638
  wifi_strength: int
639
639
 
@@ -708,7 +708,7 @@ class SmartMotionZone(MotionZone):
708
708
 
709
709
 
710
710
  class PrivacyMaskCapability(ProtectBaseObject):
711
- max_masks: int | None
711
+ max_masks: int | None = None
712
712
  rectangle_only: bool
713
713
 
714
714
 
@@ -735,9 +735,9 @@ class Hotplug(ProtectBaseObject):
735
735
 
736
736
 
737
737
  class PTZRangeSingle(ProtectBaseObject):
738
- max: float | None
739
- min: float | None
740
- step: float | None
738
+ max: float | None = None
739
+ min: float | None = None
740
+ step: float | None = None
741
741
 
742
742
 
743
743
  class PTZRange(ProtectBaseObject):
@@ -771,7 +771,7 @@ class PTZRange(ProtectBaseObject):
771
771
 
772
772
 
773
773
  class PTZZoomRange(PTZRange):
774
- ratio: float
774
+ ratio: int
775
775
 
776
776
  def to_native_value(self, zoom_value: float, is_relative: bool = False) -> float:
777
777
  """Convert zoom values to step values."""
@@ -921,13 +921,13 @@ class Camera(ProtectMotionDeviceModel):
921
921
  is_recording: bool
922
922
  is_motion_detected: bool
923
923
  is_smart_detected: bool
924
- phy_rate: float | None
924
+ phy_rate: int | None = None
925
925
  hdr_mode: bool
926
926
  # Recording Quality -> High Frame
927
927
  video_mode: VideoMode
928
928
  is_probing_for_wifi: bool
929
929
  chime_duration: timedelta
930
- last_ring: datetime | None
930
+ last_ring: datetime | None = None
931
931
  is_live_heatmap_enabled: bool
932
932
  video_reconfiguration_in_progress: bool
933
933
  channels: list[CameraChannel]
@@ -943,7 +943,7 @@ class Camera(ProtectMotionDeviceModel):
943
943
  smart_detect_zones: list[SmartMotionZone]
944
944
  stats: CameraStats
945
945
  feature_flags: CameraFeatureFlags
946
- lcd_message: LCDMessage | None
946
+ lcd_message: LCDMessage | None = None
947
947
  lenses: list[CameraLenses]
948
948
  platform: str
949
949
  has_speaker: bool
@@ -951,10 +951,10 @@ class Camera(ProtectMotionDeviceModel):
951
951
  audio_bitrate: int
952
952
  can_manage: bool
953
953
  is_managed: bool
954
- voltage: float | None
954
+ voltage: float | None = None
955
955
  # requires 1.21+
956
- is_poor_network: bool | None
957
- is_wireless_uplink_enabled: bool | None
956
+ is_poor_network: bool | None = None
957
+ is_wireless_uplink_enabled: bool | None = None
958
958
  # requires 2.6.13+
959
959
  homekit_settings: CameraHomekitSettings | None = None
960
960
  # requires 2.6.17+
@@ -1118,7 +1118,7 @@ class Camera(ProtectMotionDeviceModel):
1118
1118
  updated["lcd_message"] = {"reset_at": utc_now() - timedelta(seconds=10)}
1119
1119
  # otherwise, pass full LCD message to prevent issues
1120
1120
  elif self.lcd_message is not None:
1121
- updated["lcd_message"] = self.lcd_message.dict()
1121
+ updated["lcd_message"] = self.lcd_message.model_dump()
1122
1122
 
1123
1123
  # if reset_at is not passed in, it will default to reset in 1 minute
1124
1124
  if lcd_message is not None and "reset_at" not in lcd_message:
@@ -2771,8 +2771,8 @@ class SensorThresholdSettings(SensorSettingsBase):
2771
2771
  margin: float # read only
2772
2772
  # "safe" thresholds for alerting
2773
2773
  # anything below/above will trigger alert
2774
- low_threshold: float | None
2775
- high_threshold: float | None
2774
+ low_threshold: float | None = None
2775
+ high_threshold: float | None = None
2776
2776
 
2777
2777
 
2778
2778
  class SensorSensitivitySettings(SensorSettingsBase):
@@ -2780,12 +2780,12 @@ class SensorSensitivitySettings(SensorSettingsBase):
2780
2780
 
2781
2781
 
2782
2782
  class SensorBatteryStatus(ProtectBaseObject):
2783
- percentage: PercentInt | None
2783
+ percentage: PercentInt | None = None
2784
2784
  is_low: bool
2785
2785
 
2786
2786
 
2787
2787
  class SensorStat(ProtectBaseObject):
2788
- value: float | None
2788
+ value: float | None = None
2789
2789
  status: SensorStatusType
2790
2790
 
2791
2791
 
@@ -2797,20 +2797,20 @@ class SensorStats(ProtectBaseObject):
2797
2797
 
2798
2798
  class Sensor(ProtectAdoptableDeviceModel):
2799
2799
  alarm_settings: SensorSettingsBase
2800
- alarm_triggered_at: datetime | None
2800
+ alarm_triggered_at: datetime | None = None
2801
2801
  battery_status: SensorBatteryStatus
2802
- camera_id: str | None
2802
+ camera_id: str | None = None
2803
2803
  humidity_settings: SensorThresholdSettings
2804
2804
  is_motion_detected: bool
2805
2805
  is_opened: bool
2806
- leak_detected_at: datetime | None
2806
+ leak_detected_at: datetime | None = None
2807
2807
  led_settings: SensorSettingsBase
2808
2808
  light_settings: SensorThresholdSettings
2809
- motion_detected_at: datetime | None
2809
+ motion_detected_at: datetime | None = None
2810
2810
  motion_settings: SensorSensitivitySettings
2811
- open_status_changed_at: datetime | None
2811
+ open_status_changed_at: datetime | None = None
2812
2812
  stats: SensorStats
2813
- tampering_detected_at: datetime | None
2813
+ tampering_detected_at: datetime | None = None
2814
2814
  temperature_settings: SensorThresholdSettings
2815
2815
  mount_type: MountType
2816
2816
 
@@ -3104,13 +3104,13 @@ class Sensor(ProtectAdoptableDeviceModel):
3104
3104
 
3105
3105
 
3106
3106
  class Doorlock(ProtectAdoptableDeviceModel):
3107
- credentials: str | None
3107
+ credentials: str | None = None
3108
3108
  lock_status: LockStatusType
3109
3109
  enable_homekit: bool
3110
3110
  auto_close_time: timedelta
3111
3111
  led_settings: SensorSettingsBase
3112
3112
  battery_status: SensorBatteryStatus
3113
- camera_id: str | None
3113
+ camera_id: str | None = None
3114
3114
  has_homekit: bool
3115
3115
  private_token: str
3116
3116
 
@@ -3247,7 +3247,7 @@ class ChimeTrack(ProtectBaseObject):
3247
3247
  class Chime(ProtectAdoptableDeviceModel):
3248
3248
  volume: PercentInt
3249
3249
  is_probing_for_wifi: bool
3250
- last_ring: datetime | None
3250
+ last_ring: datetime | None = None
3251
3251
  is_wireless_uplink_enabled: bool
3252
3252
  camera_ids: list[str]
3253
3253
  # requires 2.6.17+
uiprotect/data/nvr.py CHANGED
@@ -17,7 +17,7 @@ import aiofiles
17
17
  import orjson
18
18
  from aiofiles import os as aos
19
19
  from convertertools import pop_dict_set_if_none, pop_dict_tuple
20
- from pydantic.v1.fields import PrivateAttr
20
+ from pydantic.fields import PrivateAttr
21
21
 
22
22
  from ..exceptions import BadRequest, NotAuthorized
23
23
  from ..utils import RELEASE_CACHE, convert_to_datetime
@@ -61,7 +61,7 @@ from .types import (
61
61
  from .user import User, UserLocation
62
62
 
63
63
  if TYPE_CHECKING:
64
- from pydantic.v1.typing import SetStr
64
+ from pydantic.typing import SetStr
65
65
 
66
66
 
67
67
  _LOGGER = logging.getLogger(__name__)
@@ -181,7 +181,7 @@ class EventDetectedThumbnail(ProtectBaseObject):
181
181
  type: str
182
182
  cropped_id: str
183
183
  attributes: EventThumbnailAttributes | None = None
184
- name: str | None
184
+ name: str | None = None
185
185
 
186
186
  @classmethod
187
187
  @cache
@@ -199,24 +199,24 @@ class EventDetectedThumbnail(ProtectBaseObject):
199
199
 
200
200
 
201
201
  class EventMetadata(ProtectBaseObject):
202
- client_platform: str | None
203
- reason: str | None
204
- app_update: str | None
205
- light_id: str | None
206
- light_name: str | None
207
- type: str | None
208
- sensor_id: str | None
209
- sensor_name: str | None
210
- sensor_type: SensorType | None
211
- doorlock_id: str | None
212
- doorlock_name: str | None
213
- from_value: str | None
214
- to_value: str | None
215
- mount_type: MountType | None
216
- status: SensorStatusType | None
217
- alarm_type: str | None
218
- device_id: str | None
219
- mac: str | None
202
+ client_platform: str | None = None
203
+ reason: str | None = None
204
+ app_update: str | None = None
205
+ light_id: str | None = None
206
+ light_name: str | None = None
207
+ type: str | None = None
208
+ sensor_id: str | None = None
209
+ sensor_name: str | None = None
210
+ sensor_type: SensorType | None = None
211
+ doorlock_id: str | None = None
212
+ doorlock_name: str | None = None
213
+ from_value: str | None = None
214
+ to_value: str | None = None
215
+ mount_type: MountType | None = None
216
+ status: SensorStatusType | None = None
217
+ alarm_type: str | None = None
218
+ device_id: str | None = None
219
+ mac: str | None = None
220
220
  # require 2.7.5+
221
221
  license_plate: LicensePlateMetadata | None = None
222
222
  # requires 2.11.13+
@@ -281,16 +281,16 @@ class EventMetadata(ProtectBaseObject):
281
281
  class Event(ProtectModelWithId):
282
282
  type: EventType
283
283
  start: datetime
284
- end: datetime | None
284
+ end: datetime | None = None
285
285
  score: int
286
- heatmap_id: str | None
287
- camera_id: str | None
286
+ heatmap_id: str | None = None
287
+ camera_id: str | None = None
288
288
  smart_detect_types: list[SmartDetectObjectType]
289
289
  smart_detect_event_ids: list[str]
290
- thumbnail_id: str | None
291
- user_id: str | None
292
- timestamp: datetime | None
293
- metadata: EventMetadata | None
290
+ thumbnail_id: str | None = None
291
+ user_id: str | None = None
292
+ timestamp: datetime | None = None
293
+ metadata: EventMetadata | None = None
294
294
  # requires 2.7.5+
295
295
  deleted_at: datetime | None = None
296
296
  deletion_type: Literal["manual", "automatic"] | None = None
@@ -551,9 +551,9 @@ class CPUInfo(ProtectBaseObject):
551
551
 
552
552
 
553
553
  class MemoryInfo(ProtectBaseObject):
554
- available: int | None
555
- free: int | None
556
- total: int | None
554
+ available: int | None = None
555
+ free: int | None = None
556
+ total: int | None = None
557
557
 
558
558
 
559
559
  class StorageDevice(ProtectBaseObject):
@@ -868,8 +868,8 @@ class StorageDistribution(ProtectBaseObject):
868
868
 
869
869
  class StorageStats(ProtectBaseObject):
870
870
  utilization: float
871
- capacity: timedelta | None
872
- remaining_capacity: timedelta | None
871
+ capacity: timedelta | None = None
872
+ remaining_capacity: timedelta | None = None
873
873
  recording_space: StorageSpace
874
874
  storage_distribution: StorageDistribution
875
875
 
@@ -916,7 +916,7 @@ class NVR(ProtectDeviceModel):
916
916
  ucore_version: str
917
917
  hardware_platform: str
918
918
  ports: PortConfig
919
- last_update_at: datetime | None
919
+ last_update_at: datetime | None = None
920
920
  is_station: bool
921
921
  enable_automatic_backups: bool
922
922
  enable_stats_reporting: bool
@@ -927,14 +927,14 @@ class NVR(ProtectDeviceModel):
927
927
  host_type: int
928
928
  host_shortname: str
929
929
  is_hardware: bool
930
- is_wireless_uplink_enabled: bool | None
930
+ is_wireless_uplink_enabled: bool | None = None
931
931
  time_format: Literal["12h", "24h"]
932
932
  temperature_unit: Literal["C", "F"]
933
- recording_retention_duration: timedelta | None
933
+ recording_retention_duration: timedelta | None = None
934
934
  enable_crash_reporting: bool
935
935
  disable_audio: bool
936
936
  analytics_data: AnalyticsOption
937
- anonymous_device_id: UUID | None
937
+ anonymous_device_id: UUID | None = None
938
938
  camera_utilization: int
939
939
  is_recycling: bool
940
940
  disable_auto_link: bool
uiprotect/data/types.py CHANGED
@@ -2,13 +2,18 @@ from __future__ import annotations
2
2
 
3
3
  import enum
4
4
  from collections.abc import Callable, Coroutine
5
- from functools import cache
6
- from typing import Any, Literal, Optional, TypeVar, Union
5
+ from functools import cache, lru_cache
6
+ from typing import Annotated, Any, Literal, Optional, TypeVar, Union
7
7
 
8
8
  from packaging.version import Version as BaseVersion
9
- from pydantic.v1 import BaseModel, ConstrainedInt
10
- from pydantic.v1.color import Color as BaseColor
11
- from pydantic.v1.types import ConstrainedFloat, ConstrainedStr
9
+ from pydantic import BaseModel, Field
10
+ from pydantic.types import StringConstraints
11
+ from pydantic.v1.config import BaseConfig as BaseConfigV1
12
+ from pydantic.v1.fields import SHAPE_DICT as SHAPE_DICT_V1 # noqa: F401
13
+ from pydantic.v1.fields import SHAPE_LIST as SHAPE_LIST_V1 # noqa: F401
14
+ from pydantic.v1.fields import SHAPE_SET as SHAPE_SET_V1 # noqa: F401
15
+ from pydantic.v1.fields import ModelField as ModelFieldV1
16
+ from pydantic_extra_types.color import Color # noqa: F401
12
17
 
13
18
  from .._compat import cached_property
14
19
 
@@ -16,6 +21,22 @@ KT = TypeVar("KT")
16
21
  VT = TypeVar("VT")
17
22
 
18
23
 
24
+ class _BaseConfigV1(BaseConfigV1):
25
+ arbitrary_types_allowed = True
26
+ validate_assignment = True
27
+
28
+
29
+ @lru_cache(maxsize=512)
30
+ def extract_type_shape(annotation: type[Any] | None) -> tuple[Any, int]:
31
+ """Extract the type from a type hint."""
32
+ if annotation is None:
33
+ raise ValueError("Type annotation cannot be None")
34
+ v1_field = ModelFieldV1(
35
+ name="", type_=annotation, class_validators=None, model_config=_BaseConfigV1
36
+ )
37
+ return v1_field.type_, v1_field.shape
38
+
39
+
19
40
  DEFAULT = "DEFAULT_VALUE"
20
41
  DEFAULT_TYPE = Literal["DEFAULT_VALUE"]
21
42
  EventCategories = Literal[
@@ -626,58 +647,27 @@ class LensType(str, enum.Enum):
626
647
  DLSR_17 = "m43"
627
648
 
628
649
 
629
- class DoorbellText(ConstrainedStr):
630
- max_length = 30
631
-
632
-
633
- class ICRCustomValue(ConstrainedInt):
634
- ge = 0
635
- le = 10
636
-
637
-
638
- class ICRLuxValue(ConstrainedInt):
639
- ge = 1
640
- le = 30
641
-
650
+ DoorbellText = Annotated[str, StringConstraints(max_length=30)]
642
651
 
643
- class LEDLevel(ConstrainedInt):
644
- ge = 0
645
- le = 6
652
+ ICRCustomValue = Annotated[int, Field(ge=0, le=10)]
646
653
 
654
+ ICRLuxValue = Annotated[int, Field(ge=1, le=30)]
647
655
 
648
- class PercentInt(ConstrainedInt):
649
- ge = 0
650
- le = 100
656
+ LEDLevel = Annotated[int, Field(ge=0, le=6)]
651
657
 
658
+ PercentInt = Annotated[int, Field(ge=0, le=100)]
652
659
 
653
- class TwoByteInt(ConstrainedInt):
654
- ge = 1
655
- le = 255
660
+ TwoByteInt = Annotated[int, Field(ge=1, le=255)]
656
661
 
662
+ PercentFloat = Annotated[float, Field(ge=0, le=100)]
657
663
 
658
- class PercentFloat(ConstrainedFloat):
659
- ge = 0
660
- le = 100
664
+ WDRLevel = Annotated[int, Field(ge=0, le=3)]
661
665
 
666
+ ICRSensitivity = Annotated[int, Field(ge=0, le=3)]
662
667
 
663
- class WDRLevel(ConstrainedInt):
664
- ge = 0
665
- le = 3
668
+ Percent = Annotated[float, Field(ge=0, le=1)]
666
669
 
667
-
668
- class ICRSensitivity(ConstrainedInt):
669
- ge = 0
670
- le = 3
671
-
672
-
673
- class Percent(ConstrainedFloat):
674
- ge = 0
675
- le = 1
676
-
677
-
678
- class RepeatTimes(ConstrainedInt):
679
- ge = 1
680
- le = 6
670
+ RepeatTimes = Annotated[int, Field(ge=1, le=6)]
681
671
 
682
672
 
683
673
  class PTZPositionDegree(BaseModel):
@@ -714,15 +704,6 @@ class PTZPreset(BaseModel):
714
704
  CoordType = Union[Percent, int, float]
715
705
 
716
706
 
717
- # TODO: fix when upgrading to pydantic v2
718
- class Color(BaseColor):
719
- def __eq__(self, o: object) -> bool:
720
- if isinstance(o, Color):
721
- return self.as_hex() == o.as_hex()
722
-
723
- return super().__eq__(o)
724
-
725
-
726
707
  class Version(BaseVersion):
727
708
  def __str__(self) -> str:
728
709
  super_str = super().__str__()
uiprotect/data/user.py CHANGED
@@ -8,7 +8,7 @@ from datetime import datetime
8
8
  from functools import cache
9
9
  from typing import TYPE_CHECKING, Any, Generic, TypeVar
10
10
 
11
- from pydantic.v1.fields import PrivateAttr
11
+ from pydantic.fields import PrivateAttr
12
12
 
13
13
  from .base import ProtectBaseObject, ProtectModel, ProtectModelWithId
14
14
  from .types import ModelType, PermissionNode
@@ -23,7 +23,7 @@ class Permission(ProtectBaseObject):
23
23
  raw_permission: str
24
24
  model: ModelType
25
25
  nodes: set[PermissionNode]
26
- obj_ids: set[str] | None
26
+ obj_ids: set[str] | None = None
27
27
 
28
28
  @classmethod
29
29
  def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
@@ -79,8 +79,8 @@ class Group(ProtectModelWithId):
79
79
 
80
80
  class UserLocation(ProtectModel):
81
81
  is_away: bool
82
- latitude: float | None
83
- longitude: float | None
82
+ latitude: float | None = None
83
+ longitude: float | None = None
84
84
 
85
85
 
86
86
  class CloudAccount(ProtectModelWithId):
@@ -89,7 +89,7 @@ class CloudAccount(ProtectModelWithId):
89
89
  email: str
90
90
  user_id: str
91
91
  name: str
92
- location: UserLocation | None
92
+ location: UserLocation | None = None
93
93
  profile_img: str | None = None
94
94
 
95
95
  @classmethod
@@ -123,21 +123,21 @@ class UserFeatureFlags(ProtectBaseObject):
123
123
 
124
124
  class User(ProtectModelWithId):
125
125
  permissions: list[Permission]
126
- last_login_ip: str | None
127
- last_login_time: datetime | None
126
+ last_login_ip: str | None = None
127
+ last_login_time: datetime | None = None
128
128
  is_owner: bool
129
129
  enable_notifications: bool
130
130
  has_accepted_invite: bool
131
131
  all_permissions: list[Permission]
132
132
  scopes: list[str] | None = None
133
- location: UserLocation | None
133
+ location: UserLocation | None = None
134
134
  name: str
135
135
  first_name: str
136
136
  last_name: str
137
- email: str | None
137
+ email: str | None = None
138
138
  local_username: str
139
139
  group_ids: list[str]
140
- cloud_account: CloudAccount | None
140
+ cloud_account: CloudAccount | None = None
141
141
  feature_flags: UserFeatureFlags
142
142
 
143
143
  # TODO:
uiprotect/utils.py CHANGED
@@ -29,15 +29,18 @@ from uuid import UUID
29
29
 
30
30
  import jwt
31
31
  from aiohttp import ClientResponse
32
- from pydantic.v1.fields import SHAPE_DICT, SHAPE_LIST, SHAPE_SET, ModelField
33
- from pydantic.v1.utils import to_camel
32
+ from pydantic.fields import FieldInfo
34
33
 
35
34
  from .data.types import (
35
+ SHAPE_DICT_V1,
36
+ SHAPE_LIST_V1,
37
+ SHAPE_SET_V1,
36
38
  Color,
37
39
  SmartDetectAudioType,
38
40
  SmartDetectObjectType,
39
41
  Version,
40
42
  VideoMode,
43
+ extract_type_shape,
41
44
  )
42
45
  from .exceptions import NvrError
43
46
 
@@ -88,6 +91,11 @@ IP_TYPES = {
88
91
  }
89
92
 
90
93
 
94
+ @lru_cache
95
+ def to_camel(string: str) -> str:
96
+ return "".join(word.capitalize() for word in string.split("_"))
97
+
98
+
91
99
  def set_debug() -> None:
92
100
  """Sets ENV variable for UFP_DEBUG to on (True)"""
93
101
  os.environ[DEBUG_ENV] = str(True)
@@ -202,22 +210,22 @@ def to_camel_case(name: str) -> str:
202
210
 
203
211
 
204
212
  _EMPTY_UUID = UUID("0" * 32)
205
- _SHAPE_TYPES = {SHAPE_DICT, SHAPE_LIST, SHAPE_SET}
213
+ _SHAPE_TYPES = {SHAPE_DICT_V1, SHAPE_SET_V1, SHAPE_LIST_V1}
206
214
 
207
215
 
208
- def convert_unifi_data(value: Any, field: ModelField) -> Any:
216
+ def convert_unifi_data(value: Any, field: FieldInfo) -> Any:
209
217
  """Converts value from UFP data into pydantic field class"""
210
- type_ = field.type_
218
+ type_, shape = extract_type_shape(field.annotation) # type: ignore[arg-type]
211
219
 
212
220
  if type_ is Any:
213
221
  return value
214
222
 
215
- if (shape := field.shape) in _SHAPE_TYPES:
216
- if shape == SHAPE_LIST and isinstance(value, list):
223
+ if shape in _SHAPE_TYPES:
224
+ if shape == SHAPE_LIST_V1 and isinstance(value, list):
217
225
  return [convert_unifi_data(v, field) for v in value]
218
- if shape == SHAPE_SET and isinstance(value, list):
226
+ if shape == SHAPE_SET_V1 and isinstance(value, list):
219
227
  return {convert_unifi_data(v, field) for v in value}
220
- if shape == SHAPE_DICT and isinstance(value, dict):
228
+ if shape == SHAPE_DICT_V1 and isinstance(value, dict):
221
229
  return {k: convert_unifi_data(v, field) for k, v in value.items()}
222
230
 
223
231
  if value is not None:
@@ -298,9 +306,7 @@ def serialize_dict(data: dict[str, Any], levels: int = -1) -> dict[str, Any]:
298
306
 
299
307
  def serialize_coord(coord: CoordType) -> int | float:
300
308
  """Serializes UFP zone coordinate"""
301
- from uiprotect.data import Percent
302
-
303
- if not isinstance(coord, Percent):
309
+ if not isinstance(coord, float):
304
310
  return coord
305
311
 
306
312
  if math.isclose(coord, 0) or math.isclose(coord, 1):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: uiprotect
3
- Version: 6.8.0
3
+ Version: 7.0.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
@@ -29,7 +29,8 @@ Requires-Dist: packaging (>=23)
29
29
  Requires-Dist: pillow (>=10)
30
30
  Requires-Dist: platformdirs (>=4)
31
31
  Requires-Dist: propcache (>=0.0.0)
32
- Requires-Dist: pydantic (>=1.10.17)
32
+ Requires-Dist: pydantic (>=2.10.0)
33
+ Requires-Dist: pydantic-extra-types (>=2.10.1)
33
34
  Requires-Dist: pyjwt (>=2.6)
34
35
  Requires-Dist: rich (>=10)
35
36
  Requires-Dist: typer (>=0.12.3)
@@ -4,7 +4,7 @@ uiprotect/_compat.py,sha256=HThmb1zQZCEssCxYYbQzFhJq8zYYlVaSnIEZabKc-6U,302
4
4
  uiprotect/api.py,sha256=pSQ_C2cjtttnFnrVzjmHlqS7zOfMH-bwk4NxbraUCKI,68850
5
5
  uiprotect/cli/__init__.py,sha256=1MO8rJmjjAsfVx2x01gn5DJo8B64xdPGo6gRVJbWd18,8868
6
6
  uiprotect/cli/backup.py,sha256=ZiS7RZnJGKI8TJKLW2cOUzkRM8nyTvE5Ov_jZZGtvSM,36708
7
- uiprotect/cli/base.py,sha256=k-_qGuNT7br0iV0KE5F4wYXF75iyLLjBEckTqxC71xM,7591
7
+ uiprotect/cli/base.py,sha256=GVHQMrI3thQ-4ixJlunTCfEMF90xCnt-bvRPMDupDss,7588
8
8
  uiprotect/cli/cameras.py,sha256=YvvMccQEYG3Wih0Ix8tan1R1vfaJ6cogg6YKWLzMUV8,16973
9
9
  uiprotect/cli/chimes.py,sha256=XANn21bQVkestkKOm9HjxSM8ZGrRrqvUXLouaQ3LTqs,5326
10
10
  uiprotect/cli/doorlocks.py,sha256=Go_Tn68bAcmrRAnUIi4kBiR7ciKQsu_R150ubPTjUAs,3523
@@ -15,13 +15,13 @@ uiprotect/cli/nvr.py,sha256=TwxEg2XT8jXAbOqv6gc7KFXELKadeItEDYweSL4_-e8,4260
15
15
  uiprotect/cli/sensors.py,sha256=fQtcDJCVxs4VbAqcavgBy2ABiVxAW3GXtna6_XFBp2k,8153
16
16
  uiprotect/cli/viewers.py,sha256=2cyrp104ffIvgT0wYGIO0G35QMkEbFe7fSVqLwDXQYQ,2171
17
17
  uiprotect/data/__init__.py,sha256=OcfuJl2qXfHcj_mdnrHhzZ5tEIZrw8auziX5IE7dn-I,2938
18
- uiprotect/data/base.py,sha256=sn7IHKQN96uiZL6ImN1gdCHV97EpUmy-X7xWTUAtWsg,35054
19
- uiprotect/data/bootstrap.py,sha256=j7bVRWwc3zI6gjn30LA_N5rqQbUYu6yxdy8RmrmCuSc,23224
18
+ uiprotect/data/base.py,sha256=ggW3Jdzld6Je0827RIqtFQDzVIFXCWxqTPvzSchCcA0,35353
19
+ uiprotect/data/bootstrap.py,sha256=66D4ssZiXIehn3nDRqzDbMO1LE_eJwxy0Ch8ZuhBvio,23233
20
20
  uiprotect/data/convert.py,sha256=CDPkSMxSEhvDigmzmLFKpjrz0oa5FOvOdkNIHZrOZ4Q,2586
21
- uiprotect/data/devices.py,sha256=P5U47i_YMpvD0jaWiDYbFz_mZt_Wiop_ssWGWnEMySU,113489
22
- uiprotect/data/nvr.py,sha256=FGI0eIAyy3Zy9kaxcr67HxwaVCUU8wq3oZyWvoDq7Sg,47251
23
- uiprotect/data/types.py,sha256=8fEf-fdm_Fqrfb3zwiJJle0ten-Rv3_0Vwk5uimf_vk,18571
24
- uiprotect/data/user.py,sha256=4rDMUPo02LhoVGfDjwXB9NASL4RnZjP3pvqtwjudZeE,10398
21
+ uiprotect/data/devices.py,sha256=Brj9bT9oJPb5jqAY5vc2JhFG6m7DJt8u58qVe-oTw6M,113803
22
+ uiprotect/data/nvr.py,sha256=E18DgE0nXl9VZ_ULotTPcXSi3M1u3mWQsuZbY1gIajs,47490
23
+ uiprotect/data/types.py,sha256=YCRqY5aMSFNX-X-3eqsHnUO5s1ZbPjKc5o2-eH12E34,19086
24
+ uiprotect/data/user.py,sha256=wd0MZPrDftRm2ZuTZHZvSYTmPdzyGMPszwjkbVD2iOA,10458
25
25
  uiprotect/data/websocket.py,sha256=m4EV1Qfh08eKOihy70ycViYgEQpeNSGZQJWdtGIYJDA,6791
26
26
  uiprotect/exceptions.py,sha256=kgn0cRM6lTtgLza09SDa3ZiX6ue1QqHCOogQ4qu6KTQ,965
27
27
  uiprotect/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -29,10 +29,10 @@ uiprotect/release_cache.json,sha256=NamnSFy78hOWY0DPO87J9ELFCAN6NnVquv8gQO75ZG4,
29
29
  uiprotect/stream.py,sha256=MWiTRFIhUfFLPA_csSrKl5-SkUbPZ2VhDu0XW2oVr-U,4800
30
30
  uiprotect/test_util/__init__.py,sha256=Ky8mTL61nhp5II2mxTKBAsSGvNqK8U_CfKC5AGwToAI,18704
31
31
  uiprotect/test_util/anonymize.py,sha256=f-8ijU-_y9r-uAbhIPn0f0I6hzJpAkvJzc8UpWihObI,8478
32
- uiprotect/utils.py,sha256=jIWT7n_reL90oY91svBfQ4naRxo28qHzP5jNOL12mQE,20342
32
+ uiprotect/utils.py,sha256=0q5Y0E_hxMjXqRjdo_TEheMhfLzH3_4okvHJfg6Tj-A,20475
33
33
  uiprotect/websocket.py,sha256=tEyenqblNXHcjWYuf4oRP1E7buNwx6zoECMwpBr-jig,8191
34
- uiprotect-6.8.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
35
- uiprotect-6.8.0.dist-info/METADATA,sha256=8FkmORQLIos0mSeuDePD0xTl_CfGdDiE68a1VNTpb9o,11096
36
- uiprotect-6.8.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
37
- uiprotect-6.8.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
38
- uiprotect-6.8.0.dist-info/RECORD,,
34
+ uiprotect-7.0.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
35
+ uiprotect-7.0.0.dist-info/METADATA,sha256=qjmkFG7U_AqAvBZ-8uYAJfBAe4TPWk0eMpo-iyp2YEc,11142
36
+ uiprotect-7.0.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
37
+ uiprotect-7.0.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
38
+ uiprotect-7.0.0.dist-info/RECORD,,