uiprotect 7.5.2__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/api.py +886 -59
- uiprotect/cli/__init__.py +109 -24
- uiprotect/cli/aiports.py +1 -2
- uiprotect/cli/backup.py +5 -4
- uiprotect/cli/base.py +4 -4
- uiprotect/cli/cameras.py +152 -13
- uiprotect/cli/chimes.py +5 -6
- uiprotect/cli/doorlocks.py +2 -3
- uiprotect/cli/events.py +7 -8
- uiprotect/cli/lights.py +11 -3
- uiprotect/cli/liveviews.py +1 -2
- uiprotect/cli/sensors.py +2 -3
- uiprotect/cli/viewers.py +2 -3
- uiprotect/data/base.py +32 -32
- uiprotect/data/bootstrap.py +20 -15
- uiprotect/data/devices.py +183 -16
- uiprotect/data/nvr.py +139 -38
- uiprotect/data/types.py +32 -19
- uiprotect/stream.py +13 -2
- uiprotect/test_util/__init__.py +30 -7
- uiprotect/test_util/anonymize.py +4 -5
- uiprotect/utils.py +56 -24
- uiprotect/websocket.py +3 -3
- {uiprotect-7.5.2.dist-info → uiprotect-7.32.0.dist-info}/METADATA +70 -17
- uiprotect-7.32.0.dist-info/RECORD +39 -0
- {uiprotect-7.5.2.dist-info → uiprotect-7.32.0.dist-info}/WHEEL +1 -1
- uiprotect-7.5.2.dist-info/RECORD +0 -39
- {uiprotect-7.5.2.dist-info → uiprotect-7.32.0.dist-info}/entry_points.txt +0 -0
- {uiprotect-7.5.2.dist-info → uiprotect-7.32.0.dist-info/licenses}/LICENSE +0 -0
uiprotect/cli/__init__.py
CHANGED
|
@@ -3,19 +3,21 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import base64
|
|
5
5
|
import logging
|
|
6
|
+
import ssl
|
|
6
7
|
import sys
|
|
7
8
|
from pathlib import Path
|
|
8
|
-
from typing import
|
|
9
|
+
from typing import cast
|
|
9
10
|
|
|
11
|
+
import aiohttp
|
|
10
12
|
import orjson
|
|
11
13
|
import typer
|
|
12
14
|
from rich.progress import track
|
|
13
15
|
|
|
14
|
-
from uiprotect.api import ProtectApiClient
|
|
16
|
+
from uiprotect.api import MetaInfo, ProtectApiClient
|
|
15
17
|
|
|
16
|
-
from ..data import
|
|
18
|
+
from ..data import WSPacket
|
|
17
19
|
from ..test_util import SampleDataGenerator
|
|
18
|
-
from ..utils import
|
|
20
|
+
from ..utils import get_local_timezone, run_async
|
|
19
21
|
from ..utils import profile_ws as profile_ws_job
|
|
20
22
|
from .aiports import app as aiports_app
|
|
21
23
|
from .base import CliContext, OutputFormatEnum
|
|
@@ -60,6 +62,13 @@ OPTION_PASSWORD = typer.Option(
|
|
|
60
62
|
hide_input=True,
|
|
61
63
|
envvar="UFP_PASSWORD",
|
|
62
64
|
)
|
|
65
|
+
OPTION_API_KEY = typer.Option(
|
|
66
|
+
None,
|
|
67
|
+
"--api-key",
|
|
68
|
+
"-k",
|
|
69
|
+
help="UniFi Protect API key (required for public API operations)",
|
|
70
|
+
envvar="UFP_API_KEY",
|
|
71
|
+
)
|
|
63
72
|
OPTION_ADDRESS = typer.Option(
|
|
64
73
|
...,
|
|
65
74
|
"--address",
|
|
@@ -76,10 +85,10 @@ OPTION_PORT = typer.Option(
|
|
|
76
85
|
envvar="UFP_PORT",
|
|
77
86
|
)
|
|
78
87
|
OPTION_SECONDS = typer.Option(15, "--seconds", "-s", help="Seconds to pull events")
|
|
79
|
-
|
|
88
|
+
OPTION_VERIFY_SSL = typer.Option(
|
|
80
89
|
True,
|
|
81
|
-
"--no-verify",
|
|
82
|
-
help="Verify SSL",
|
|
90
|
+
"--verify-ssl/--no-verify-ssl",
|
|
91
|
+
help="Verify SSL certificate. Disable for self-signed certificates.",
|
|
83
92
|
envvar="UFP_SSL_VERIFY",
|
|
84
93
|
)
|
|
85
94
|
OPTION_ANON = typer.Option(True, "--actual", help="Do not anonymize test data")
|
|
@@ -135,14 +144,36 @@ if backup_app is not None:
|
|
|
135
144
|
app.add_typer(backup_app, name="backup")
|
|
136
145
|
|
|
137
146
|
|
|
147
|
+
def _is_ssl_error(exc: BaseException) -> bool:
|
|
148
|
+
"""Check if an exception is an SSL certificate verification error."""
|
|
149
|
+
if isinstance(exc, aiohttp.ClientConnectorCertificateError):
|
|
150
|
+
return True
|
|
151
|
+
if isinstance(exc, aiohttp.ClientConnectorSSLError):
|
|
152
|
+
return True
|
|
153
|
+
if isinstance(exc, ssl.SSLCertVerificationError):
|
|
154
|
+
return True
|
|
155
|
+
# Check nested exceptions
|
|
156
|
+
if exc.__cause__ is not None:
|
|
157
|
+
return _is_ssl_error(exc.__cause__)
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def _connect_and_bootstrap(protect: ProtectApiClient) -> None:
|
|
162
|
+
"""Connect to the Protect API and fetch bootstrap data."""
|
|
163
|
+
protect._bootstrap = await protect.get_bootstrap()
|
|
164
|
+
await protect.close_session()
|
|
165
|
+
await protect.close_public_api_session()
|
|
166
|
+
|
|
167
|
+
|
|
138
168
|
@app.callback()
|
|
139
169
|
def main(
|
|
140
170
|
ctx: typer.Context,
|
|
141
171
|
username: str = OPTION_USERNAME,
|
|
142
172
|
password: str = OPTION_PASSWORD,
|
|
173
|
+
api_key: str | None = OPTION_API_KEY,
|
|
143
174
|
address: str = OPTION_ADDRESS,
|
|
144
175
|
port: int = OPTION_PORT,
|
|
145
|
-
|
|
176
|
+
verify_ssl: bool = OPTION_VERIFY_SSL,
|
|
146
177
|
output_format: OutputFormatEnum = OPTION_OUT_FORMAT,
|
|
147
178
|
include_unadopted: bool = OPTION_UNADOPTED,
|
|
148
179
|
) -> None:
|
|
@@ -155,15 +186,51 @@ def main(
|
|
|
155
186
|
port,
|
|
156
187
|
username,
|
|
157
188
|
password,
|
|
158
|
-
|
|
189
|
+
api_key,
|
|
190
|
+
verify_ssl=verify_ssl,
|
|
159
191
|
ignore_unadopted=not include_unadopted,
|
|
160
192
|
)
|
|
161
193
|
|
|
162
|
-
async def
|
|
163
|
-
|
|
194
|
+
async def close_protect() -> None:
|
|
195
|
+
"""Close the Protect API client sessions."""
|
|
164
196
|
await protect.close_session()
|
|
197
|
+
await protect.close_public_api_session()
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
run_async(_connect_and_bootstrap(protect))
|
|
201
|
+
except Exception as exc:
|
|
202
|
+
# Always close the session on error to avoid "Unclosed client session" warning
|
|
203
|
+
run_async(close_protect())
|
|
204
|
+
|
|
205
|
+
if verify_ssl and _is_ssl_error(exc):
|
|
206
|
+
typer.secho(
|
|
207
|
+
"SSL certificate verification failed. "
|
|
208
|
+
"This is common with self-signed certificates on UniFi devices.",
|
|
209
|
+
fg="yellow",
|
|
210
|
+
)
|
|
211
|
+
if typer.confirm("Would you like to disable SSL verification and retry?"):
|
|
212
|
+
# Create new client with SSL disabled
|
|
213
|
+
protect = ProtectApiClient(
|
|
214
|
+
address,
|
|
215
|
+
port,
|
|
216
|
+
username,
|
|
217
|
+
password,
|
|
218
|
+
api_key,
|
|
219
|
+
verify_ssl=False,
|
|
220
|
+
ignore_unadopted=not include_unadopted,
|
|
221
|
+
)
|
|
222
|
+
run_async(_connect_and_bootstrap(protect))
|
|
223
|
+
typer.secho(
|
|
224
|
+
"Connected successfully with SSL verification disabled.\n"
|
|
225
|
+
"Tip: Use --no-verify-ssl to skip this prompt in the future.",
|
|
226
|
+
fg="green",
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
typer.secho("Connection aborted.", fg="red")
|
|
230
|
+
raise typer.Exit(code=1) from exc
|
|
231
|
+
else:
|
|
232
|
+
raise
|
|
165
233
|
|
|
166
|
-
run_async(update())
|
|
167
234
|
ctx.obj = CliContext(protect=protect, output_format=output_format)
|
|
168
235
|
|
|
169
236
|
|
|
@@ -222,7 +289,7 @@ def generate_sample_data(
|
|
|
222
289
|
ctx: typer.Context,
|
|
223
290
|
anonymize: bool = OPTION_ANON,
|
|
224
291
|
wait_time: int = OPTION_WAIT,
|
|
225
|
-
output_folder:
|
|
292
|
+
output_folder: Path | None = OPTION_OUTPUT,
|
|
226
293
|
do_zip: bool = OPTION_ZIP,
|
|
227
294
|
) -> None:
|
|
228
295
|
"""Generates sample data for UniFi Protect instance."""
|
|
@@ -258,7 +325,7 @@ def generate_sample_data(
|
|
|
258
325
|
def profile_ws(
|
|
259
326
|
ctx: typer.Context,
|
|
260
327
|
wait_time: int = OPTION_WAIT,
|
|
261
|
-
output_path:
|
|
328
|
+
output_path: Path | None = OPTION_OUTPUT,
|
|
262
329
|
) -> None:
|
|
263
330
|
"""Profiles Websocket messages for UniFi Protect instance."""
|
|
264
331
|
protect = cast(ProtectApiClient, ctx.obj.protect)
|
|
@@ -275,6 +342,7 @@ def profile_ws(
|
|
|
275
342
|
unsub()
|
|
276
343
|
await protect.async_disconnect_ws()
|
|
277
344
|
await protect.close_session()
|
|
345
|
+
await protect.close_public_api_session()
|
|
278
346
|
|
|
279
347
|
_setup_logger()
|
|
280
348
|
|
|
@@ -284,7 +352,7 @@ def profile_ws(
|
|
|
284
352
|
@app.command()
|
|
285
353
|
def decode_ws_msg(
|
|
286
354
|
ws_file: typer.FileBinaryRead = OPTION_WS_FILE,
|
|
287
|
-
ws_data:
|
|
355
|
+
ws_data: str | None = ARG_WS_DATA,
|
|
288
356
|
) -> None:
|
|
289
357
|
"""Decodes a base64 encoded UniFi Protect Websocket binary message."""
|
|
290
358
|
if ws_file is None and ws_data is None: # type: ignore[unreachable]
|
|
@@ -304,19 +372,36 @@ def decode_ws_msg(
|
|
|
304
372
|
|
|
305
373
|
|
|
306
374
|
@app.command()
|
|
307
|
-
def
|
|
308
|
-
|
|
375
|
+
def create_api_key(
|
|
376
|
+
ctx: typer.Context,
|
|
377
|
+
name: str = typer.Argument(..., help="Name for the API key"),
|
|
378
|
+
) -> None:
|
|
379
|
+
"""Create a new API key for the current user."""
|
|
309
380
|
protect = cast(ProtectApiClient, ctx.obj.protect)
|
|
310
381
|
|
|
311
|
-
async def callback() ->
|
|
312
|
-
|
|
382
|
+
async def callback() -> str:
|
|
383
|
+
api_key = await protect.create_api_key(name)
|
|
313
384
|
await protect.close_session()
|
|
314
|
-
|
|
385
|
+
await protect.close_public_api_session()
|
|
386
|
+
return api_key
|
|
315
387
|
|
|
316
388
|
_setup_logger()
|
|
389
|
+
result = run_async(callback())
|
|
390
|
+
typer.echo(result)
|
|
317
391
|
|
|
318
|
-
versions = run_async(callback())
|
|
319
|
-
output = orjson.dumps(sorted([str(v) for v in versions]))
|
|
320
392
|
|
|
321
|
-
|
|
322
|
-
|
|
393
|
+
@app.command()
|
|
394
|
+
def get_meta_info(ctx: typer.Context) -> None:
|
|
395
|
+
"""Get metadata about the current UniFi Protect instance."""
|
|
396
|
+
protect = cast(ProtectApiClient, ctx.obj.protect)
|
|
397
|
+
|
|
398
|
+
async def callback() -> MetaInfo:
|
|
399
|
+
meta = await protect.get_meta_info()
|
|
400
|
+
await protect.close_session()
|
|
401
|
+
await protect.close_public_api_session()
|
|
402
|
+
return meta
|
|
403
|
+
|
|
404
|
+
_setup_logger()
|
|
405
|
+
|
|
406
|
+
result = run_async(callback())
|
|
407
|
+
typer.echo(result.model_dump_json())
|
uiprotect/cli/aiports.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
import typer
|
|
7
6
|
|
|
@@ -26,7 +25,7 @@ ALL_COMMANDS, DEVICE_COMMANDS = base.init_common_commands(app)
|
|
|
26
25
|
|
|
27
26
|
|
|
28
27
|
@app.callback(invoke_without_command=True)
|
|
29
|
-
def main(ctx: typer.Context, device_id:
|
|
28
|
+
def main(ctx: typer.Context, device_id: str | None = ARG_DEVICE_ID) -> None:
|
|
30
29
|
"""
|
|
31
30
|
AiPort device CLI.
|
|
32
31
|
|
uiprotect/cli/backup.py
CHANGED
|
@@ -10,7 +10,7 @@ from dataclasses import dataclass
|
|
|
10
10
|
from datetime import datetime, timedelta, timezone
|
|
11
11
|
from enum import Enum
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import TYPE_CHECKING, Any,
|
|
13
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
14
14
|
|
|
15
15
|
import aiofiles
|
|
16
16
|
import aiofiles.os as aos
|
|
@@ -396,9 +396,9 @@ def _setup_logger(verbose: bool) -> None:
|
|
|
396
396
|
@app.callback()
|
|
397
397
|
def main(
|
|
398
398
|
ctx: typer.Context,
|
|
399
|
-
start:
|
|
400
|
-
end:
|
|
401
|
-
output_folder:
|
|
399
|
+
start: str | None = OPTION_START,
|
|
400
|
+
end: str | None = OPTION_END,
|
|
401
|
+
output_folder: Path | None = OPTION_OUTPUT,
|
|
402
402
|
thumbnail_format: str = OPTION_THUMBNAIL_FORMAT,
|
|
403
403
|
gif_format: str = OPTION_GIF_FORMAT,
|
|
404
404
|
event_format: str = OPTION_EVENT_FORMAT,
|
|
@@ -1064,6 +1064,7 @@ async def _events(
|
|
|
1064
1064
|
finally:
|
|
1065
1065
|
_LOGGER.debug("Cleaning up Protect connection/database...")
|
|
1066
1066
|
await ctx.protect.close_session()
|
|
1067
|
+
await ctx.protect.close_public_api_session()
|
|
1067
1068
|
await ctx.db_engine.dispose()
|
|
1068
1069
|
|
|
1069
1070
|
|
uiprotect/cli/base.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from collections.abc import Callable, Coroutine, Mapping, Sequence
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from enum import Enum
|
|
6
|
-
from typing import Any,
|
|
6
|
+
from typing import Any, TypeVar
|
|
7
7
|
|
|
8
8
|
import orjson
|
|
9
9
|
import typer
|
|
@@ -36,6 +36,7 @@ def run(ctx: typer.Context, func: Coroutine[Any, Any, T]) -> T:
|
|
|
36
36
|
async def callback() -> T:
|
|
37
37
|
return_value = await func
|
|
38
38
|
await ctx.obj.protect.close_session()
|
|
39
|
+
await ctx.obj.protect.close_public_api_session()
|
|
39
40
|
return return_value
|
|
40
41
|
|
|
41
42
|
try:
|
|
@@ -165,7 +166,7 @@ def set_ssh(ctx: typer.Context, enabled: bool) -> None:
|
|
|
165
166
|
run(ctx, obj.set_ssh(enabled))
|
|
166
167
|
|
|
167
168
|
|
|
168
|
-
def set_name(ctx: typer.Context, name:
|
|
169
|
+
def set_name(ctx: typer.Context, name: str | None = typer.Argument(None)) -> None:
|
|
169
170
|
"""Sets name for the device"""
|
|
170
171
|
require_device_id(ctx)
|
|
171
172
|
obj: NVR | ProtectAdoptableDeviceModel = ctx.obj.device
|
|
@@ -203,7 +204,7 @@ def unadopt(ctx: typer.Context, force: bool = OPTION_FORCE) -> None:
|
|
|
203
204
|
run(ctx, obj.unadopt())
|
|
204
205
|
|
|
205
206
|
|
|
206
|
-
def adopt(ctx: typer.Context, name:
|
|
207
|
+
def adopt(ctx: typer.Context, name: str | None = typer.Argument(None)) -> None:
|
|
207
208
|
"""
|
|
208
209
|
Adopts a device.
|
|
209
210
|
|
|
@@ -223,7 +224,6 @@ def init_common_commands(
|
|
|
223
224
|
device_commands: dict[str, Callable[..., Any]] = {}
|
|
224
225
|
|
|
225
226
|
deviceless_commands["list-ids"] = app.command()(list_ids)
|
|
226
|
-
device_commands["protect-url"] = app.command()(protect_url)
|
|
227
227
|
device_commands["is-wired"] = app.command()(is_wired)
|
|
228
228
|
device_commands["is-wifi"] = app.command()(is_wifi)
|
|
229
229
|
device_commands["is-bluetooth"] = app.command()(is_bluetooth)
|
uiprotect/cli/cameras.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import cast
|
|
7
7
|
|
|
8
8
|
import typer
|
|
9
9
|
from rich.progress import Progress
|
|
@@ -27,7 +27,7 @@ ALL_COMMANDS, DEVICE_COMMANDS = base.init_common_commands(app)
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
@app.callback(invoke_without_command=True)
|
|
30
|
-
def main(ctx: typer.Context, device_id:
|
|
30
|
+
def main(ctx: typer.Context, device_id: str | None = ARG_DEVICE_ID) -> None:
|
|
31
31
|
"""
|
|
32
32
|
Camera device CLI.
|
|
33
33
|
|
|
@@ -74,7 +74,7 @@ def timelapse_url(ctx: typer.Context) -> None:
|
|
|
74
74
|
@app.command()
|
|
75
75
|
def privacy_mode(
|
|
76
76
|
ctx: typer.Context,
|
|
77
|
-
enabled:
|
|
77
|
+
enabled: bool | None = typer.Argument(None),
|
|
78
78
|
) -> None:
|
|
79
79
|
"""
|
|
80
80
|
Returns/sets library managed privacy mode.
|
|
@@ -91,7 +91,7 @@ def privacy_mode(
|
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
@app.command()
|
|
94
|
-
def chime_type(ctx: typer.Context, value:
|
|
94
|
+
def chime_type(ctx: typer.Context, value: d.ChimeType | None = None) -> None:
|
|
95
95
|
"""Returns/sets the current chime type if the camera has a chime."""
|
|
96
96
|
base.require_device_id(ctx)
|
|
97
97
|
obj: d.Camera = ctx.obj.device
|
|
@@ -137,9 +137,9 @@ def stream_urls(ctx: typer.Context) -> None:
|
|
|
137
137
|
def save_snapshot(
|
|
138
138
|
ctx: typer.Context,
|
|
139
139
|
output_path: Path = typer.Argument(..., help="JPEG format"),
|
|
140
|
-
width:
|
|
141
|
-
height:
|
|
142
|
-
dt:
|
|
140
|
+
width: int | None = typer.Option(None, "-w", "--width"),
|
|
141
|
+
height: int | None = typer.Option(None, "-h", "--height"),
|
|
142
|
+
dt: datetime | None = typer.Option(None, "-t", "--timestamp"),
|
|
143
143
|
package: bool = typer.Option(False, "-p", "--package", help="Get package camera"),
|
|
144
144
|
) -> None:
|
|
145
145
|
"""
|
|
@@ -188,7 +188,7 @@ def save_video(
|
|
|
188
188
|
max=3,
|
|
189
189
|
help="0 = High, 1 = Medium, 2 = Low, 3 = Package",
|
|
190
190
|
),
|
|
191
|
-
fps:
|
|
191
|
+
fps: int | None = typer.Option(
|
|
192
192
|
None,
|
|
193
193
|
"--fps",
|
|
194
194
|
min=1,
|
|
@@ -245,7 +245,7 @@ def save_video(
|
|
|
245
245
|
def play_audio(
|
|
246
246
|
ctx: typer.Context,
|
|
247
247
|
url: str = typer.Argument(..., help="ffmpeg playable URL"),
|
|
248
|
-
ffmpeg_path:
|
|
248
|
+
ffmpeg_path: Path | None = typer.Option(
|
|
249
249
|
None,
|
|
250
250
|
"--ffmpeg-path",
|
|
251
251
|
help="Path to ffmpeg executable",
|
|
@@ -487,13 +487,37 @@ def set_speaker_volume(
|
|
|
487
487
|
ctx: typer.Context,
|
|
488
488
|
level: int = typer.Argument(..., min=0, max=100),
|
|
489
489
|
) -> None:
|
|
490
|
-
"""Sets the speaker
|
|
490
|
+
"""Sets the speaker output volume on camera"""
|
|
491
491
|
base.require_device_id(ctx)
|
|
492
492
|
obj: d.Camera = ctx.obj.device
|
|
493
493
|
|
|
494
494
|
base.run(ctx, obj.set_speaker_volume(level))
|
|
495
495
|
|
|
496
496
|
|
|
497
|
+
@app.command()
|
|
498
|
+
def set_volume(
|
|
499
|
+
ctx: typer.Context,
|
|
500
|
+
level: int = typer.Argument(..., min=0, max=100),
|
|
501
|
+
) -> None:
|
|
502
|
+
"""Sets the general volume level on camera"""
|
|
503
|
+
base.require_device_id(ctx)
|
|
504
|
+
obj: d.Camera = ctx.obj.device
|
|
505
|
+
|
|
506
|
+
base.run(ctx, obj.set_volume(level))
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
@app.command()
|
|
510
|
+
def set_ring_volume(
|
|
511
|
+
ctx: typer.Context,
|
|
512
|
+
level: int = typer.Argument(..., min=0, max=100),
|
|
513
|
+
) -> None:
|
|
514
|
+
"""Sets the doorbell ring volume"""
|
|
515
|
+
base.require_device_id(ctx)
|
|
516
|
+
obj: d.Camera = ctx.obj.device
|
|
517
|
+
|
|
518
|
+
base.run(ctx, obj.set_ring_volume(level))
|
|
519
|
+
|
|
520
|
+
|
|
497
521
|
@app.command()
|
|
498
522
|
def set_system_sounds(ctx: typer.Context, enabled: bool) -> None:
|
|
499
523
|
"""Sets system sound playback through speakers"""
|
|
@@ -542,15 +566,15 @@ def set_osd_bitrate(ctx: typer.Context, enabled: bool) -> None:
|
|
|
542
566
|
@app.command()
|
|
543
567
|
def set_lcd_text(
|
|
544
568
|
ctx: typer.Context,
|
|
545
|
-
text_type:
|
|
569
|
+
text_type: d.DoorbellMessageType | None = typer.Argument(
|
|
546
570
|
None,
|
|
547
571
|
help="No value sets it back to the global default doorbell message.",
|
|
548
572
|
),
|
|
549
|
-
text:
|
|
573
|
+
text: str | None = typer.Argument(
|
|
550
574
|
None,
|
|
551
575
|
help="Only for CUSTOM_MESSAGE text type",
|
|
552
576
|
),
|
|
553
|
-
reset_at:
|
|
577
|
+
reset_at: datetime | None = typer.Option(
|
|
554
578
|
None,
|
|
555
579
|
"-r",
|
|
556
580
|
"--reset-time",
|
|
@@ -572,3 +596,118 @@ def set_lcd_text(
|
|
|
572
596
|
obj: d.Camera = ctx.obj.device
|
|
573
597
|
|
|
574
598
|
base.run(ctx, obj.set_lcd_text(text_type, text, reset_at))
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
@app.command()
|
|
602
|
+
def create_rtsps_streams(
|
|
603
|
+
ctx: typer.Context,
|
|
604
|
+
qualities: list[str] = typer.Argument(
|
|
605
|
+
...,
|
|
606
|
+
help="List of stream qualities to create (e.g., high medium low)",
|
|
607
|
+
),
|
|
608
|
+
) -> None:
|
|
609
|
+
"""
|
|
610
|
+
Creates RTSPS streams for camera.
|
|
611
|
+
|
|
612
|
+
Available qualities are typically: high, medium, low, ultra.
|
|
613
|
+
Requires API key authentication and public API access.
|
|
614
|
+
"""
|
|
615
|
+
base.require_device_id(ctx)
|
|
616
|
+
obj: d.Camera = ctx.obj.device
|
|
617
|
+
|
|
618
|
+
async def create_streams() -> None:
|
|
619
|
+
try:
|
|
620
|
+
result = await obj.create_rtsps_streams(qualities)
|
|
621
|
+
if result is None:
|
|
622
|
+
typer.secho("Failed to create RTSPS streams", fg="red")
|
|
623
|
+
raise typer.Exit(1)
|
|
624
|
+
|
|
625
|
+
if ctx.obj.output_format == base.OutputFormatEnum.JSON:
|
|
626
|
+
stream_data = {
|
|
627
|
+
quality: result.get_stream_url(quality)
|
|
628
|
+
for quality in result.get_available_stream_qualities()
|
|
629
|
+
}
|
|
630
|
+
base.json_output(stream_data)
|
|
631
|
+
else:
|
|
632
|
+
for quality in result.get_available_stream_qualities():
|
|
633
|
+
url = result.get_stream_url(quality)
|
|
634
|
+
typer.echo(f"{quality:10}\t{url}")
|
|
635
|
+
except Exception as e:
|
|
636
|
+
typer.secho(f"Error creating RTSPS streams: {e}", fg="red")
|
|
637
|
+
raise typer.Exit(1) from e
|
|
638
|
+
|
|
639
|
+
base.run(ctx, create_streams())
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
@app.command()
|
|
643
|
+
def get_rtsps_streams(ctx: typer.Context) -> None:
|
|
644
|
+
"""
|
|
645
|
+
Gets existing RTSPS streams for camera.
|
|
646
|
+
|
|
647
|
+
Requires API key authentication and public API access.
|
|
648
|
+
"""
|
|
649
|
+
base.require_device_id(ctx)
|
|
650
|
+
obj: d.Camera = ctx.obj.device
|
|
651
|
+
|
|
652
|
+
async def get_streams() -> None:
|
|
653
|
+
try:
|
|
654
|
+
result = await obj.get_rtsps_streams()
|
|
655
|
+
if result is None:
|
|
656
|
+
typer.secho("No RTSPS streams found or failed to retrieve", fg="yellow")
|
|
657
|
+
return
|
|
658
|
+
|
|
659
|
+
if ctx.obj.output_format == base.OutputFormatEnum.JSON:
|
|
660
|
+
stream_data = {
|
|
661
|
+
quality: result.get_stream_url(quality)
|
|
662
|
+
for quality in result.get_available_stream_qualities()
|
|
663
|
+
}
|
|
664
|
+
base.json_output(stream_data)
|
|
665
|
+
else:
|
|
666
|
+
available_qualities = result.get_available_stream_qualities()
|
|
667
|
+
if not available_qualities:
|
|
668
|
+
typer.echo("No RTSPS streams available")
|
|
669
|
+
else:
|
|
670
|
+
for quality in available_qualities:
|
|
671
|
+
url = result.get_stream_url(quality)
|
|
672
|
+
typer.echo(f"{quality:10}\t{url}")
|
|
673
|
+
except Exception as e:
|
|
674
|
+
typer.secho(f"Error getting RTSPS streams: {e}", fg="red")
|
|
675
|
+
raise typer.Exit(1) from e
|
|
676
|
+
|
|
677
|
+
base.run(ctx, get_streams())
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
@app.command()
|
|
681
|
+
def delete_rtsps_streams(
|
|
682
|
+
ctx: typer.Context,
|
|
683
|
+
qualities: list[str] = typer.Argument(
|
|
684
|
+
...,
|
|
685
|
+
help="List of stream qualities to delete (e.g., high medium low)",
|
|
686
|
+
),
|
|
687
|
+
) -> None:
|
|
688
|
+
"""
|
|
689
|
+
Deletes RTSPS streams for camera.
|
|
690
|
+
|
|
691
|
+
Requires API key authentication and public API access.
|
|
692
|
+
"""
|
|
693
|
+
base.require_device_id(ctx)
|
|
694
|
+
obj: d.Camera = ctx.obj.device
|
|
695
|
+
|
|
696
|
+
async def delete_streams() -> None:
|
|
697
|
+
try:
|
|
698
|
+
result = await obj.delete_rtsps_streams(qualities)
|
|
699
|
+
if result:
|
|
700
|
+
typer.secho(
|
|
701
|
+
f"Successfully deleted RTSPS streams: {', '.join(qualities)}",
|
|
702
|
+
fg="green",
|
|
703
|
+
)
|
|
704
|
+
else:
|
|
705
|
+
typer.secho(
|
|
706
|
+
f"Failed to delete RTSPS streams: {', '.join(qualities)}", fg="red"
|
|
707
|
+
)
|
|
708
|
+
raise typer.Exit(1)
|
|
709
|
+
except Exception as e:
|
|
710
|
+
typer.secho(f"Error deleting RTSPS streams: {e}", fg="red")
|
|
711
|
+
raise typer.Exit(1) from e
|
|
712
|
+
|
|
713
|
+
base.run(ctx, delete_streams())
|
uiprotect/cli/chimes.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
import typer
|
|
7
6
|
|
|
@@ -26,7 +25,7 @@ ALL_COMMANDS, DEVICE_COMMANDS = base.init_common_commands(app)
|
|
|
26
25
|
|
|
27
26
|
|
|
28
27
|
@app.callback(invoke_without_command=True)
|
|
29
|
-
def main(ctx: typer.Context, device_id:
|
|
28
|
+
def main(ctx: typer.Context, device_id: str | None = ARG_DEVICE_ID) -> None:
|
|
30
29
|
"""
|
|
31
30
|
Chime device CLI.
|
|
32
31
|
|
|
@@ -114,7 +113,7 @@ def cameras(
|
|
|
114
113
|
def set_volume(
|
|
115
114
|
ctx: typer.Context,
|
|
116
115
|
value: int = ARG_VOLUME,
|
|
117
|
-
camera_id:
|
|
116
|
+
camera_id: str | None = typer.Option(
|
|
118
117
|
None,
|
|
119
118
|
"-c",
|
|
120
119
|
"--camera",
|
|
@@ -138,8 +137,8 @@ def set_volume(
|
|
|
138
137
|
@app.command()
|
|
139
138
|
def play(
|
|
140
139
|
ctx: typer.Context,
|
|
141
|
-
volume:
|
|
142
|
-
repeat_times:
|
|
140
|
+
volume: int | None = typer.Option(None, "-v", "--volume", min=1, max=100),
|
|
141
|
+
repeat_times: int | None = typer.Option(None, "-r", "--repeat", min=1, max=6),
|
|
143
142
|
) -> None:
|
|
144
143
|
"""Plays chime tone."""
|
|
145
144
|
base.require_device_id(ctx)
|
|
@@ -159,7 +158,7 @@ def play_buzzer(ctx: typer.Context) -> None:
|
|
|
159
158
|
def set_repeat_times(
|
|
160
159
|
ctx: typer.Context,
|
|
161
160
|
value: int = ARG_REPEAT,
|
|
162
|
-
camera_id:
|
|
161
|
+
camera_id: str | None = typer.Option(
|
|
163
162
|
None,
|
|
164
163
|
"-c",
|
|
165
164
|
"--camera",
|
uiprotect/cli/doorlocks.py
CHANGED
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from datetime import timedelta
|
|
5
|
-
from typing import Optional
|
|
6
5
|
|
|
7
6
|
import typer
|
|
8
7
|
|
|
@@ -25,7 +24,7 @@ ALL_COMMANDS, DEVICE_COMMANDS = base.init_common_commands(app)
|
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
@app.callback(invoke_without_command=True)
|
|
28
|
-
def main(ctx: typer.Context, device_id:
|
|
27
|
+
def main(ctx: typer.Context, device_id: str | None = ARG_DEVICE_ID) -> None:
|
|
29
28
|
"""
|
|
30
29
|
Doorlock device CLI.
|
|
31
30
|
|
|
@@ -59,7 +58,7 @@ def main(ctx: typer.Context, device_id: Optional[str] = ARG_DEVICE_ID) -> None:
|
|
|
59
58
|
|
|
60
59
|
|
|
61
60
|
@app.command()
|
|
62
|
-
def camera(ctx: typer.Context, camera_id:
|
|
61
|
+
def camera(ctx: typer.Context, camera_id: str | None = typer.Argument(None)) -> None:
|
|
63
62
|
"""Returns or sets tha paired camera for a doorlock."""
|
|
64
63
|
base.require_device_id(ctx)
|
|
65
64
|
obj: Doorlock = ctx.obj.device
|
uiprotect/cli/events.py
CHANGED
|
@@ -4,7 +4,6 @@ from collections.abc import Callable
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from typing import Optional
|
|
8
7
|
|
|
9
8
|
import typer
|
|
10
9
|
from rich.progress import Progress
|
|
@@ -43,13 +42,13 @@ ALL_COMMANDS: dict[str, Callable[..., None]] = {}
|
|
|
43
42
|
@app.callback(invoke_without_command=True)
|
|
44
43
|
def main(
|
|
45
44
|
ctx: typer.Context,
|
|
46
|
-
event_id:
|
|
47
|
-
start:
|
|
48
|
-
end:
|
|
49
|
-
limit:
|
|
50
|
-
offset:
|
|
51
|
-
types:
|
|
52
|
-
smart_types:
|
|
45
|
+
event_id: str | None = ARG_EVENT_ID,
|
|
46
|
+
start: datetime | None = OPTION_START,
|
|
47
|
+
end: datetime | None = OPTION_END,
|
|
48
|
+
limit: int | None = OPTION_LIMIT,
|
|
49
|
+
offset: int | None = OPTION_OFFSET,
|
|
50
|
+
types: list[d.EventType] | None = OPTION_TYPES,
|
|
51
|
+
smart_types: list[d.SmartDetectObjectType] | None = OPTION_SMART_TYPES,
|
|
53
52
|
) -> None:
|
|
54
53
|
"""
|
|
55
54
|
Events CLI.
|