uiprotect 3.8.0__py3-none-any.whl → 7.32.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.
uiprotect/data/types.py CHANGED
@@ -1,19 +1,52 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import enum
4
- from collections.abc import Callable, Coroutine
5
- from functools import cache, cached_property
6
- from typing import Any, Literal, Optional, TypeVar, Union
4
+ import types
5
+ from collections.abc import Callable, Coroutine, Sequence
6
+ from functools import cache, lru_cache
7
+ from typing import Annotated, Any, Literal, TypeVar, Union, get_args, get_origin
7
8
 
8
9
  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
10
+ from pydantic import BaseModel, Field
11
+ from pydantic.types import StringConstraints
12
+ from pydantic_extra_types.color import Color # noqa: F401
13
+
14
+ from .._compat import cached_property
12
15
 
13
16
  KT = TypeVar("KT")
14
17
  VT = TypeVar("VT")
15
18
 
16
19
 
20
+ @lru_cache(maxsize=512)
21
+ def get_field_type(annotation: type[Any] | None) -> tuple[type | None, Any]:
22
+ """Extract the origin and type from an annotation."""
23
+ if annotation is None:
24
+ raise ValueError("Type annotation cannot be None")
25
+ origin = get_origin(annotation)
26
+ args: Sequence[Any]
27
+ if origin in (list, set):
28
+ if not (args := get_args(annotation)):
29
+ raise ValueError(f"Unable to determine args of type: {annotation}")
30
+ return origin, args[0]
31
+ if origin is dict:
32
+ if not (args := get_args(annotation)):
33
+ raise ValueError(f"Unable to determine args of type: {annotation}")
34
+ return origin, args[1]
35
+ if origin is Annotated:
36
+ if not (args := get_args(annotation)):
37
+ raise ValueError(f"Unable to determine args of type: {annotation}")
38
+ return None, args[0]
39
+ if origin is Union or origin is types.UnionType:
40
+ if not (args := get_args(annotation)):
41
+ raise ValueError(f"Unable to determine args of type: {annotation}")
42
+ args = [get_field_type(arg) for arg in args]
43
+ if len(args) == 2 and type(None) in list(zip(*args, strict=False))[1]:
44
+ # Strip '| None' type from Union
45
+ return next(arg for arg in args if arg[1] is not type(None))
46
+ return None, annotation
47
+ return origin, annotation
48
+
49
+
17
50
  DEFAULT = "DEFAULT_VALUE"
18
51
  DEFAULT_TYPE = Literal["DEFAULT_VALUE"]
19
52
  EventCategories = Literal[
@@ -27,7 +60,7 @@ EventCategories = Literal[
27
60
  ]
28
61
 
29
62
  ProgressCallback = Callable[[int, int, int], Coroutine[Any, Any, None]]
30
- IteratorCallback = Callable[[int, Optional[bytes]], Coroutine[Any, Any, None]]
63
+ IteratorCallback = Callable[[int, bytes | None], Coroutine[Any, Any, None]]
31
64
 
32
65
 
33
66
  class FixSizeOrderedDict(dict[KT, VT]):
@@ -49,6 +82,11 @@ class ValuesEnumMixin:
49
82
  _values: list[str] | None = None
50
83
  _values_normalized: dict[str, str] | None = None
51
84
 
85
+ @classmethod
86
+ @cache
87
+ def from_string(cls, value: str) -> Any:
88
+ return cls(value) # type: ignore[call-arg]
89
+
52
90
  @classmethod
53
91
  @cache
54
92
  def values(cls) -> list[str]:
@@ -96,8 +134,12 @@ class ModelType(str, UnknownValuesEnumMixin, enum.Enum):
96
134
  DOORLOCK = "doorlock"
97
135
  SCHEDULE = "schedule"
98
136
  CHIME = "chime"
137
+ AIPORT = "aiport"
99
138
  DEVICE_GROUP = "deviceGroup"
100
139
  RECORDING_SCHEDULE = "recordingSchedule"
140
+ ULP_USER = "ulpUser"
141
+ RINGTONE = "ringtone"
142
+ KEYRING = "keyring"
101
143
  UNKNOWN = "unknown"
102
144
 
103
145
  bootstrap_model_types: tuple[ModelType, ...]
@@ -111,6 +153,16 @@ class ModelType(str, UnknownValuesEnumMixin, enum.Enum):
111
153
  """Return the devices key."""
112
154
  return f"{self.value}s"
113
155
 
156
+ @cached_property
157
+ def name(self) -> str:
158
+ """Return the name."""
159
+ return self._name_
160
+
161
+ @cached_property
162
+ def value(self) -> str:
163
+ """Return the value."""
164
+ return self._value_
165
+
114
166
  @classmethod
115
167
  @cache
116
168
  def from_string(cls, value: str) -> ModelType:
@@ -133,6 +185,7 @@ class ModelType(str, UnknownValuesEnumMixin, enum.Enum):
133
185
  ModelType.SENSOR,
134
186
  ModelType.DOORLOCK,
135
187
  ModelType.CHIME,
188
+ ModelType.AIPORT,
136
189
  )
