uiprotect 0.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/__init__.py +13 -0
- uiprotect/__main__.py +24 -0
- uiprotect/api.py +1936 -0
- uiprotect/cli/__init__.py +314 -0
- uiprotect/cli/backup.py +1103 -0
- uiprotect/cli/base.py +238 -0
- uiprotect/cli/cameras.py +574 -0
- uiprotect/cli/chimes.py +180 -0
- uiprotect/cli/doorlocks.py +125 -0
- uiprotect/cli/events.py +258 -0
- uiprotect/cli/lights.py +119 -0
- uiprotect/cli/liveviews.py +65 -0
- uiprotect/cli/nvr.py +154 -0
- uiprotect/cli/sensors.py +278 -0
- uiprotect/cli/viewers.py +76 -0
- uiprotect/data/__init__.py +157 -0
- uiprotect/data/base.py +1116 -0
- uiprotect/data/bootstrap.py +634 -0
- uiprotect/data/convert.py +77 -0
- uiprotect/data/devices.py +3384 -0
- uiprotect/data/nvr.py +1520 -0
- uiprotect/data/types.py +630 -0
- uiprotect/data/user.py +236 -0
- uiprotect/data/websocket.py +236 -0
- uiprotect/exceptions.py +41 -0
- uiprotect/py.typed +0 -0
- uiprotect/release_cache.json +1 -0
- uiprotect/stream.py +166 -0
- uiprotect/test_util/__init__.py +531 -0
- uiprotect/test_util/anonymize.py +257 -0
- uiprotect/utils.py +610 -0
- uiprotect/websocket.py +225 -0
- uiprotect-0.1.0.dist-info/LICENSE +23 -0
- uiprotect-0.1.0.dist-info/METADATA +245 -0
- uiprotect-0.1.0.dist-info/RECORD +37 -0
- uiprotect-0.1.0.dist-info/WHEEL +4 -0
- uiprotect-0.1.0.dist-info/entry_points.txt +3 -0
uiprotect/cli/cameras.py
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, cast
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.progress import Progress
|
|
10
|
+
|
|
11
|
+
from uiprotect import data as d
|
|
12
|
+
from uiprotect.api import ProtectApiClient
|
|
13
|
+
from uiprotect.cli import base
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(rich_markup_mode="rich")
|
|
16
|
+
|
|
17
|
+
ARG_DEVICE_ID = typer.Argument(None, help="ID of camera to select for subcommands")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class CameraContext(base.CliContext):
|
|
22
|
+
devices: dict[str, d.Camera]
|
|
23
|
+
device: d.Camera | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
ALL_COMMANDS, DEVICE_COMMANDS = base.init_common_commands(app)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.callback(invoke_without_command=True)
|
|
30
|
+
def main(ctx: typer.Context, device_id: Optional[str] = ARG_DEVICE_ID) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Camera device CLI.
|
|
33
|
+
|
|
34
|
+
Returns full list of Cameras without any arguments passed.
|
|
35
|
+
"""
|
|
36
|
+
protect: ProtectApiClient = ctx.obj.protect
|
|
37
|
+
context = CameraContext(
|
|
38
|
+
protect=ctx.obj.protect,
|
|
39
|
+
device=None,
|
|
40
|
+
devices=protect.bootstrap.cameras,
|
|
41
|
+
output_format=ctx.obj.output_format,
|
|
42
|
+
)
|
|
43
|
+
ctx.obj = context
|
|
44
|
+
|
|
45
|
+
if device_id is not None and device_id not in ALL_COMMANDS:
|
|
46
|
+
if (device := protect.bootstrap.cameras.get(device_id)) is None:
|
|
47
|
+
typer.secho("Invalid camera ID", fg="red")
|
|
48
|
+
raise typer.Exit(1)
|
|
49
|
+
ctx.obj.device = device
|
|
50
|
+
|
|
51
|
+
if not ctx.invoked_subcommand:
|
|
52
|
+
if device_id in ALL_COMMANDS:
|
|
53
|
+
ctx.invoke(ALL_COMMANDS[device_id], ctx)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
if ctx.obj.device is not None:
|
|
57
|
+
base.print_unifi_obj(ctx.obj.device, ctx.obj.output_format)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
base.print_unifi_dict(ctx.obj.devices)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@app.command()
|
|
64
|
+
def timelapse_url(ctx: typer.Context) -> None:
|
|
65
|
+
"""Returns UniFi Protect timelapse URL."""
|
|
66
|
+
base.require_device_id(ctx)
|
|
67
|
+
obj: d.Camera = ctx.obj.device
|
|
68
|
+
if ctx.obj.output_format == base.OutputFormatEnum.JSON:
|
|
69
|
+
base.json_output(obj.timelapse_url)
|
|
70
|
+
else:
|
|
71
|
+
typer.echo(obj.timelapse_url)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.command()
|
|
75
|
+
def privacy_mode(
|
|
76
|
+
ctx: typer.Context,
|
|
77
|
+
enabled: Optional[bool] = typer.Argument(None),
|
|
78
|
+
) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Returns/sets library managed privacy mode.
|
|
81
|
+
|
|
82
|
+
Does not change the microphone sensitivity or recording mode.
|
|
83
|
+
It must be changed seperately.
|
|
84
|
+
"""
|
|
85
|
+
base.require_device_id(ctx)
|
|
86
|
+
obj: d.Camera = ctx.obj.device
|
|
87
|
+
if enabled is None:
|
|
88
|
+
base.json_output(obj.is_privacy_on)
|
|
89
|
+
return
|
|
90
|
+
base.run(ctx, obj.set_privacy(enabled))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@app.command()
|
|
94
|
+
def chime_type(ctx: typer.Context, value: Optional[d.ChimeType] = None) -> None:
|
|
95
|
+
"""Returns/sets the current chime type if the camera has a chime."""
|
|
96
|
+
base.require_device_id(ctx)
|
|
97
|
+
obj: d.Camera = ctx.obj.device
|
|
98
|
+
if not obj.feature_flags.has_chime:
|
|
99
|
+
typer.secho("Camera does not have a chime", fg="red")
|
|
100
|
+
raise typer.Exit(1)
|
|
101
|
+
|
|
102
|
+
if value is None:
|
|
103
|
+
if ctx.obj.output_format == base.OutputFormatEnum.JSON:
|
|
104
|
+
base.json_output(obj.chime_type)
|
|
105
|
+
elif obj.chime_type is not None:
|
|
106
|
+
typer.echo(obj.chime_type.name)
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
base.run(ctx, obj.set_chime_type(value))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@app.command()
|
|
113
|
+
def stream_urls(ctx: typer.Context) -> None:
|
|
114
|
+
"""Returns all of the enabled RTSP(S) URLs."""
|
|
115
|
+
base.require_device_id(ctx)
|
|
116
|
+
obj: d.Camera = ctx.obj.device
|
|
117
|
+
data: list[tuple[str, str]] = []
|
|
118
|
+
for channel in obj.channels:
|
|
119
|
+
if channel.is_rtsp_enabled:
|
|
120
|
+
rtsp_url = cast(str, channel.rtsp_url)
|
|
121
|
+
rtsps_url = cast(str, channel.rtsps_url)
|
|
122
|
+
data.extend(
|
|
123
|
+
(
|
|
124
|
+
(f"{channel.name} RTSP", rtsp_url),
|
|
125
|
+
(f"{channel.name} RTSPS", rtsps_url),
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if ctx.obj.output_format == base.OutputFormatEnum.JSON:
|
|
130
|
+
base.json_output(data)
|
|
131
|
+
else:
|
|
132
|
+
for name, url in data:
|
|
133
|
+
typer.echo(f"{name:20}\t{url}")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@app.command()
|
|
137
|
+
def save_snapshot(
|
|
138
|
+
ctx: typer.Context,
|
|
139
|
+
output_path: Path = typer.Argument(..., help="JPEG format"),
|
|
140
|
+
width: Optional[int] = typer.Option(None, "-w", "--width"),
|
|
141
|
+
height: Optional[int] = typer.Option(None, "-h", "--height"),
|
|
142
|
+
dt: Optional[datetime] = typer.Option(None, "-t", "--timestamp"),
|
|
143
|
+
package: bool = typer.Option(False, "-p", "--package", help="Get package camera"),
|
|
144
|
+
) -> None:
|
|
145
|
+
"""
|
|
146
|
+
Takes snapshot of camera.
|
|
147
|
+
|
|
148
|
+
If you specify a timestamp, they are approximate. It will not export with down to the second
|
|
149
|
+
accuracy so it may be +/- a few seconds.
|
|
150
|
+
|
|
151
|
+
Timestamps use your locale timezone. If it is not configured correctly,
|
|
152
|
+
it will default to UTC. You can override your timezone with the
|
|
153
|
+
TZ environment variable.
|
|
154
|
+
"""
|
|
155
|
+
base.require_device_id(ctx)
|
|
156
|
+
obj: d.Camera = ctx.obj.device
|
|
157
|
+
|
|
158
|
+
if dt is not None:
|
|
159
|
+
local_tz = datetime.now(timezone.utc).astimezone().tzinfo
|
|
160
|
+
dt = dt.replace(tzinfo=local_tz)
|
|
161
|
+
|
|
162
|
+
if package:
|
|
163
|
+
if not obj.feature_flags.has_package_camera:
|
|
164
|
+
typer.secho("Camera does not have package camera", fg="red")
|
|
165
|
+
raise typer.Exit(1)
|
|
166
|
+
snapshot = base.run(ctx, obj.get_package_snapshot(width, height, dt=dt))
|
|
167
|
+
else:
|
|
168
|
+
snapshot = base.run(ctx, obj.get_snapshot(width, height, dt=dt))
|
|
169
|
+
|
|
170
|
+
if snapshot is None:
|
|
171
|
+
typer.secho("Could not get snapshot", fg="red")
|
|
172
|
+
raise typer.Exit(1)
|
|
173
|
+
|
|
174
|
+
Path(output_path).write_bytes(snapshot)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@app.command()
|
|
178
|
+
def save_video(
|
|
179
|
+
ctx: typer.Context,
|
|
180
|
+
output_path: Path = typer.Argument(..., help="MP4 format"),
|
|
181
|
+
start: datetime = typer.Argument(...),
|
|
182
|
+
end: datetime = typer.Argument(...),
|
|
183
|
+
channel: int = typer.Option(
|
|
184
|
+
0,
|
|
185
|
+
"-c",
|
|
186
|
+
"--channel",
|
|
187
|
+
min=0,
|
|
188
|
+
max=3,
|
|
189
|
+
help="0 = High, 1 = Medium, 2 = Low, 3 = Package",
|
|
190
|
+
),
|
|
191
|
+
fps: Optional[int] = typer.Option(
|
|
192
|
+
None,
|
|
193
|
+
"--fps",
|
|
194
|
+
min=1,
|
|
195
|
+
max=40,
|
|
196
|
+
help="Export as timelapse. 4 = 60x, 8 = 120x, 20 = 300x, 40 = 600x",
|
|
197
|
+
),
|
|
198
|
+
) -> None:
|
|
199
|
+
"""
|
|
200
|
+
Exports video of camera.
|
|
201
|
+
|
|
202
|
+
Exports are approximate. It will not export with down to the second
|
|
203
|
+
accuracy so it may be +/- a few seconds.
|
|
204
|
+
|
|
205
|
+
Uses your locale timezone. If it is not configured correctly,
|
|
206
|
+
it will default to UTC. You can override your timezone with the
|
|
207
|
+
TZ environment variable.
|
|
208
|
+
"""
|
|
209
|
+
base.require_device_id(ctx)
|
|
210
|
+
obj: d.Camera = ctx.obj.device
|
|
211
|
+
|
|
212
|
+
local_tz = datetime.now(timezone.utc).astimezone().tzinfo
|
|
213
|
+
start = start.replace(tzinfo=local_tz)
|
|
214
|
+
end = end.replace(tzinfo=local_tz)
|
|
215
|
+
|
|
216
|
+
if channel == 4 and not obj.feature_flags.has_package_camera:
|
|
217
|
+
typer.secho("Camera does not have package camera", fg="red")
|
|
218
|
+
raise typer.Exit(1)
|
|
219
|
+
|
|
220
|
+
with Progress() as pb:
|
|
221
|
+
task_id = pb.add_task("(1/2) Exporting", total=100)
|
|
222
|
+
|
|
223
|
+
async def callback(step: int, current: int, total: int) -> None:
|
|
224
|
+
pb.update(
|
|
225
|
+
task_id,
|
|
226
|
+
total=total,
|
|
227
|
+
completed=current,
|
|
228
|
+
description="(2/2) Downloading",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
base.run(
|
|
232
|
+
ctx,
|
|
233
|
+
obj.get_video(
|
|
234
|
+
start,
|
|
235
|
+
end,
|
|
236
|
+
channel,
|
|
237
|
+
output_file=output_path,
|
|
238
|
+
progress_callback=callback,
|
|
239
|
+
fps=fps,
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@app.command()
|
|
245
|
+
def play_audio(
|
|
246
|
+
ctx: typer.Context,
|
|
247
|
+
url: str = typer.Argument(..., help="ffmpeg playable URL"),
|
|
248
|
+
ffmpeg_path: Optional[Path] = typer.Option(
|
|
249
|
+
None,
|
|
250
|
+
"--ffmpeg-path",
|
|
251
|
+
help="Path to ffmpeg executable",
|
|
252
|
+
envvar="FFMPEG_PATH",
|
|
253
|
+
),
|
|
254
|
+
) -> None:
|
|
255
|
+
"""Plays audio file on camera speaker."""
|
|
256
|
+
base.require_device_id(ctx)
|
|
257
|
+
obj: d.Camera = ctx.obj.device
|
|
258
|
+
base.run(ctx, obj.play_audio(url, ffmpeg_path=ffmpeg_path))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@app.command()
|
|
262
|
+
def smart_detects(
|
|
263
|
+
ctx: typer.Context,
|
|
264
|
+
values: list[d.SmartDetectObjectType] = typer.Argument(
|
|
265
|
+
None,
|
|
266
|
+
help="Set to [] to empty list of detect types.",
|
|
267
|
+
),
|
|
268
|
+
add: bool = typer.Option(False, "-a", "--add", help="Add values instead of set"),
|
|
269
|
+
remove: bool = typer.Option(
|
|
270
|
+
False,
|
|
271
|
+
"-r",
|
|
272
|
+
"--remove",
|
|
273
|
+
help="Remove values instead of set",
|
|
274
|
+
),
|
|
275
|
+
) -> None:
|
|
276
|
+
"""Returns/set smart detect types for camera."""
|
|
277
|
+
base.require_device_id(ctx)
|
|
278
|
+
obj: d.Camera = ctx.obj.device
|
|
279
|
+
|
|
280
|
+
if add and remove:
|
|
281
|
+
typer.secho("Add and remove are mutally exclusive", fg="red")
|
|
282
|
+
raise typer.Exit(1)
|
|
283
|
+
|
|
284
|
+
if not obj.feature_flags.has_smart_detect:
|
|
285
|
+
typer.secho("Camera does not support smart detections", fg="red")
|
|
286
|
+
raise typer.Exit(1)
|
|
287
|
+
|
|
288
|
+
if len(values) == 0:
|
|
289
|
+
if ctx.obj.output_format == base.OutputFormatEnum.JSON:
|
|
290
|
+
base.json_output(obj.smart_detect_settings.object_types)
|
|
291
|
+
else:
|
|
292
|
+
for value in obj.smart_detect_settings.object_types:
|
|
293
|
+
typer.echo(value.value)
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
if len(values) == 1 and values[0] == "[]":
|
|
297
|
+
values = []
|
|
298
|
+
|
|
299
|
+
for value in values:
|
|
300
|
+
if value not in obj.feature_flags.smart_detect_types:
|
|
301
|
+
typer.secho(f"Camera does not support {value}", fg="red")
|
|
302
|
+
raise typer.Exit(1)
|
|
303
|
+
|
|
304
|
+
if add:
|
|
305
|
+
values = list(set(obj.smart_detect_settings.object_types) | set(values))
|
|
306
|
+
elif remove:
|
|
307
|
+
values = list(set(obj.smart_detect_settings.object_types) - set(values))
|
|
308
|
+
|
|
309
|
+
data_before_changes = obj.dict_with_excludes()
|
|
310
|
+
obj.smart_detect_settings.object_types = values
|
|
311
|
+
base.run(ctx, obj.save_device(data_before_changes))
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@app.command()
|
|
315
|
+
def smart_audio_detects(
|
|
316
|
+
ctx: typer.Context,
|
|
317
|
+
values: list[d.SmartDetectAudioType] = typer.Argument(
|
|
318
|
+
None,
|
|
319
|
+
help="Set to [] to empty list of detect types.",
|
|
320
|
+
),
|
|
321
|
+
add: bool = typer.Option(False, "-a", "--add", help="Add values instead of set"),
|
|
322
|
+
remove: bool = typer.Option(
|
|
323
|
+
False,
|
|
324
|
+
"-r",
|
|
325
|
+
"--remove",
|
|
326
|
+
help="Remove values instead of set",
|
|
327
|
+
),
|
|
328
|
+
) -> None:
|
|
329
|
+
"""Returns/set smart detect types for camera."""
|
|
330
|
+
base.require_device_id(ctx)
|
|
331
|
+
obj: d.Camera = ctx.obj.device
|
|
332
|
+
|
|
333
|
+
if add and remove:
|
|
334
|
+
typer.secho("Add and remove are mutually exclusive", fg="red")
|
|
335
|
+
raise typer.Exit(1)
|
|
336
|
+
|
|
337
|
+
if not obj.feature_flags.has_smart_detect:
|
|
338
|
+
typer.secho("Camera does not support smart detections", fg="red")
|
|
339
|
+
raise typer.Exit(1)
|
|
340
|
+
|
|
341
|
+
obj.smart_detect_settings.audio_types = obj.smart_detect_settings.audio_types or []
|
|
342
|
+
obj.smart_detect_settings.audio_types = obj.smart_detect_settings.audio_types or []
|
|
343
|
+
|
|
344
|
+
if len(values) == 0:
|
|
345
|
+
if ctx.obj.output_format == base.OutputFormatEnum.JSON:
|
|
346
|
+
base.json_output(obj.smart_detect_settings.audio_types)
|
|
347
|
+
else:
|
|
348
|
+
for value in obj.smart_detect_settings.audio_types:
|
|
349
|
+
typer.echo(value.value)
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
if len(values) == 1 and values[0] == "[]":
|
|
353
|
+
values = []
|
|
354
|
+
|
|
355
|
+
for value in values:
|
|
356
|
+
if (
|
|
357
|
+
obj.feature_flags.smart_detect_audio_types is None
|
|
358
|
+
or value not in obj.feature_flags.smart_detect_audio_types
|
|
359
|
+
):
|
|
360
|
+
typer.secho(f"Camera does not support {value}", fg="red")
|
|
361
|
+
raise typer.Exit(1)
|
|
362
|
+
|
|
363
|
+
if add:
|
|
364
|
+
values = list(set(obj.smart_detect_settings.audio_types) | set(values))
|
|
365
|
+
elif remove:
|
|
366
|
+
values = list(set(obj.smart_detect_settings.audio_types) - set(values))
|
|
367
|
+
|
|
368
|
+
data_before_changes = obj.dict_with_excludes()
|
|
369
|
+
obj.smart_detect_settings.audio_types = values
|
|
370
|
+
base.run(ctx, obj.save_device(data_before_changes))
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@app.command()
|
|
374
|
+
def set_motion_detection(ctx: typer.Context, enabled: bool) -> None:
|
|
375
|
+
"""Sets motion detection on camera"""
|
|
376
|
+
base.require_device_id(ctx)
|
|
377
|
+
obj: d.Camera = ctx.obj.device
|
|
378
|
+
|
|
379
|
+
base.run(ctx, obj.set_motion_detection(enabled))
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@app.command()
|
|
383
|
+
def set_recording_mode(ctx: typer.Context, mode: d.RecordingMode) -> None:
|
|
384
|
+
"""Sets recording mode on camera"""
|
|
385
|
+
base.require_device_id(ctx)
|
|
386
|
+
obj: d.Camera = ctx.obj.device
|
|
387
|
+
|
|
388
|
+
base.run(ctx, obj.set_recording_mode(mode))
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@app.command()
|
|
392
|
+
def set_ir_led_mode(ctx: typer.Context, mode: d.IRLEDMode) -> None:
|
|
393
|
+
"""Sets IR LED mode on camera"""
|
|
394
|
+
base.require_device_id(ctx)
|
|
395
|
+
obj: d.Camera = ctx.obj.device
|
|
396
|
+
|
|
397
|
+
base.run(ctx, obj.set_ir_led_model(mode))
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
@app.command()
|
|
401
|
+
def set_status_light(ctx: typer.Context, enabled: bool) -> None:
|
|
402
|
+
"""Sets status indicicator light on camera"""
|
|
403
|
+
base.require_device_id(ctx)
|
|
404
|
+
obj: d.Camera = ctx.obj.device
|
|
405
|
+
|
|
406
|
+
base.run(ctx, obj.set_status_light(enabled))
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
@app.command()
|
|
410
|
+
def set_hdr(ctx: typer.Context, enabled: bool) -> None:
|
|
411
|
+
"""Sets HDR (High Dynamic Range) on camera"""
|
|
412
|
+
base.require_device_id(ctx)
|
|
413
|
+
obj: d.Camera = ctx.obj.device
|
|
414
|
+
|
|
415
|
+
base.run(ctx, obj.set_hdr(enabled))
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@app.command()
|
|
419
|
+
def set_color_night_vision(ctx: typer.Context, enabled: bool) -> None:
|
|
420
|
+
"""Sets Color Night Vision on camera"""
|
|
421
|
+
base.require_device_id(ctx)
|
|
422
|
+
obj: d.Camera = ctx.obj.device
|
|
423
|
+
|
|
424
|
+
base.run(ctx, obj.set_color_night_vision(enabled=enabled))
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@app.command()
|
|
428
|
+
def set_person_track(ctx: typer.Context, enabled: bool) -> None:
|
|
429
|
+
"""Sets person tracking on camera"""
|
|
430
|
+
base.require_device_id(ctx)
|
|
431
|
+
obj: d.Camera = ctx.obj.device
|
|
432
|
+
|
|
433
|
+
if not obj.feature_flags.is_ptz:
|
|
434
|
+
typer.secho("Camera does not support person tracking", fg="red")
|
|
435
|
+
raise typer.Exit(1)
|
|
436
|
+
|
|
437
|
+
base.run(ctx, (obj.set_person_track(enabled=enabled)))
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@app.command()
|
|
441
|
+
def set_video_mode(ctx: typer.Context, mode: d.VideoMode) -> None:
|
|
442
|
+
"""Sets video mode on camera"""
|
|
443
|
+
base.require_device_id(ctx)
|
|
444
|
+
obj: d.Camera = ctx.obj.device
|
|
445
|
+
|
|
446
|
+
base.run(ctx, obj.set_video_mode(mode))
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@app.command()
|
|
450
|
+
def set_camera_zoom(
|
|
451
|
+
ctx: typer.Context,
|
|
452
|
+
level: int = typer.Argument(..., min=0, max=100),
|
|
453
|
+
) -> None:
|
|
454
|
+
"""Sets zoom level for camera"""
|
|
455
|
+
base.require_device_id(ctx)
|
|
456
|
+
obj: d.Camera = ctx.obj.device
|
|
457
|
+
|
|
458
|
+
base.run(ctx, obj.set_camera_zoom(level))
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@app.command()
|
|
462
|
+
def set_wdr_level(
|
|
463
|
+
ctx: typer.Context,
|
|
464
|
+
level: int = typer.Argument(..., min=0, max=3),
|
|
465
|
+
) -> None:
|
|
466
|
+
"""Sets WDR (Wide Dynamic Range) on camera"""
|
|
467
|
+
base.require_device_id(ctx)
|
|
468
|
+
obj: d.Camera = ctx.obj.device
|
|
469
|
+
|
|
470
|
+
base.run(ctx, obj.set_wdr_level(level))
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@app.command()
|
|
474
|
+
def set_mic_volume(
|
|
475
|
+
ctx: typer.Context,
|
|
476
|
+
level: int = typer.Argument(..., min=0, max=100),
|
|
477
|
+
) -> None:
|
|
478
|
+
"""Sets the mic sensitivity level on camera"""
|
|
479
|
+
base.require_device_id(ctx)
|
|
480
|
+
obj: d.Camera = ctx.obj.device
|
|
481
|
+
|
|
482
|
+
base.run(ctx, obj.set_mic_volume(level))
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
@app.command()
|
|
486
|
+
def set_speaker_volume(
|
|
487
|
+
ctx: typer.Context,
|
|
488
|
+
level: int = typer.Argument(..., min=0, max=100),
|
|
489
|
+
) -> None:
|
|
490
|
+
"""Sets the speaker sensitivity level on camera"""
|
|
491
|
+
base.require_device_id(ctx)
|
|
492
|
+
obj: d.Camera = ctx.obj.device
|
|
493
|
+
|
|
494
|
+
base.run(ctx, obj.set_speaker_volume(level))
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
@app.command()
|
|
498
|
+
def set_system_sounds(ctx: typer.Context, enabled: bool) -> None:
|
|
499
|
+
"""Sets system sound playback through speakers"""
|
|
500
|
+
base.require_device_id(ctx)
|
|
501
|
+
obj: d.Camera = ctx.obj.device
|
|
502
|
+
|
|
503
|
+
base.run(ctx, obj.set_system_sounds(enabled))
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
@app.command()
|
|
507
|
+
def set_osd_name(ctx: typer.Context, enabled: bool) -> None:
|
|
508
|
+
"""Sets whether camera name is in the On Screen Display"""
|
|
509
|
+
base.require_device_id(ctx)
|
|
510
|
+
obj: d.Camera = ctx.obj.device
|
|
511
|
+
|
|
512
|
+
base.run(ctx, obj.set_osd_name(enabled))
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
@app.command()
|
|
516
|
+
def set_osd_date(ctx: typer.Context, enabled: bool) -> None:
|
|
517
|
+
"""Sets whether current date is in the On Screen Display"""
|
|
518
|
+
base.require_device_id(ctx)
|
|
519
|
+
obj: d.Camera = ctx.obj.device
|
|
520
|
+
|
|
521
|
+
base.run(ctx, obj.set_osd_date(enabled))
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
@app.command()
|
|
525
|
+
def set_osd_logo(ctx: typer.Context, enabled: bool) -> None:
|
|
526
|
+
"""Sets whether the UniFi logo is in the On Screen Display"""
|
|
527
|
+
base.require_device_id(ctx)
|
|
528
|
+
obj: d.Camera = ctx.obj.device
|
|
529
|
+
|
|
530
|
+
base.run(ctx, obj.set_osd_logo(enabled))
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
@app.command()
|
|
534
|
+
def set_osd_bitrate(ctx: typer.Context, enabled: bool) -> None:
|
|
535
|
+
"""Sets whether camera bitrate is in the On Screen Display"""
|
|
536
|
+
base.require_device_id(ctx)
|
|
537
|
+
obj: d.Camera = ctx.obj.device
|
|
538
|
+
|
|
539
|
+
base.run(ctx, obj.set_osd_bitrate(enabled))
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
@app.command()
|
|
543
|
+
def set_lcd_text(
|
|
544
|
+
ctx: typer.Context,
|
|
545
|
+
text_type: Optional[d.DoorbellMessageType] = typer.Argument(
|
|
546
|
+
None,
|
|
547
|
+
help="No value sets it back to the global default doorbell message.",
|
|
548
|
+
),
|
|
549
|
+
text: Optional[str] = typer.Argument(
|
|
550
|
+
None,
|
|
551
|
+
help="Only for CUSTOM_MESSAGE text type",
|
|
552
|
+
),
|
|
553
|
+
reset_at: Optional[datetime] = typer.Option(
|
|
554
|
+
None,
|
|
555
|
+
"-r",
|
|
556
|
+
"--reset-time",
|
|
557
|
+
help="Does not apply to default message",
|
|
558
|
+
),
|
|
559
|
+
) -> None:
|
|
560
|
+
"""
|
|
561
|
+
Sets doorbell LCD text.
|
|
562
|
+
|
|
563
|
+
Uses your locale timezone. If it is not configured correctly,
|
|
564
|
+
it will default to UTC. You can override your timezone with the
|
|
565
|
+
TZ environment variable.
|
|
566
|
+
"""
|
|
567
|
+
if reset_at is not None:
|
|
568
|
+
local_tz = datetime.now(timezone.utc).astimezone().tzinfo
|
|
569
|
+
reset_at = reset_at.replace(tzinfo=local_tz)
|
|
570
|
+
|
|
571
|
+
base.require_device_id(ctx)
|
|
572
|
+
obj: d.Camera = ctx.obj.device
|
|
573
|
+
|
|
574
|
+
base.run(ctx, obj.set_lcd_text(text_type, text, reset_at))
|