uiprotect 7.0.1__py3-none-any.whl → 7.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of uiprotect might be problematic. Click here for more details.

uiprotect/api.py CHANGED
@@ -57,7 +57,7 @@ from .data import (
57
57
  create_from_unifi_dict,
58
58
  )
59
59
  from .data.base import ProtectModelWithId
60
- from .data.devices import Chime
60
+ from .data.devices import AiPort, Chime
61
61
  from .data.types import IteratorCallback, ProgressCallback
62
62
  from .exceptions import BadRequest, NotAuthorized, NvrError
63
63
  from .utils import (
@@ -1268,6 +1268,14 @@ class ProtectApiClient(BaseApiClient):
1268
1268
  """
1269
1269
  return cast(list[Chime], await self.get_devices(ModelType.CHIME, Chime))
1270
1270
 
1271
+ async def get_aiports(self) -> list[AiPort]:
1272
+ """
1273
+ Gets the list of aiports straight from the NVR.
1274
+
1275
+ The websocket is connected and running, you likely just want to use `self.bootstrap.aiports`
1276
+ """
1277
+ return cast(list[AiPort], await self.get_devices(ModelType.AIPORT, AiPort))
1278
+
1271
1279
  async def get_viewers(self) -> list[Viewer]:
1272
1280
  """
1273
1281
  Gets the list of viewers straight from the NVR.
@@ -1386,6 +1394,14 @@ class ProtectApiClient(BaseApiClient):
1386
1394
  """
1387
1395
  return cast(Chime, await self.get_device(ModelType.CHIME, device_id, Chime))
1388
1396
 
1397
+ async def get_aiport(self, device_id: str) -> AiPort:
1398
+ """
1399
+ Gets a AiPort straight from the NVR.
1400
+
1401
+ The websocket is connected and running, you likely just want to use `self.bootstrap.aiport[device_id]`
1402
+ """
1403
+ return cast(AiPort, await self.get_device(ModelType.AIPORT, device_id, AiPort))
1404
+
1389
1405
  async def get_viewer(self, device_id: str) -> Viewer:
1390
1406
  """
1391
1407
  Gets a viewer straight from the NVR.
uiprotect/cli/__init__.py CHANGED
@@ -17,6 +17,7 @@ from ..data import Version, WSPacket
17
17
  from ..test_util import SampleDataGenerator
18
18
  from ..utils import RELEASE_CACHE, get_local_timezone, run_async
19
19
  from ..utils import profile_ws as profile_ws_job
20
+ from .aiports import app as aiports_app
20
21
  from .base import CliContext, OutputFormatEnum
21
22
  from .cameras import app as camera_app
22
23
  from .chimes import app as chime_app
@@ -128,6 +129,7 @@ app.add_typer(doorlock_app, name="doorlocks")
128
129
  app.add_typer(light_app, name="lights")
129
130
  app.add_typer(sensor_app, name="sensors")
130
131
  app.add_typer(viewer_app, name="viewers")
132
+ app.add_typer(aiports_app, name="aiports")
131
133
 
132
134
  if backup_app is not None:
133
135
  app.add_typer(backup_app, name="backup")
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+ import typer
7
+
8
+ from ..api import ProtectApiClient
9
+ from ..cli import base
10
+ from ..data import AiPort
11
+
12
+ app = typer.Typer(rich_markup_mode="rich")
13
+
14
+ ARG_DEVICE_ID = typer.Argument(
15
+ None, help="ID of AiPort device to select for subcommands"
16
+ )
17
+
18
+
19
+ @dataclass
20
+ class AiPortContext(base.CliContext):
21
+ devices: dict[str, AiPort]
22
+ device: AiPort | None = None
23
+
24
+
25
+ ALL_COMMANDS, DEVICE_COMMANDS = base.init_common_commands(app)
26
+
27
+
28
+ @app.callback(invoke_without_command=True)
29
+ def main(ctx: typer.Context, device_id: Optional[str] = ARG_DEVICE_ID) -> None:
30
+ """
31
+ AiPort device CLI.
32
+
33
+ Returns full list of AiPorts without any arguments passed.
34
+ """
35
+ protect: ProtectApiClient = ctx.obj.protect
36
+ context = AiPortContext(
37
+ protect=ctx.obj.protect,
38
+ device=None,
39
+ devices=protect.bootstrap.aiports,
40
+ output_format=ctx.obj.output_format,
41
+ )
42
+ ctx.obj = context
43
+
44
+ if device_id is not None and device_id not in ALL_COMMANDS:
45
+ if (device := protect.bootstrap.aiports.get(device_id)) is None:
46
+ typer.secho("Invalid aiport ID", fg="red")
47
+ raise typer.Exit(1)
48
+ ctx.obj.device = device
49
+
50
+ if not ctx.invoked_subcommand:
51
+ if device_id in ALL_COMMANDS:
52
+ ctx.invoke(ALL_COMMANDS[device_id], ctx)
53
+ return
54
+
55
+ if ctx.obj.device is not None:
56
+ base.print_unifi_obj(ctx.obj.device, ctx.obj.output_format)
57
+ return
58
+
59
+ base.print_unifi_dict(ctx.obj.devices)
@@ -10,6 +10,7 @@ from .base import (
10
10
  from .bootstrap import Bootstrap
11
11
  from .convert import create_from_unifi_dict
12
12
  from .devices import (
13
+ AiPort,
13
14
  Bridge,
14
15
  Camera,
15
16
  CameraChannel,
@@ -85,6 +86,7 @@ __all__ = [
85
86
  "DEFAULT_TYPE",
86
87
  "NVR",
87
88
  "WS_HEADER_SIZE",
89
+ "AiPort",
88
90
  "AnalyticsOption",
89
91
  "AudioStyle",
90
92
  "Bootstrap",
uiprotect/data/base.py CHANGED
@@ -137,10 +137,12 @@ class ProtectBaseObject(BaseModel):
137
137
  data.pop("api", None)
138
138
  return cls(api=api, **data)
139
139
 
140
- return cls.construct(**data)
140
+ return cls.model_construct(**data)
141
141
 
142
142
  @classmethod
143
- def construct(cls, _fields_set: set[str] | None = None, **values: Any) -> Self:
143
+ def model_construct(
144
+ cls, _fields_set: set[str] | None = None, **values: Any
145
+ ) -> Self:
144
146
  api: ProtectApiClient | None = values.pop("api", None)
145
147
  (
146
148
  unifi_objs,
@@ -152,19 +154,21 @@ class ProtectBaseObject(BaseModel):
152
154
  ) = cls._get_protect_model()
153
155
  for key, value in values.items():
154
156
  if has_unifi_objs and key in unifi_objs and isinstance(value, dict):
155
- values[key] = unifi_objs[key].construct(**value)
157
+ values[key] = unifi_objs[key].model_construct(**value)
156
158
  elif has_unifi_lists and key in unifi_lists and isinstance(value, list):
157
159
  values[key] = [
158
- unifi_lists[key].construct(**v) if isinstance(v, dict) else v
160
+ unifi_lists[key].model_construct(**v) if isinstance(v, dict) else v
159
161
  for v in value
160
162
  ]
161
163
  elif has_unifi_dicts and key in unifi_dicts and isinstance(value, dict):
162
164
  values[key] = {
163
- k: unifi_dicts[key].construct(**v) if isinstance(v, dict) else v
165
+ k: unifi_dicts[key].model_construct(**v)
166
+ if isinstance(v, dict)
167
+ else v
164
168
  for k, v in value.items()
165
169
  }
166
170
 
167
- obj = super().construct(_fields_set=_fields_set, **values)
171
+ obj = super().model_construct(_fields_set=_fields_set, **values)
168
172
  if api is not None:
169
173
  obj._api = api
170
174
 
@@ -369,7 +373,7 @@ class ProtectBaseObject(BaseModel):
369
373
  if isinstance(value, ProtectBaseObject):
370
374
  value = value.unifi_dict()
371
375
  elif isinstance(value, dict):
372
- value = klass.construct({}).unifi_dict(data=value) # type: ignore[arg-type]
376
+ value = klass.model_construct({}).unifi_dict(data=value) # type: ignore[arg-type]
373
377
 
374
378
  return value
375
379
 
@@ -390,7 +394,7 @@ class ProtectBaseObject(BaseModel):
390
394
  return [
391
395
  item.unifi_dict()
392
396
  if isinstance(item, ProtectBaseObject)
393
- else klass.construct({}).unifi_dict(data=item) # type: ignore[arg-type]
397
+ else klass.model_construct({}).unifi_dict(data=item) # type: ignore[arg-type]
394
398
  for item in value
395
399
  ]
396
400
 
@@ -587,9 +591,11 @@ class ProtectModelWithId(ProtectModel):
587
591
  self._update_sync = update_sync or UpdateSynchronization()
588
592
 
589
593
  @classmethod
590
- def construct(cls, _fields_set: set[str] | None = None, **values: Any) -> Self:
594
+ def model_construct(
595
+ cls, _fields_set: set[str] | None = None, **values: Any
596
+ ) -> Self:
591
597
  update_sync = values.pop("update_sync", None)
592
- obj = super().construct(_fields_set=_fields_set, **values)
598
+ obj = super().model_construct(_fields_set=_fields_set, **values)
593
599
  obj._update_sync = update_sync or UpdateSynchronization()
594
600
  return obj
595
601
 
@@ -23,6 +23,7 @@ from .base import (
23
23
  )
24
24
  from .convert import MODEL_TO_CLASS, create_from_unifi_dict
25
25
  from .devices import (
26
+ AiPort,
26
27
  Bridge,
27
28
  Camera,
28
29
  Chime,
@@ -181,6 +182,7 @@ class Bootstrap(ProtectBaseObject):
181
182
  sensors: dict[str, Sensor]
182
183
  doorlocks: dict[str, Doorlock]
183
184
  chimes: dict[str, Chime]
185
+ aiports: dict[str, AiPort]
184
186
  last_update_id: str
185
187
 
186
188
  # TODO:
@@ -426,7 +428,7 @@ class Bootstrap(ProtectBaseObject):
426
428
  if updated_obj is None:
427
429
  return None
428
430
 
429
- old_obj = updated_obj.copy()
431
+ old_obj = updated_obj.model_copy()
430
432
  updated_data = {to_snake_case(k): v for k, v in data.items()}
431
433
  updated_obj.update_from_dict(updated_data)
432
434
 
@@ -461,7 +463,7 @@ class Bootstrap(ProtectBaseObject):
461
463
  if not (data := self.nvr.unifi_dict_to_dict(data)):
462
464
  return None
463
465
 
464
- old_nvr = self.nvr.copy()
466
+ old_nvr = self.nvr.model_copy()
465
467
  self.nvr = self.nvr.update_from_dict(data)
466
468
 
467
469
  return WSSubscriptionMessage(
@@ -517,7 +519,7 @@ class Bootstrap(ProtectBaseObject):
517
519
  # nothing left to process
518
520
  return None
519
521
 
520
- old_obj = obj.copy()
522
+ old_obj = obj.model_copy()
521
523
  obj = obj.update_from_dict(data)
522
524
 
523
525
  if model_type is ModelType.EVENT:
uiprotect/data/convert.py CHANGED
@@ -8,6 +8,7 @@ from uiprotect.data.base import ProtectModelWithId
8
8
 
9
9
  from ..exceptions import DataDecodeError
10
10
  from .devices import (
11
+ AiPort,
11
12
  Bridge,
12
13
  Camera,
13
14
  Chime,
@@ -40,6 +41,7 @@ MODEL_TO_CLASS: dict[str, type[ProtectModel]] = {
40
41
  ModelType.SENSOR: Sensor,
41
42
  ModelType.DOORLOCK: Doorlock,
42
43
  ModelType.CHIME: Chime,
44
+ ModelType.AIPORT: AiPort,
43
45
  ModelType.KEYRING: Keyring,
44
46
  ModelType.ULP_USER: UlpUser,
45
47
  }
uiprotect/data/devices.py CHANGED
@@ -976,6 +976,8 @@ class Camera(ProtectMotionDeviceModel):
976
976
  audio_settings: CameraAudioSettings | None = None
977
977
  # requires 5.0.33+
978
978
  is_third_party_camera: bool | None = None
979
+ # requires 5.1.78+
980
+ is_paired_with_ai_port: bool | None = None
979
981
  # TODO: used for adopting
980
982
  # apMac read only
981
983
  # apRssi read only
@@ -3382,3 +3384,7 @@ class Chime(ProtectAdoptableDeviceModel):
3382
3384
  raise BadRequest("Camera %s is not paired with chime", camera.id)
3383
3385
 
3384
3386
  await self.queue_update(callback)
3387
+
3388
+
3389
+ class AiPort(Camera):
3390
+ paired_cameras: list[str]
uiprotect/data/types.py CHANGED
@@ -124,6 +124,7 @@ class ModelType(str, UnknownValuesEnumMixin, enum.Enum):
124
124
  DOORLOCK = "doorlock"
125
125
  SCHEDULE = "schedule"
126
126
  CHIME = "chime"
127
+ AIPORT = "aiport"
127
128
  DEVICE_GROUP = "deviceGroup"
128
129
  RECORDING_SCHEDULE = "recordingSchedule"
129
130
  ULP_USER = "ulpUser"
@@ -173,6 +174,7 @@ class ModelType(str, UnknownValuesEnumMixin, enum.Enum):
173
174
  ModelType.SENSOR,
174
175
  ModelType.DOORLOCK,
175
176
  ModelType.CHIME,
177
+ ModelType.AIPORT,
176
178
  )
177
179
 
178
180
  @classmethod
@@ -131,6 +131,7 @@ class SampleDataGenerator:
131
131
  "sensor": len(bootstrap["sensors"]),
132
132
  "doorlock": len(bootstrap["doorlocks"]),
133
133
  "chime": len(bootstrap["chimes"]),
134
+ "aiport": len(bootstrap["aiports"]),
134
135
  }
135
136
 
136
137
  self.log("Generating event data...")
@@ -283,6 +284,7 @@ class SampleDataGenerator:
283
284
  self.generate_sensor_data(),
284
285
  self.generate_lock_data(),
285
286
  self.generate_chime_data(),
287
+ self.generate_aiport_data(),
286
288
  self.generate_bridge_data(),
287
289
  self.generate_liveview_data(),
288
290
  )
@@ -469,6 +471,21 @@ class SampleDataGenerator:
469
471
  obj = await self.client.api_request_obj(f"chimes/{device_id}")
470
472
  await self.write_json_file("sample_chime", obj)
471
473
 
474
+ async def generate_aiport_data(self) -> None:
475
+ objs = await self.client.api_request_list("aiports")
476
+ device_id: str | None = None
477
+ for obj_dict in objs:
478
+ device_id = obj_dict["id"]
479
+ if is_online(obj_dict):
480
+ break
481
+
482
+ if device_id is None:
483
+ self.log("No aiport found. Skipping aiport endpoints...")
484
+ return
485
+
486
+ obj = await self.client.api_request_obj(f"aiports/{device_id}")
487
+ await self.write_json_file("sample_aiport", obj)
488
+
472
489
  async def generate_bridge_data(self) -> None:
473
490
  objs = await self.client.api_request_list("bridges")
474
491
  device_id: str | None = None
uiprotect/utils.py CHANGED
@@ -572,7 +572,7 @@ def local_datetime(dt: datetime | None = None) -> datetime:
572
572
  def log_event(event: Event) -> None:
573
573
  from uiprotect.data import EventType
574
574
 
575
- _LOGGER.debug("event WS msg: %s", event.dict())
575
+ _LOGGER.debug("event WS msg: %s", event.model_dump())
576
576
  if "smart" not in event.type.value:
577
577
  return
578
578
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: uiprotect
3
- Version: 7.0.1
3
+ Version: 7.1.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,8 +1,9 @@
1
1
  uiprotect/__init__.py,sha256=Oz6i1tonIz4QWVnEPkbielJDJ3WQdwZVgYtjY4IwGAQ,636
2
2
  uiprotect/__main__.py,sha256=C_bHCOkv5qj6WMy-6ELoY3Y6HDhLxOa1a30CzmbZhsg,462
3
3
  uiprotect/_compat.py,sha256=HThmb1zQZCEssCxYYbQzFhJq8zYYlVaSnIEZabKc-6U,302
4
- uiprotect/api.py,sha256=qj6qEKLY3j25kNTL7Jlo-wwreOdmFzcV_MZxkNo7Oeg,69038
5
- uiprotect/cli/__init__.py,sha256=1MO8rJmjjAsfVx2x01gn5DJo8B64xdPGo6gRVJbWd18,8868
4
+ uiprotect/api.py,sha256=EW_2s8FSxhwFykl4Vw_YhhjzC2AZVAZBDL-ha9yUBqg,69690
5
+ uiprotect/cli/__init__.py,sha256=gHo9G2WusvrWgnHhecT8Q2NLRXG0VS-rc9TKy4V5Kw8,8951
6
+ uiprotect/cli/aiports.py,sha256=wpEr2w_hY18CGpFiQM2Yc0FiVwG_1l2CzZhZLGNigvI,1576
6
7
  uiprotect/cli/backup.py,sha256=ZiS7RZnJGKI8TJKLW2cOUzkRM8nyTvE5Ov_jZZGtvSM,36708
7
8
  uiprotect/cli/base.py,sha256=GVHQMrI3thQ-4ixJlunTCfEMF90xCnt-bvRPMDupDss,7588
8
9
  uiprotect/cli/cameras.py,sha256=YvvMccQEYG3Wih0Ix8tan1R1vfaJ6cogg6YKWLzMUV8,16973
@@ -14,25 +15,25 @@ uiprotect/cli/liveviews.py,sha256=GU5z-ZLRBXHyspDKiJpiv-kbaBcvxK_-K70rPoqx2Ms,18
14
15
  uiprotect/cli/nvr.py,sha256=TwxEg2XT8jXAbOqv6gc7KFXELKadeItEDYweSL4_-e8,4260
15
16
  uiprotect/cli/sensors.py,sha256=fQtcDJCVxs4VbAqcavgBy2ABiVxAW3GXtna6_XFBp2k,8153
16
17
  uiprotect/cli/viewers.py,sha256=2cyrp104ffIvgT0wYGIO0G35QMkEbFe7fSVqLwDXQYQ,2171
17
- uiprotect/data/__init__.py,sha256=OcfuJl2qXfHcj_mdnrHhzZ5tEIZrw8auziX5IE7dn-I,2938
18
- uiprotect/data/base.py,sha256=ggW3Jdzld6Je0827RIqtFQDzVIFXCWxqTPvzSchCcA0,35353
19
- uiprotect/data/bootstrap.py,sha256=66D4ssZiXIehn3nDRqzDbMO1LE_eJwxy0Ch8ZuhBvio,23233
20
- uiprotect/data/convert.py,sha256=CDPkSMxSEhvDigmzmLFKpjrz0oa5FOvOdkNIHZrOZ4Q,2586
21
- uiprotect/data/devices.py,sha256=Brj9bT9oJPb5jqAY5vc2JhFG6m7DJt8u58qVe-oTw6M,113803
18
+ uiprotect/data/__init__.py,sha256=audwJBjxRiYdNPeYlP6iofFIOq3gyQzh6VpDsOCM2dQ,2964
19
+ uiprotect/data/base.py,sha256=LRqmK60PKeDFgP2k5qpVj93AxEvdfC6kLk0Cqvt1W5k,35481
20
+ uiprotect/data/bootstrap.py,sha256=2X5_ejlu1FnoL7RTeIJeR38yQQFMGzZlxN9l7l2fo8g,23294
21
+ uiprotect/data/convert.py,sha256=xEN878_hm0HZZCVYGwJSxcSp2as9zpkvsemVIibReOA,2628
22
+ uiprotect/data/devices.py,sha256=Ht9ANMG4TRxz6wYKYuWlTx8uGxGJJKMHa_PJRZ7g6HQ,113927
22
23
  uiprotect/data/nvr.py,sha256=E18DgE0nXl9VZ_ULotTPcXSi3M1u3mWQsuZbY1gIajs,47490
23
- uiprotect/data/types.py,sha256=YCRqY5aMSFNX-X-3eqsHnUO5s1ZbPjKc5o2-eH12E34,19086
24
+ uiprotect/data/types.py,sha256=r09nuG3ZIsgld4Uo7fyBz_Nd6I4wNfyhfViX5ldp_LI,19138
24
25
  uiprotect/data/user.py,sha256=Del5LUmt5uCfAQMI9-kl_GaKm085oTLjxmcCrlEKXxc,10526
25
26
  uiprotect/data/websocket.py,sha256=m4EV1Qfh08eKOihy70ycViYgEQpeNSGZQJWdtGIYJDA,6791
26
27
  uiprotect/exceptions.py,sha256=kgn0cRM6lTtgLza09SDa3ZiX6ue1QqHCOogQ4qu6KTQ,965
27
28
  uiprotect/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
29
  uiprotect/release_cache.json,sha256=NamnSFy78hOWY0DPO87J9ELFCAN6NnVquv8gQO75ZG4,386
29
30
  uiprotect/stream.py,sha256=MWiTRFIhUfFLPA_csSrKl5-SkUbPZ2VhDu0XW2oVr-U,4800
30
- uiprotect/test_util/__init__.py,sha256=Ky8mTL61nhp5II2mxTKBAsSGvNqK8U_CfKC5AGwToAI,18704
31
+ uiprotect/test_util/__init__.py,sha256=HlQBgIgdtrvT-gQ5OWP92LbgVr_YzsD5NFImLRonUZk,19320
31
32
  uiprotect/test_util/anonymize.py,sha256=f-8ijU-_y9r-uAbhIPn0f0I6hzJpAkvJzc8UpWihObI,8478
32
- uiprotect/utils.py,sha256=0q5Y0E_hxMjXqRjdo_TEheMhfLzH3_4okvHJfg6Tj-A,20475
33
+ uiprotect/utils.py,sha256=q2YYQfxr0b3QEWMSP7SdpcJZbB4huEhpo8PbtnTbEFI,20481
33
34
  uiprotect/websocket.py,sha256=tEyenqblNXHcjWYuf4oRP1E7buNwx6zoECMwpBr-jig,8191
34
- uiprotect-7.0.1.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
35
- uiprotect-7.0.1.dist-info/METADATA,sha256=6Mdae5G8Tclyu6WKduDgQUgKEqXtmiSJuVdRqYZJcTk,11142
36
- uiprotect-7.0.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
37
- uiprotect-7.0.1.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
38
- uiprotect-7.0.1.dist-info/RECORD,,
35
+ uiprotect-7.1.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
36
+ uiprotect-7.1.0.dist-info/METADATA,sha256=W8UXVUWhcruyMXTU9ajSMwJ_cuOj8_MLfHf-M1hRAGk,11142
37
+ uiprotect-7.1.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
38
+ uiprotect-7.1.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
39
+ uiprotect-7.1.0.dist-info/RECORD,,