137
190
 
138
191
  @classmethod
@@ -184,6 +237,8 @@ class EventType(str, ValuesEnumMixin, enum.Enum):
184
237
  POOR_CONNECTION = "poorConnection"
185
238
  STREAM_RECOVERY = "streamRecovery"
186
239
  MOTION = "motion"
240
+ NFC_CARD_SCANNED = "nfcCardScanned"
241
+ FINGERPRINT_IDENTIFIED = "fingerprintIdentified"
187
242
  RECORDING_DELETED = "recordingDeleted"
188
243
  SMART_AUDIO_DETECT = "smartAudioDetect"
189
244
  SMART_DETECT = "smartDetectZone"
@@ -257,8 +312,12 @@ class EventType(str, ValuesEnumMixin, enum.Enum):
257
312
  def device_events() -> list[str]:
258
313
  return [
259
314
  EventType.MOTION.value,
315
+ EventType.NFC_CARD_SCANNED.value,
316
+ EventType.FINGERPRINT_IDENTIFIED.value,
260
317
  EventType.RING.value,
261
318
  EventType.SMART_DETECT.value,
319
+ EventType.SMART_AUDIO_DETECT.value,
320
+ EventType.SMART_DETECT_LINE.value,
262
321
  ]
263
322
 
264
323
  @staticmethod
@@ -386,6 +445,8 @@ class VideoMode(str, ValuesEnumMixin, enum.Enum):
386
445
  HOMEKIT = "homekit"
387
446
  SPORT = "sport"
388
447
  SLOW_SHUTTER = "slowShutter"
448
+ LPR_NONE_REFLEX = "lprNoneReflex"
449
+ LPR_REFLEX = "lprReflex"
389
450
  # should only be for unadopted devices
390
451
  UNKNOWN = "unknown"
391
452
 
@@ -398,6 +459,7 @@ class AudioStyle(str, UnknownValuesEnumMixin, enum.Enum):
398
459
 
399
460
  @enum.unique
400
461
  class RecordingMode(str, ValuesEnumMixin, enum.Enum):
462
+ ADAPTIVE = "adaptive"
401
463
  ALWAYS = "always"
402
464
  NEVER = "never"
403
465
  SCHEDULE = "schedule"
@@ -475,6 +537,7 @@ class SleepStateType(str, ValuesEnumMixin, enum.Enum):
475
537
  class AutoExposureMode(str, ValuesEnumMixin, enum.Enum):
476
538
  MANUAL = "manual"
477
539
  AUTO = "auto"
540
+ NONE = "none"
478
541
  SHUTTER = "shutter"
479
542
  FLICK50 = "flick50"
480
543
  FLICK60 = "flick60"
@@ -484,6 +547,7 @@ class AutoExposureMode(str, ValuesEnumMixin, enum.Enum):
484
547
  class FocusMode(str, ValuesEnumMixin, enum.Enum):
485
548
  MANUAL = "manual"
486
549
  AUTO = "auto"
550
+ NONE = "none"
487
551
  ZTRIG = "ztrig"
488
552
  TOUCH = "touch"
489
553
 
@@ -575,9 +639,20 @@ class PermissionNode(str, UnknownValuesEnumMixin, enum.Enum):
575
639
  READ_LIVE = "readlive"
576
640
  UNKNOWN = "unknown"
577
641
 
642
+ @cached_property
643
+ def name(self) -> str:
644
+ """Return the name."""
645
+ return self._name_
646
+
647
+ @cached_property
648
+ def value(self) -> str:
649
+ """Return the value."""
650
+ return self._value_
651
+
578
652
 
