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/base.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Coroutine, Mapping, Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Optional, TypeVar
|
|
7
|
+
|
|
8
|
+
import orjson
|
|
9
|
+
import typer
|
|
10
|
+
from pydantic.v1 import ValidationError
|
|
11
|
+
|
|
12
|
+
from uiprotect.api import ProtectApiClient
|
|
13
|
+
from uiprotect.data import NVR, ProtectAdoptableDeviceModel, ProtectBaseObject
|
|
14
|
+
from uiprotect.exceptions import BadRequest, NvrError, StreamError
|
|
15
|
+
from uiprotect.utils import run_async
|
|
16
|
+
|
|
17
|
+
T = TypeVar("T")
|
|
18
|
+
|
|
19
|
+
OPTION_FORCE = typer.Option(False, "-f", "--force", help="Skip confirmation prompt")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OutputFormatEnum(str, Enum):
|
|
23
|
+
JSON = "json"
|
|
24
|
+
PLAIN = "plain"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class CliContext:
|
|
29
|
+
protect: ProtectApiClient
|
|
30
|
+
output_format: OutputFormatEnum
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run(ctx: typer.Context, func: Coroutine[Any, Any, T]) -> T:
|
|
34
|
+
"""Helper method to call async function and clean up API client"""
|
|
35
|
+
|
|
36
|
+
async def callback() -> T:
|
|
37
|
+
return_value = await func
|
|
38
|
+
await ctx.obj.protect.close_session()
|
|
39
|
+
return return_value
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
return run_async(callback())
|
|
43
|
+
except (BadRequest, ValidationError, StreamError, NvrError) as err:
|
|
44
|
+
typer.secho(str(err), fg="red")
|
|
45
|
+
raise typer.Exit(1) from err
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def json_output(obj: Any) -> None:
|
|
49
|
+
typer.echo(orjson.dumps(obj, option=orjson.OPT_INDENT_2).decode("utf-8"))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def print_unifi_obj(
|
|
53
|
+
obj: ProtectBaseObject | None,
|
|
54
|
+
output_format: OutputFormatEnum,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Helper method to print a single protect object"""
|
|
57
|
+
if obj is not None:
|
|
58
|
+
json_output(obj.unifi_dict())
|
|
59
|
+
elif output_format == OutputFormatEnum.JSON:
|
|
60
|
+
json_output(None)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def print_unifi_list(objs: Sequence[ProtectBaseObject]) -> None:
|
|
64
|
+
"""Helper method to print a list of protect objects"""
|
|
65
|
+
data = [o.unifi_dict() for o in objs]
|
|
66
|
+
json_output(data)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def print_unifi_dict(objs: Mapping[str, ProtectBaseObject]) -> None:
|
|
70
|
+
"""Helper method to print a dictionary of protect objects"""
|
|
71
|
+
data = {k: v.unifi_dict() for k, v in objs.items()}
|
|
72
|
+
json_output(data)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def require_device_id(ctx: typer.Context) -> None:
|
|
76
|
+
"""Requires device ID in context"""
|
|
77
|
+
if ctx.obj.device is None:
|
|
78
|
+
typer.secho("Requires a valid device ID to be selected")
|
|
79
|
+
raise typer.Exit(1)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def require_no_device_id(ctx: typer.Context) -> None:
|
|
83
|
+
"""Requires no device ID in context"""
|
|
84
|
+
if ctx.obj.device is not None:
|
|
85
|
+
typer.secho("Requires no device ID to be selected")
|
|
86
|
+
raise typer.Exit(1)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def list_ids(ctx: typer.Context) -> None:
|
|
90
|
+
"""Requires no device ID. Prints list of "id name" for each device."""
|
|
91
|
+
require_no_device_id(ctx)
|
|
92
|
+
objs: dict[str, ProtectAdoptableDeviceModel] = ctx.obj.devices
|
|
93
|
+
to_print: list[tuple[str, str | None]] = []
|
|
94
|
+
for obj in objs.values():
|
|
95
|
+
name = obj.display_name
|
|
96
|
+
if obj.is_adopted_by_other:
|
|
97
|
+
name = f"{name} [Managed by Another Console]"
|
|
98
|
+
elif obj.is_adopting:
|
|
99
|
+
name = f"{name} [Adopting]"
|
|
100
|
+
elif obj.can_adopt:
|
|
101
|
+
name = f"{name} [Unadopted]"
|
|
102
|
+
elif obj.is_rebooting:
|
|
103
|
+
name = f"{name} [Restarting]"
|
|
104
|
+
elif obj.is_updating:
|
|
105
|
+
name = f"{name} [Updating]"
|
|
106
|
+
elif not obj.is_connected:
|
|
107
|
+
name = f"{name} [Disconnected]"
|
|
108
|
+
|
|
109
|
+
to_print.append((obj.id, name))
|
|
110
|
+
|
|
111
|
+
if ctx.obj.output_format == OutputFormatEnum.JSON:
|
|
112
|
+
json_output(to_print)
|
|
113
|
+
else:
|
|
114
|
+
for item in to_print:
|
|
115
|
+
typer.echo(f"{item[0]}\t{item[1]}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def protect_url(ctx: typer.Context) -> None:
|
|
119
|
+
"""Gets UniFi Protect management URL."""
|
|
120
|
+
require_device_id(ctx)
|
|
121
|
+
obj: NVR | ProtectAdoptableDeviceModel = ctx.obj.device
|
|
122
|
+
if ctx.obj.output_format == OutputFormatEnum.JSON:
|
|
123
|
+
json_output(obj.protect_url)
|
|
124
|
+
else:
|
|
125
|
+
typer.echo(obj.protect_url)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def is_wired(ctx: typer.Context) -> None:
|
|
129
|
+
"""Returns if the device is wired or not."""
|
|
130
|
+
require_device_id(ctx)
|
|
131
|
+
obj: ProtectAdoptableDeviceModel = ctx.obj.device
|
|
132
|
+
json_output(obj.is_wired)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def is_wifi(ctx: typer.Context) -> None:
|
|
136
|
+
"""Returns if the device has WiFi or not."""
|
|
137
|
+
require_device_id(ctx)
|
|
138
|
+
obj: ProtectAdoptableDeviceModel = ctx.obj.device
|
|
139
|
+
json_output(obj.is_wifi)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def is_bluetooth(ctx: typer.Context) -> None:
|
|
143
|
+
"""Returns if the device has Bluetooth or not."""
|
|
144
|
+
require_device_id(ctx)
|
|
145
|
+
obj: ProtectAdoptableDeviceModel = ctx.obj.device
|
|
146
|
+
json_output(obj.is_bluetooth)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def bridge(ctx: typer.Context) -> None:
|
|
150
|
+
"""Returns bridge device if connected via Bluetooth."""
|
|
151
|
+
require_device_id(ctx)
|
|
152
|
+
obj: ProtectAdoptableDeviceModel = ctx.obj.device
|
|
153
|
+
print_unifi_obj(obj.bridge, ctx.obj.output_format)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def set_ssh(ctx: typer.Context, enabled: bool) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Sets the isSshEnabled value for device.
|
|
159
|
+
|
|
160
|
+
May not have an effect on many device types. Only seems to work for
|
|
161
|
+
Linux and BusyBox based devices (camera, light and viewport).
|
|
162
|
+
"""
|
|
163
|
+
require_device_id(ctx)
|
|
164
|
+
obj: ProtectAdoptableDeviceModel = ctx.obj.device
|
|
165
|
+
run(ctx, obj.set_ssh(enabled))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def set_name(ctx: typer.Context, name: Optional[str] = typer.Argument(None)) -> None:
|
|
169
|
+
"""Sets name for the device"""
|
|
170
|
+
require_device_id(ctx)
|
|
171
|
+
obj: NVR | ProtectAdoptableDeviceModel = ctx.obj.device
|
|
172
|
+
run(ctx, obj.set_name(name))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def update(ctx: typer.Context, data: str) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Updates the device.
|
|
178
|
+
|
|
179
|
+
Makes a raw PATCH request to update a device. Advanced usage and usually recommended not to use.
|
|
180
|
+
"""
|
|
181
|
+
require_device_id(ctx)
|
|
182
|
+
obj: ProtectAdoptableDeviceModel = ctx.obj.device
|
|
183
|
+
|
|
184
|
+
if obj.model is not None:
|
|
185
|
+
run(ctx, obj.api.update_device(obj.model, obj.id, orjson.loads(data)))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def reboot(ctx: typer.Context, force: bool = OPTION_FORCE) -> None:
|
|
189
|
+
"""Reboots the device."""
|
|
190
|
+
require_device_id(ctx)
|
|
191
|
+
obj: NVR | ProtectAdoptableDeviceModel = ctx.obj.device
|
|
192
|
+
|
|
193
|
+
if force or typer.confirm(f'Confirm reboot of "{obj.name}"" (id: {obj.id})'):
|
|
194
|
+
run(ctx, obj.reboot())
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def unadopt(ctx: typer.Context, force: bool = OPTION_FORCE) -> None:
|
|
198
|
+
"""Unadopt/Unmanage adopted device."""
|
|
199
|
+
require_device_id(ctx)
|
|
200
|
+
obj: ProtectAdoptableDeviceModel = ctx.obj.device
|
|
201
|
+
|
|
202
|
+
if force or typer.confirm(f'Confirm undopt of "{obj.name}"" (id: {obj.id})'):
|
|
203
|
+
run(ctx, obj.unadopt())
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def adopt(ctx: typer.Context, name: Optional[str] = typer.Argument(None)) -> None:
|
|
207
|
+
"""
|
|
208
|
+
Adopts a device.
|
|
209
|
+
|
|
210
|
+
By default, unadopted devices do not show up in the bootstrap. Use
|
|
211
|
+
`unifi-protect -u` to show unadopted devices.
|
|
212
|
+
"""
|
|
213
|
+
require_device_id(ctx)
|
|
214
|
+
obj: ProtectAdoptableDeviceModel = ctx.obj.device
|
|
215
|
+
|
|
216
|
+
run(ctx, obj.adopt(name))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def init_common_commands(
|
|
220
|
+
app: typer.Typer,
|
|
221
|
+
) -> tuple[dict[str, Callable[..., Any]], dict[str, Callable[..., Any]]]:
|
|
222
|
+
deviceless_commands: dict[str, Callable[..., Any]] = {}
|
|
223
|
+
device_commands: dict[str, Callable[..., Any]] = {}
|
|
224
|
+
|
|
225
|
+
deviceless_commands["list-ids"] = app.command()(list_ids)
|
|
226
|
+
device_commands["protect-url"] = app.command()(protect_url)
|
|
227
|
+
device_commands["is-wired"] = app.command()(is_wired)
|
|
228
|
+
device_commands["is-wifi"] = app.command()(is_wifi)
|
|
229
|
+
device_commands["is-bluetooth"] = app.command()(is_bluetooth)
|
|
230
|
+
device_commands["bridge"] = app.command()(bridge)
|
|
231
|
+
device_commands["set-ssh"] = app.command()(set_ssh)
|
|
232
|
+
device_commands["set-name"] = app.command()(set_name)
|
|
233
|
+
device_commands["update"] = app.command()(update)
|
|
234
|
+
device_commands["reboot"] = app.command()(reboot)
|
|
235
|
+
device_commands["unadopt"] = app.command()(unadopt)
|
|
236
|
+
device_commands["adopt"] = app.command()(adopt)
|
|
237
|
+
|
|
238
|
+
return deviceless_commands, device_commands
|