uiprotect 7.0.2__tar.gz → 7.1.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 (38) hide show
  1. {uiprotect-7.0.2 → uiprotect-7.1.0}/PKG-INFO +1 -1
  2. {uiprotect-7.0.2 → uiprotect-7.1.0}/pyproject.toml +2 -2
  3. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/api.py +17 -1
  4. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/cli/__init__.py +2 -0
  5. uiprotect-7.1.0/src/uiprotect/cli/aiports.py +59 -0
  6. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/data/__init__.py +2 -0
  7. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/data/bootstrap.py +2 -0
  8. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/data/convert.py +2 -0
  9. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/data/devices.py +6 -0
  10. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/data/types.py +2 -0
  11. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/test_util/__init__.py +17 -0
  12. {uiprotect-7.0.2 → uiprotect-7.1.0}/LICENSE +0 -0
  13. {uiprotect-7.0.2 → uiprotect-7.1.0}/README.md +0 -0
  14. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/__init__.py +0 -0
  15. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/__main__.py +0 -0
  16. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/_compat.py +0 -0
  17. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/cli/backup.py +0 -0
  18. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/cli/base.py +0 -0
  19. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/cli/cameras.py +0 -0
  20. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/cli/chimes.py +0 -0
  21. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/cli/doorlocks.py +0 -0
  22. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/cli/events.py +0 -0
  23. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/cli/lights.py +0 -0
  24. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/cli/liveviews.py +0 -0
  25. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/cli/nvr.py +0 -0
  26. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/cli/sensors.py +0 -0
  27. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/cli/viewers.py +0 -0
  28. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/data/base.py +0 -0
  29. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/data/nvr.py +0 -0
  30. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/data/user.py +0 -0
  31. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/data/websocket.py +0 -0
  32. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/exceptions.py +0 -0
  33. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/py.typed +0 -0
  34. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/release_cache.json +0 -0
  35. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/stream.py +0 -0
  36. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/test_util/anonymize.py +0 -0
  37. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/utils.py +0 -0
  38. {uiprotect-7.0.2 → uiprotect-7.1.0}/src/uiprotect/websocket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: uiprotect
3
- Version: 7.0.2
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,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "uiprotect"
3
- version = "7.0.2"
3
+ version = "7.1.0"
4
4
  description = "Python API for Unifi Protect (Unofficial)"
5
5
  authors = ["UI Protect Maintainers <ui@koston.org>"]
6
6
  readme = "README.md"
@@ -54,7 +54,7 @@ pytest = ">=7,<9"
54
54
  pytest-cov = ">=3,<7"
55
55
  aiosqlite = ">=0.20.0"
56
56
  asttokens = ">=2.4.1,<4.0.0"
57
- pytest-asyncio = ">=0.23.7,<0.25.0"
57
+ pytest-asyncio = ">=0.23.7,<0.26.0"
58
58
  pytest-benchmark = ">=4,<6"
59
59
  pytest-sugar = "^1.0.0"
60
60
  pytest-timeout = "^2.3.1"
@@ -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.
@@ -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",
@@ -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:
@@ -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
  }
@@ -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]
@@ -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
File without changes
File without changes