579
653
  @enum.unique
580
654
  class HDRMode(str, UnknownValuesEnumMixin, enum.Enum):
655
+ NONE = "none"
581
656
  NORMAL = "normal"
582
657
  ALWAYS_ON = "superHdr"
583
658
 
@@ -591,64 +666,33 @@ class LensType(str, enum.Enum):
591
666
  DLSR_17 = "m43"
592
667
 
593
668
 
594
- class DoorbellText(ConstrainedStr):
595
- max_length = 30
596
-
597
-
598
- class ICRCustomValue(ConstrainedInt):
599
- ge = 0
600
- le = 10
601
-
602
-
603
- class ICRLuxValue(ConstrainedInt):
604
- ge = 1
605
- le = 30
606
-
607
-
608
- class LEDLevel(ConstrainedInt):
609
- ge = 0
610
- le = 6
611
-
669
+ DoorbellText = Annotated[str, StringConstraints(max_length=30)]
612
670
 
613
- class PercentInt(ConstrainedInt):
614
- ge = 0
615
- le = 100
671
+ ICRCustomValue = Annotated[int, Field(ge=0, le=11)]
616
672
 
673
+ ICRLuxValue = Annotated[int, Field(ge=1, le=30)]
617
674
 
618
- class TwoByteInt(ConstrainedInt):
619
- ge = 1
620
- le = 255
675
+ LEDLevel = Annotated[int, Field(ge=0, le=6)]
621
676
 
677
+ PercentInt = Annotated[int, Field(ge=0, le=101)]
622
678
 
623
- class PercentFloat(ConstrainedFloat):
624
- ge = 0
625
- le = 100
679
+ TwoByteInt = Annotated[int, Field(ge=1, le=256)]
626
680
 
681
+ PercentFloat = Annotated[float, Field(ge=0, le=100)]
627
682
 
628
- class WDRLevel(ConstrainedInt):
629
- ge = 0
630
- le = 3
683
+ WDRLevel = Annotated[int, Field(ge=0, le=4)]
631
684
 
685
+ ICRSensitivity = Annotated[int, Field(ge=0, le=4)]
632
686
 
633
- class ICRSensitivity(ConstrainedInt):
634
- ge = 0
635
- le = 3
687
+ Percent = Annotated[float, Field(ge=0, le=1)]
636
688
 
637
-
638
- class Percent(ConstrainedFloat):
639
- ge = 0
640
- le = 1
641
-
642
-
643
- class RepeatTimes(ConstrainedInt):
644
- ge = 1
645
- le = 6
689
+ RepeatTimes = Annotated[int, Field(ge=1, le=6)]
646
690
 
647
691
 
648
692
  class PTZPositionDegree(BaseModel):
649
693
  pan: float
650
694
  tilt: float
651
- zoom: int
695
+ zoom: float
652
696
 
653
697
 
654
698
  class PTZPositionSteps(BaseModel):
@@ -679,15 +723,6 @@ class PTZPreset(BaseModel):
679
723
  CoordType = Union[Percent, int, float]
680
724
 
681
725
 
682
- # TODO: fix when upgrading to pydantic v2
683
- class Color(BaseColor):
684
- def __eq__(self, o: object) -> bool:
685
- if isinstance(o, Color):
686
- return self.as_hex() == o.as_hex()
687
-
688
- return super().__eq__(o)
689
-
690
-
691
726
  class Version(BaseVersion):
692
727
  def __str__(self) -> str:
693
728
  super_str = super().__str__()
uiprotect/data/user.py CHANGED
@@ -2,21 +2,28 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import sys
6
+ from abc import abstractmethod
5
7
  from datetime import datetime
6
8
  from functools import cache
7
- from typing import Any
9
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
8
10
 
9
- from pydantic.v1.fields import PrivateAttr
11
+ from pydantic.fields import PrivateAttr
10
12
 
11
13
  from .base import ProtectBaseObject, ProtectModel, ProtectModelWithId
12
14
  from .types import ModelType, PermissionNode
13
15
 
16
+ if sys.version_info >= (3, 11):
17
+ from typing import Self
18
+ else:
19
+ from typing_extensions import Self
20
+
14
21
 
15
22
  class Permission(ProtectBaseObject):
16
23
  raw_permission: str
17
24
  model: ModelType
18
25
  nodes: set[PermissionNode]
