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
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import base64
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional, cast
|
|
9
|
+
|
|
10
|
+
import orjson
|
|
11
|
+
import typer
|
|
12
|
+
from rich.progress import track
|
|
13
|
+
|
|
14
|
+
from uiprotect.api import ProtectApiClient
|
|
15
|
+
from uiprotect.cli.base import CliContext, OutputFormatEnum
|
|
16
|
+
from uiprotect.cli.cameras import app as camera_app
|
|
17
|
+
from uiprotect.cli.chimes import app as chime_app
|
|
18
|
+
from uiprotect.cli.doorlocks import app as doorlock_app
|
|
19
|
+
from uiprotect.cli.events import app as event_app
|
|
20
|
+
from uiprotect.cli.lights import app as light_app
|
|
21
|
+
from uiprotect.cli.liveviews import app as liveview_app
|
|
22
|
+
from uiprotect.cli.nvr import app as nvr_app
|
|
23
|
+
from uiprotect.cli.sensors import app as sensor_app
|
|
24
|
+
from uiprotect.cli.viewers import app as viewer_app
|
|
25
|
+
from uiprotect.data import Version, WSPacket
|
|
26
|
+
from uiprotect.test_util import SampleDataGenerator
|
|
27
|
+
from uiprotect.utils import RELEASE_CACHE, get_local_timezone, run_async
|
|
28
|
+
from uiprotect.utils import profile_ws as profile_ws_job
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
from uiprotect.cli.backup import app as backup_app
|
|
32
|
+
except ImportError:
|
|
33
|
+
backup_app = None # type: ignore[assignment]
|
|
34
|
+
|
|
35
|
+
_LOGGER = logging.getLogger("uiprotect")
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
from IPython import embed
|
|
39
|
+
from termcolor import colored
|
|
40
|
+
from traitlets.config import get_config
|
|
41
|
+
except ImportError:
|
|
42
|
+
embed = termcolor = get_config = None # type: ignore[assignment]
|
|
43
|
+
|
|
44
|
+
OPTION_USERNAME = typer.Option(
|
|
45
|
+
...,
|
|
46
|
+
"--username",
|
|
47
|
+
"-U",
|
|
48
|
+
help="UniFi Protect username",
|
|
49
|
+
prompt=True,
|
|
50
|
+
envvar="UFP_USERNAME",
|
|
51
|
+
)
|
|
52
|
+
OPTION_PASSWORD = typer.Option(
|
|
53
|
+
...,
|
|
54
|
+
"--password",
|
|
55
|
+
"-P",
|
|
56
|
+
help="UniFi Protect password",
|
|
57
|
+
prompt=True,
|
|
58
|
+
hide_input=True,
|
|
59
|
+
envvar="UFP_PASSWORD",
|
|
60
|
+
)
|
|
61
|
+
OPTION_ADDRESS = typer.Option(
|
|
62
|
+
...,
|
|
63
|
+
"--address",
|
|
64
|
+
"-a",
|
|
65
|
+
prompt=True,
|
|
66
|
+
help="UniFi Protect IP address or hostname",
|
|
67
|
+
envvar="UFP_ADDRESS",
|
|
68
|
+
)
|
|
69
|
+
OPTION_PORT = typer.Option(
|
|
70
|
+
443,
|
|
71
|
+
"--port",
|
|
72
|
+
"-p",
|
|
73
|
+
help="UniFi Protect Port",
|
|
74
|
+
envvar="UFP_PORT",
|
|
75
|
+
)
|
|
76
|
+
OPTION_SECONDS = typer.Option(15, "--seconds", "-s", help="Seconds to pull events")
|
|
77
|
+
OPTION_VERIFY = typer.Option(
|
|
78
|
+
True,
|
|
79
|
+
"--no-verify",
|
|
80
|
+
help="Verify SSL",
|
|
81
|
+
envvar="UFP_SSL_VERIFY",
|
|
82
|
+
)
|
|
83
|
+
OPTION_ANON = typer.Option(True, "--actual", help="Do not anonymize test data")
|
|
84
|
+
OPTION_ZIP = typer.Option(False, "--zip", help="Zip up data after generate")
|
|
85
|
+
OPTION_WAIT = typer.Option(
|
|
86
|
+
30,
|
|
87
|
+
"--wait",
|
|
88
|
+
"-w",
|
|
89
|
+
help="Time to wait for Websocket messages",
|
|
90
|
+
)
|
|
91
|
+
OPTION_OUTPUT = typer.Option(
|
|
92
|
+
None,
|
|
93
|
+
"--output",
|
|
94
|
+
"-o",
|
|
95
|
+
help="Output folder, defaults to `tests` folder one level above this file",
|
|
96
|
+
envvar="UFP_SAMPLE_DIR",
|
|
97
|
+
)
|
|
98
|
+
OPTION_OUT_FORMAT = typer.Option(
|
|
99
|
+
OutputFormatEnum.PLAIN,
|
|
100
|
+
"--output-format",
|
|
101
|
+
help="Preferred output format. Not all commands support both JSON and plain and may still output in one or the other.",
|
|
102
|
+
)
|
|
103
|
+
OPTION_WS_FILE = typer.Option(
|
|
104
|
+
None,
|
|
105
|
+
"--file",
|
|
106
|
+
"-f",
|
|
107
|
+
help="Path or raw binary Websocket message",
|
|
108
|
+
)
|
|
109
|
+
OPTION_UNADOPTED = typer.Option(
|
|
110
|
+
False,
|
|
111
|
+
"-u",
|
|
112
|
+
"--include-unadopted",
|
|
113
|
+
help="Include devices not adopted by this NVR.",
|
|
114
|
+
)
|
|
115
|
+
ARG_WS_DATA = typer.Argument(None, help="base64 encoded Websocket message")
|
|
116
|
+
|
|
117
|
+
SLEEP_INTERVAL = 2
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
app = typer.Typer(rich_markup_mode="rich")
|
|
121
|
+
app.add_typer(nvr_app, name="nvr")
|
|
122
|
+
app.add_typer(event_app, name="events")
|
|
123
|
+
app.add_typer(liveview_app, name="liveviews")
|
|
124
|
+
app.add_typer(camera_app, name="cameras")
|
|
125
|
+
app.add_typer(chime_app, name="chimes")
|
|
126
|
+
app.add_typer(doorlock_app, name="doorlocks")
|
|
127
|
+
app.add_typer(light_app, name="lights")
|
|
128
|
+
app.add_typer(sensor_app, name="sensors")
|
|
129
|
+
app.add_typer(viewer_app, name="viewers")
|
|
130
|
+
|
|
131
|
+
if backup_app is not None:
|
|
132
|
+
app.add_typer(backup_app, name="backup")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@app.callback()
|
|
136
|
+
def main(
|
|
137
|
+
ctx: typer.Context,
|
|
138
|
+
username: str = OPTION_USERNAME,
|
|
139
|
+
password: str = OPTION_PASSWORD,
|
|
140
|
+
address: str = OPTION_ADDRESS,
|
|
141
|
+
port: int = OPTION_PORT,
|
|
142
|
+
verify: bool = OPTION_VERIFY,
|
|
143
|
+
output_format: OutputFormatEnum = OPTION_OUT_FORMAT,
|
|
144
|
+
include_unadopted: bool = OPTION_UNADOPTED,
|
|
145
|
+
) -> None:
|
|
146
|
+
"""UniFi Protect CLI"""
|
|
147
|
+
# preload the timezone before any async code runs
|
|
148
|
+
get_local_timezone()
|
|
149
|
+
|
|
150
|
+
protect = ProtectApiClient(
|
|
151
|
+
address,
|
|
152
|
+
port,
|
|
153
|
+
username,
|
|
154
|
+
password,
|
|
155
|
+
verify_ssl=verify,
|
|
156
|
+
ignore_unadopted=not include_unadopted,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
async def update() -> None:
|
|
160
|
+
protect._bootstrap = await protect.get_bootstrap()
|
|
161
|
+
await protect.close_session()
|
|
162
|
+
|
|
163
|
+
run_async(update())
|
|
164
|
+
ctx.obj = CliContext(protect=protect, output_format=output_format)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _setup_logger(level: int = logging.DEBUG, show_level: bool = False) -> None:
|
|
168
|
+
console_handler = logging.StreamHandler()
|
|
169
|
+
console_handler.setLevel(level)
|
|
170
|
+
if show_level:
|
|
171
|
+
formatter = logging.Formatter("%(levelname)s: %(message)s")
|
|
172
|
+
console_handler.setFormatter(formatter)
|
|
173
|
+
_LOGGER.setLevel(logging.DEBUG)
|
|
174
|
+
_LOGGER.addHandler(console_handler)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def _progress_bar(wait_time: int, label: str) -> None:
|
|
178
|
+
for i in track(range(wait_time // SLEEP_INTERVAL), description=label):
|
|
179
|
+
if i > 0:
|
|
180
|
+
await asyncio.sleep(SLEEP_INTERVAL)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@app.command()
|
|
184
|
+
def shell(ctx: typer.Context) -> None:
|
|
185
|
+
"""
|
|
186
|
+
Opens iPython shell with Protect client initialized.
|
|
187
|
+
|
|
188
|
+
Requires the `shell` extra to also be installed.
|
|
189
|
+
"""
|
|
190
|
+
if embed is None or colored is None:
|
|
191
|
+
typer.echo("ipython and termcolor required for shell subcommand")
|
|
192
|
+
sys.exit(1)
|
|
193
|
+
|
|
194
|
+
# locals passed to shell
|
|
195
|
+
protect = cast(
|
|
196
|
+
ProtectApiClient,
|
|
197
|
+
ctx.obj.protect,
|
|
198
|
+
)
|
|
199
|
+
_setup_logger(show_level=True)
|
|
200
|
+
|
|
201
|
+
async def wait_forever() -> None:
|
|
202
|
+
await protect.update()
|
|
203
|
+
while True:
|
|
204
|
+
await asyncio.sleep(10)
|
|
205
|
+
await protect.update()
|
|
206
|
+
|
|
207
|
+
c = get_config()
|
|
208
|
+
c.InteractiveShellEmbed.colors = "Linux"
|
|
209
|
+
embed( # type: ignore[no-untyped-call]
|
|
210
|
+
header=colored("protect = ProtectApiClient(*args)", "green"),
|
|
211
|
+
config=c,
|
|
212
|
+
using="asyncio",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@app.command()
|
|
217
|
+
def generate_sample_data(
|
|
218
|
+
ctx: typer.Context,
|
|
219
|
+
anonymize: bool = OPTION_ANON,
|
|
220
|
+
wait_time: int = OPTION_WAIT,
|
|
221
|
+
output_folder: Optional[Path] = OPTION_OUTPUT,
|
|
222
|
+
do_zip: bool = OPTION_ZIP,
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Generates sample data for UniFi Protect instance."""
|
|
225
|
+
protect = cast(ProtectApiClient, ctx.obj.protect)
|
|
226
|
+
|
|
227
|
+
if output_folder is None:
|
|
228
|
+
tests_folder = Path(__file__).parent.parent / "tests"
|
|
229
|
+
|
|
230
|
+
if not tests_folder.exists():
|
|
231
|
+
typer.secho("Output folder required when not in dev-mode", fg="red")
|
|
232
|
+
sys.exit(1)
|
|
233
|
+
output_folder = (tests_folder / "sample_data").absolute()
|
|
234
|
+
|
|
235
|
+
def log(msg: str) -> None:
|
|
236
|
+
typer.echo(msg)
|
|
237
|
+
|
|
238
|
+
def log_warning(msg: str) -> None:
|
|
239
|
+
typer.secho(msg, fg="yellow")
|
|
240
|
+
|
|
241
|
+
SampleDataGenerator(
|
|
242
|
+
protect,
|
|
243
|
+
output_folder,
|
|
244
|
+
anonymize,
|
|
245
|
+
wait_time,
|
|
246
|
+
log=log,
|
|
247
|
+
log_warning=log_warning,
|
|
248
|
+
ws_progress=_progress_bar,
|
|
249
|
+
do_zip=do_zip,
|
|
250
|
+
).generate()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@app.command()
|
|
254
|
+
def profile_ws(
|
|
255
|
+
ctx: typer.Context,
|
|
256
|
+
wait_time: int = OPTION_WAIT,
|
|
257
|
+
output_path: Optional[Path] = OPTION_OUTPUT,
|
|
258
|
+
) -> None:
|
|
259
|
+
"""Profiles Websocket messages for UniFi Protect instance."""
|
|
260
|
+
protect = cast(ProtectApiClient, ctx.obj.protect)
|
|
261
|
+
|
|
262
|
+
async def callback() -> None:
|
|
263
|
+
await protect.update()
|
|
264
|
+
await profile_ws_job(
|
|
265
|
+
protect,
|
|
266
|
+
wait_time,
|
|
267
|
+
output_path=output_path,
|
|
268
|
+
ws_progress=_progress_bar,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
_setup_logger()
|
|
272
|
+
|
|
273
|
+
run_async(callback())
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@app.command()
|
|
277
|
+
def decode_ws_msg(
|
|
278
|
+
ws_file: typer.FileBinaryRead = OPTION_WS_FILE,
|
|
279
|
+
ws_data: Optional[str] = ARG_WS_DATA,
|
|
280
|
+
) -> None:
|
|
281
|
+
"""Decodes a base64 encoded UniFi Protect Websocket binary message."""
|
|
282
|
+
if ws_file is None and ws_data is None: # type: ignore[unreachable]
|
|
283
|
+
typer.secho("Websocket data required", fg="red") # type: ignore[unreachable]
|
|
284
|
+
sys.exit(1)
|
|
285
|
+
|
|
286
|
+
ws_data_raw = b""
|
|
287
|
+
if ws_file is not None:
|
|
288
|
+
ws_data_raw = ws_file.read()
|
|
289
|
+
elif ws_data is not None: # type: ignore[unreachable]
|
|
290
|
+
ws_data_raw = base64.b64decode(ws_data.encode("utf8"))
|
|
291
|
+
|
|
292
|
+
packet = WSPacket(ws_data_raw)
|
|
293
|
+
response = {"action": packet.action_frame.data, "data": packet.data_frame.data}
|
|
294
|
+
|
|
295
|
+
typer.echo(orjson.dumps(response).decode("utf-8"))
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@app.command()
|
|
299
|
+
def release_versions(ctx: typer.Context) -> None:
|
|
300
|
+
"""Updates the release version cache on disk."""
|
|
301
|
+
protect = cast(ProtectApiClient, ctx.obj.protect)
|
|
302
|
+
|
|
303
|
+
async def callback() -> set[Version]:
|
|
304
|
+
versions = await protect.get_release_versions()
|
|
305
|
+
await protect.close_session()
|
|
306
|
+
return versions
|
|
307
|
+
|
|
308
|
+
_setup_logger()
|
|
309
|
+
|
|
310
|
+
versions = run_async(callback())
|
|
311
|
+
output = orjson.dumps(sorted([str(v) for v in versions]))
|
|
312
|
+
|
|
313
|
+
Path(RELEASE_CACHE).write_bytes(output)
|
|
314
|
+
typer.echo(output.decode("utf-8"))
|