19
- obj_ids: set[str] | None
26
+ obj_ids: set[str] | None = None
20
27
 
21
28
  @classmethod
22
29
  def unifi_dict_to_dict(cls, data: dict[str, Any]) -> dict[str, Any]:
@@ -72,8 +79,8 @@ class Group(ProtectModelWithId):
72
79
 
73
80
  class UserLocation(ProtectModel):
74
81
  is_away: bool
75
- latitude: float | None
76
- longitude: float | None
82
+ latitude: float | None = None
83
+ longitude: float | None = None
77
84
 
78
85
 
79
86
  class CloudAccount(ProtectModelWithId):
@@ -82,7 +89,7 @@ class CloudAccount(ProtectModelWithId):
82
89
  email: str
83
90
  user_id: str
84
91
  name: str
85
- location: UserLocation | None
92
+ location: UserLocation | None = None
86
93
  profile_img: str | None = None
87
94
 
88
95
  @classmethod
@@ -116,21 +123,21 @@ class UserFeatureFlags(ProtectBaseObject):
116
123
 
117
124
  class User(ProtectModelWithId):
118
125
  permissions: list[Permission]
119
- last_login_ip: str | None
120
- last_login_time: datetime | None
126
+ last_login_ip: str | None = None
127
+ last_login_time: datetime | None = None
121
128
  is_owner: bool
122
129
  enable_notifications: bool
123
130
  has_accepted_invite: bool
124
131
  all_permissions: list[Permission]
125
132
  scopes: list[str] | None = None
126
- location: UserLocation | None
133
+ location: UserLocation | None = None
127
134
  name: str
128
135
  first_name: str
129
136
  last_name: str
130
- email: str | None
137
+ email: str | None = None
131
138
  local_username: str
132
139
  group_ids: list[str]
133
- cloud_account: CloudAccount | None
140
+ cloud_account: CloudAccount | None = None
134
141
  feature_flags: UserFeatureFlags
135
142
 
136
143
  # TODO:
@@ -210,7 +217,7 @@ class User(ProtectModelWithId):
210
217
  ) -> bool:
211
218
  """Checks if a user can do a specific action"""
212
219
  check_self = False
213
- if model == self.model and obj is not None and obj.id == self.id:
220
+ if model is self.model and obj is not None and obj.id == self.id:
214
221
  perm_str = f"{model.value}:{node.value}:$"
215
222
  check_self = True
216
223
  else:
@@ -221,7 +228,7 @@ class User(ProtectModelWithId):
221
228
  return self._perm_cache[perm_str]
222
229
 
223
230
  for perm in self.all_permissions:
224
- if model != perm.model or node not in perm.nodes:
231
+ if model is not perm.model or node not in perm.nodes:
225
232
  continue
226
233
  if perm.obj_ids is None:
227
234
  self._perm_cache[perm_str] = True
@@ -234,3 +241,115 @@ class User(ProtectModelWithId):
234
241
  return True
235
242
  self._perm_cache[perm_str] = False
236
243
  return False
244
+
245
+
246
+ T = TypeVar("T", bound="ProtectModelWithId")
247
+
248
+
249
+ class UlpUserKeyringBase(Generic[T]):
250
+ """Base class for collections of ULP users and keyrings."""
251
+
252
+ def __init__(self) -> None:
253
+ self._id_to_item: dict[str, T] = {}
254
+
255
+ def __len__(self) -> int:
256
+ return len(self._id_to_item)
257
+
258
+ @classmethod
259
+ def from_list(cls, items: list[T]) -> Self:
260
+ instance = cls()
261
+ for item in items:
262
+ instance.add(item)
263
+ return instance
264
+
265
+ def add(self, item: T) -> None:
266
+ """Add an item to the collection."""
267
+ self._id_to_item[item.id] = item
268
+
269
+ def remove(self, item: T) -> None:
270
+ """Remove an item from the collection."""
271
+ self._id_to_item.pop(item.id, None)
272
+
273
+ def by_id(self, item_id: str) -> T | None:
274
+ """Retrieve an item by its ID."""
275
+ return self._id_to_item.get(item_id)
276
+
277
+ @abstractmethod
278
+ def by_ulp_id(self, item_id: str) -> T | None:
279
+ """Retrieve an item by its ULP ID."""
280
+
281
+ def as_list(self) -> list[T]:
282
+ return list(self._id_to_item.values())
283
+
284
+ def __eq__(self, other: Any) -> bool:
285
+ if TYPE_CHECKING:
286
+ assert isinstance(other, UlpUserKeyringBase)
287
+ return self._id_to_item == other._id_to_item
288
+
289
+
290
+ class Keyring(ProtectModelWithId):
291
+ device_type: str
292
+ device_id: str
293
+ registry_type: str
294
+ registry_id: str
295
+ last_activity: datetime | None = None
296
+ ulp_user: str
297
+
298
+
299
+ class Keyrings(UlpUserKeyringBase[Keyring]):
300
+ def __init__(self) -> None:
301
+ super().__init__()
302
+ self._keyrings_by_registry_id: dict[str, Keyring] = {}
303
+ self._keyrings_by_ulp_user: dict[str, Keyring] = {}
304
+
305
+ def add(self, keyring: Keyring) -> None:
306
+ super().add(keyring)
307
+ self._keyrings_by_registry_id[keyring.registry_id] = keyring
308
+ self._keyrings_by_ulp_user[keyring.ulp_user] = keyring
309
+
310
+ def remove(self, keyring: Keyring) -> None:
311
+ super().remove(keyring)
312
+ self._keyrings_by_registry_id.pop(keyring.registry_id, None)
313
+ self._keyrings_by_ulp_user.pop(keyring.ulp_user, None)
314
+
315
+ def by_ulp_id(self, ulp_id: str) -> Keyring | None:
316
+ return self._keyrings_by_ulp_user.get(ulp_id)
317
+
318
+ def by_registry_id(self, registry_id: str) -> Keyring | None:
319
+ return self._keyrings_by_registry_id.get(registry_id)
320
+
321
+ def __eq__(self, other: Any) -> bool:
322
+ if not isinstance(other, Keyrings):
323
+ return NotImplemented
324
+ return super().__eq__(other)
325
+
326
+
327
+ class UlpUser(ProtectModelWithId):
328
+ ulp_id: str
329
+ first_name: str
330
+ last_name: str
331
+ full_name: str
332
+ avatar: str
333
+ status: str
334
+
335
+
336
+ class UlpUsers(UlpUserKeyringBase[UlpUser]):
337
+ def __init__(self) -> None:
338
+ super().__init__()
339
+ self._users_by_ulp_id: dict[str, UlpUser] = {}
340
+
341
+ def add(self, user: UlpUser) -> None:
342
+ super().add(user)
343
+ self._users_by_ulp_id[user.ulp_id] = user
344
+
345
+ def remove(self, user: UlpUser) -> None:
346
+ super().remove(user)
347
+ self._users_by_ulp_id.pop(user.ulp_id, None)
348
+
349
+ def by_ulp_id(self, ulp_id: str) -> UlpUser | None:
350
+ return self._users_by_ulp_id.get(ulp_id)
351
+
352
+ def __eq__(self, other: Any) -> bool:
353
+ if not isinstance(other, UlpUsers):
354
+ return NotImplemented
355
+ return super().__eq__(other)
@@ -7,11 +7,12 @@ import enum
7
7
  import struct
8
8
  import zlib
9
9
  from dataclasses import dataclass
10
- from functools import cache, cached_property
10
+ from functools import cache
11
11
  from typing import TYPE_CHECKING, Any
12
12
 
13
13
  import orjson
14
14
 
15
+ from .._compat import cached_property
15
16
  from ..exceptions import WSDecodeError, WSEncodeError
16
17
  from .types import ProtectWSPayloadFormat
17
18
 
uiprotect/stream.py CHANGED
@@ -18,6 +18,12 @@ if TYPE_CHECKING:
18
18
 
19
19
  _LOGGER = logging.getLogger(__name__)
20
20
 
21
+ CODEC_TO_ENCODER = {
22
+ "aac": {"encoder": "aac", "format": "adts"},
23
+ "opus": {"encoder": "libopus", "format": "rtp"},
24
+ "vorbis": {"encoder": "libvorbis", "format": "ogg"},
25
+ }
26
+
21
27
 
22
28
  class FfmpegCommand:
23
29
  ffmpeg_path: Path | None
@@ -130,9 +136,10 @@ class TalkbackStream(FfmpegCommand):
130
136
  if len(input_args) > 0:
131
137
  input_args += " "
132
138
 
133
- bitrate = camera.talkback_settings.bits_per_sample * 1000
134
- # 8000 seems to result in best quality without overloading the camera
135
- udp_bitrate = bitrate + 8000
139
+ codec = camera.talkback_settings.type_fmt.value
140
+ encoder = CODEC_TO_ENCODER.get(codec)
141
+ if encoder is None:
142
+ raise ValueError(f"Unsupported codec: {codec}")
136
143
 
137
144
  # vn = no video
138
145
  # acodec = audio codec to encode output in (aac)
@@ -142,9 +149,9 @@ class TalkbackStream(FfmpegCommand):
142
149
  cmd = (
143
150
  "-loglevel info -hide_banner "
144
151
  f'{input_args}-i "{content_url}" -vn '
145
- f"-acodec {camera.talkback_settings.type_fmt.value} -ac {camera.talkback_settings.channels} "
146
- f"-ar {camera.talkback_settings.sampling_rate} -b:a {bitrate} -map 0:a "
147
- f'-f adts "udp://{camera.host}:{camera.talkback_settings.bind_port}?bitrate={udp_bitrate}"'
152
+ f"-acodec {encoder['encoder']} -ac {camera.talkback_settings.channels} "
153
+ f"-ar {camera.talkback_settings.sampling_rate} -b:a {camera.talkback_settings.sampling_rate} -map 0:a "
154
+ f'-f {encoder["format"]} "udp://{camera.host}:{camera.talkback_settings.bind_port}?bitrate={camera.talkback_settings.sampling_rate}"'
148
155
  )
149
156
 
150
157
  super().__init__(cmd, ffmpeg_path)
@@ -126,11 +126,12 @@ class SampleDataGenerator:
126
126
  "group": len(bootstrap["groups"]),
127
127
  "liveview": len(bootstrap["liveviews"]),
128
128
  "viewer": len(bootstrap["viewers"]),
129
- "light": len(bootstrap["lights"]),
130
- "bridge": len(bootstrap["bridges"]),
131
- "sensor": len(bootstrap["sensors"]),
132
- "doorlock": len(bootstrap["doorlocks"]),
133
- "chime": len(bootstrap["chimes"]),
129
+ "light": len(bootstrap.get("lights", [])),
130
+ "bridge": len(bootstrap.get("bridges", [])),
131
+ "sensor": len(bootstrap.get("sensors", [])),
132
+ "doorlock": len(bootstrap.get("doorlocks", [])),
133
+ "chime": len(bootstrap.get("chimes", [])),
134
+ "aiport": len(bootstrap.get("aiports", [])),
134
135
  }
135
136
 
136
137
  self.log("Generating event data...")
@@ -141,6 +142,7 @@ class SampleDataGenerator:
141
142
 
142
143
  if close_session:
143
144
  await self.client.close_session()
145
+ await self.client.close_public_api_session()
144
146
 
145
147
  await self.write_json_file("sample_constants", self.constants, anonymize=False)
146
148
 
@@ -283,6 +285,7 @@ class SampleDataGenerator:
283
285
  self.generate_sensor_data(),
284
286
  self.generate_lock_data(),
285
287
  self.generate_chime_data(),
288
+ self.generate_aiport_data(),
286
289
  self.generate_bridge_data(),
287
290
  self.generate_liveview_data(),
288
291
  )
@@ -306,10 +309,18 @@ class SampleDataGenerator:
306
309
  await self.write_json_file("sample_camera", deepcopy(obj))
307
310
  self.constants["camera_online"] = camera_is_online
308
311
 
312
+ # Check if camera has channels
313
+ if not obj.get("channels") or len(obj["channels"]) == 0:
314
+ self.log(
315
+ "Camera has no channels, skipping snapshot, thumbnail and heatmap generation",
316
+ )
317
+ return
318
+
309
319
  if not camera_is_online:
310
320
  self.log(
311
321
  "Camera is not online, skipping snapshot, thumbnail and heatmap generation",
312
322
  )
323
+ return
313
324
 
314
325
  # snapshot
315
326
  width = obj["channels"][0]["width"]
@@ -322,6 +333,15 @@ class SampleDataGenerator:
322
333
  snapshot = await self.client.get_camera_snapshot(obj["id"], width, height)
323
334
  await self.write_image_file(filename, snapshot)
324
335
 
336
+ # public api snapshot
337
+ pub_filename = "sample_camera_public_api_snapshot"
338
+ if self.anonymize:
339
+ self.log(f"Writing {pub_filename}...")
340
+ placeholder_image(self.output_folder / f"{pub_filename}.png", width, height)
341
+ else:
342
+ pub_snapshot = await self.client.get_public_api_camera_snapshot(obj["id"])
343
+ await self.write_image_file(pub_filename, pub_snapshot)
344
+
325
345
  async def generate_motion_data(
326
346
  self,
327
347
  motion_event: dict[str, Any] | None,
@@ -359,7 +379,7 @@ class SampleDataGenerator:
359
379
  length = int((motion_event["end"] - motion_event["start"]) / 1000)
360
380
  if self.anonymize:
361
381
  run(
362
- split( # noqa: S603
382
+ split(
363
383
  BLANK_VIDEO_CMD.format(
364
384
  length=length,
365
385
  filename=self.output_folder / f"{filename}.mp4",
@@ -440,7 +460,12 @@ class SampleDataGenerator:
440
460
  await self.write_json_file("sample_sensor", obj)
441
461
 
442
462
  async def generate_lock_data(self) -> None:
443
- objs = await self.client.api_request_list("doorlocks")
463
+ try:
464
+ objs = await self.client.api_request_list("doorlocks")
465
+ except BadRequest:
466
+ self.log("No doorlock endpoint available. Skipping doorlock endpoints...")
467
+ return
468
+
444
469
  device_id: str | None = None
445
470
  for obj_dict in objs:
446
471
  device_id = obj_dict["id"]
@@ -469,6 +494,21 @@ class SampleDataGenerator:
469
494
  obj = await self.client.api_request_obj(f"chimes/{device_id}")
470
495
  await self.write_json_file("sample_chime", obj)
471
496
 
497
+ async def generate_aiport_data(self) -> None:
498
+ objs = await self.client.api_request_list("aiports")
499
+ device_id: str | None = None
500
+ for obj_dict in objs:
501
+ device_id = obj_dict["id"]
502
+ if is_online(obj_dict):
503
+ break
504
+
505
+ if device_id is None:
506
+ self.log("No aiport found. Skipping aiport endpoints...")
507
+ return
508
+
509
+ obj = await self.client.api_request_obj(f"aiports/{device_id}")
510
+ await self.write_json_file("sample_aiport", obj)
511
+
472
512
  async def generate_bridge_data(self) -> None:
473
513
  objs = await self.client.api_request_list("bridges")
474
514
  device_id: str | None = None
@@ -3,11 +3,10 @@ from __future__ import annotations
3
3
  import secrets
4
4
  import string
5
5
  import uuid
6
+ import warnings
6
7
  from typing import Any
7
8
  from urllib.parse import urlparse
8
9
 
9
- import typer
10
-
11
10
  from ..data import ModelType
12
11
 
13
12
  object_id_mapping: dict[str, str] = {}
@@ -72,7 +71,7 @@ def anonymize_user(user_dict: dict[str, Any]) -> dict[str, Any]:
72
71
  return user_dict
73
72
 
74
73
 
75
- def anonymize_value(value: Any, name: str | None = None) -> Any:
74
+ def anonymize_value(value: Any, name: str | None = None) -> Any: # noqa: PLR0912
76
75
  if isinstance(value, str):
77
76
  if name == "accessKey":
78
77
  value = f"{random_number(13)}:{random_hex(24)}:{random_hex(128)}"
@@ -105,13 +104,13 @@ def anonymize_value(value: Any, name: str | None = None) -> Any:
105
104
  return value
106
105
 
107
106
 
108
- def anonymize_dict(obj: dict[str, Any], name: str | None = None) -> dict[str, Any]:
107
+ def anonymize_dict(obj: dict[str, Any], name: str | None = None) -> dict[str, Any]: # noqa: PLR0912
109
108
  obj_type = None
110
109
  if "modelKey" in obj:
111
110
  if obj["modelKey"] in [m.value for m in ModelType]:
112
111
  obj_type = ModelType(obj["modelKey"])
113
112
  else:
114
- typer.secho(f"Unknown modelKey: {obj['modelKey']}", fg="yellow")
113
+ warnings.warn(f"Unknown modelKey: {obj['modelKey']}", stacklevel=2)
115
114
 
116
115
  if obj_type == ModelType.USER:
117
116
  return anonymize_user(obj)