uiprotect 7.11.0__py3-none-any.whl → 7.13.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/api.py +52 -15
- uiprotect/cli/__init__.py +28 -1
- uiprotect/data/nvr.py +4 -0
- {uiprotect-7.11.0.dist-info → uiprotect-7.13.0.dist-info}/METADATA +1 -1
- {uiprotect-7.11.0.dist-info → uiprotect-7.13.0.dist-info}/RECORD +8 -8
- {uiprotect-7.11.0.dist-info → uiprotect-7.13.0.dist-info}/LICENSE +0 -0
- {uiprotect-7.11.0.dist-info → uiprotect-7.13.0.dist-info}/WHEEL +0 -0
- {uiprotect-7.11.0.dist-info → uiprotect-7.13.0.dist-info}/entry_points.txt +0 -0
uiprotect/api.py
CHANGED
|
@@ -28,6 +28,7 @@ from platformdirs import user_cache_dir, user_config_dir
|
|
|
28
28
|
from yarl import URL
|
|
29
29
|
|
|
30
30
|
from uiprotect.data.convert import list_from_unifi_list
|
|
31
|
+
from uiprotect.data.nvr import MetaInfo
|
|
31
32
|
from uiprotect.data.user import Keyring, Keyrings, UlpUser, UlpUsers
|
|
32
33
|
|
|
33
34
|
from ._compat import cached_property
|
|
@@ -163,6 +164,7 @@ class BaseApiClient:
|
|
|
163
164
|
_port: int
|
|
164
165
|
_username: str
|
|
165
166
|
_password: str
|
|
167
|
+
_api_key: str | None = None
|
|
166
168
|
_verify_ssl: bool
|
|
167
169
|
_ws_timeout: int
|
|
168
170
|
|
|
@@ -174,10 +176,11 @@ class BaseApiClient:
|
|
|
174
176
|
_cookiename = "TOKEN"
|
|
175
177
|
|
|
176
178
|
headers: dict[str, str] | None = None
|
|
177
|
-
|
|
179
|
+
_private_websocket: Websocket | None = None
|
|
178
180
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
private_api_path: str = "/proxy/protect/api/"
|
|
182
|
+
public_api_path: str = "/proxy/protect/integration"
|
|
183
|
+
private_ws_path: str = "/proxy/protect/ws/updates"
|
|
181
184
|
|
|
182
185
|
cache_dir: Path
|
|
183
186
|
config_dir: Path
|
|
@@ -189,6 +192,7 @@ class BaseApiClient:
|
|
|
189
192
|
port: int,
|
|
190
193
|
username: str,
|
|
191
194
|
password: str,
|
|
195
|
+
api_key: str | None = None,
|
|
192
196
|
verify_ssl: bool = True,
|
|
193
197
|
session: aiohttp.ClientSession | None = None,
|
|
194
198
|
ws_timeout: int = 30,
|
|
@@ -203,6 +207,7 @@ class BaseApiClient:
|
|
|
203
207
|
|
|
204
208
|
self._username = username
|
|
205
209
|
self._password = password
|
|
210
|
+
self._api_key = api_key
|
|
206
211
|
self._verify_ssl = verify_ssl
|
|
207
212
|
self._ws_timeout = ws_timeout
|
|
208
213
|
self._ws_receive_timeout = ws_receive_timeout
|
|
@@ -226,10 +231,10 @@ class BaseApiClient:
|
|
|
226
231
|
"""Updates the url after changing _host or _port."""
|
|
227
232
|
if self._port != 443:
|
|
228
233
|
self._url = URL(f"https://{self._host}:{self._port}")
|
|
229
|
-
self._ws_url = URL(f"wss://{self._host}:{self._port}{self.
|
|
234
|
+
self._ws_url = URL(f"wss://{self._host}:{self._port}{self.private_ws_path}")
|
|
230
235
|
else:
|
|
231
236
|
self._url = URL(f"https://{self._host}")
|
|
232
|
-
self._ws_url = URL(f"wss://{self._host}{self.
|
|
237
|
+
self._ws_url = URL(f"wss://{self._host}{self.private_ws_path}")
|
|
233
238
|
|
|
234
239
|
self.base_url = str(self._url)
|
|
235
240
|
|
|
@@ -273,8 +278,8 @@ class BaseApiClient:
|
|
|
273
278
|
|
|
274
279
|
def _get_websocket(self) -> Websocket:
|
|
275
280
|
"""Gets or creates current Websocket."""
|
|
276
|
-
if self.
|
|
277
|
-
self.
|
|
281
|
+
if self._private_websocket is None:
|
|
282
|
+
self._private_websocket = Websocket(
|
|
278
283
|
self._get_websocket_url,
|
|
279
284
|
self._auth_websocket,
|
|
280
285
|
self._update_bootstrap_soon,
|
|
@@ -285,7 +290,7 @@ class BaseApiClient:
|
|
|
285
290
|
timeout=self._ws_timeout,
|
|
286
291
|
receive_timeout=self._ws_receive_timeout,
|
|
287
292
|
)
|
|
288
|
-
return self.
|
|
293
|
+
return self._private_websocket
|
|
289
294
|
|
|
290
295
|
def _update_bootstrap_soon(self) -> None:
|
|
291
296
|
"""Update bootstrap soon."""
|
|
@@ -325,16 +330,21 @@ class BaseApiClient:
|
|
|
325
330
|
url: str,
|
|
326
331
|
require_auth: bool = False,
|
|
327
332
|
auto_close: bool = True,
|
|
333
|
+
public_api: bool = False,
|
|
328
334
|
**kwargs: Any,
|
|
329
335
|
) -> aiohttp.ClientResponse:
|
|
330
336
|
"""Make a request to UniFi Protect"""
|
|
331
|
-
if require_auth:
|
|
337
|
+
if require_auth and not public_api:
|
|
332
338
|
await self.ensure_authenticated()
|
|
333
339
|
|
|
334
340
|
request_url = self._url.join(
|
|
335
341
|
URL(SplitResult("", "", url, "", ""), encoded=True)
|
|
336
342
|
)
|
|
337
|
-
headers = kwargs.get("headers") or self.headers
|
|
343
|
+
headers = kwargs.get("headers") or self.headers or {}
|
|
344
|
+
if require_auth and public_api:
|
|
345
|
+
if self._api_key is None:
|
|
346
|
+
raise NotAuthorized("API key is required for public API requests")
|
|
347
|
+
headers = {"X-API-KEY": self._api_key}
|
|
338
348
|
_LOGGER.debug("Request url: %s", request_url)
|
|
339
349
|
if not self._verify_ssl:
|
|
340
350
|
kwargs["ssl"] = False
|
|
@@ -391,16 +401,22 @@ class BaseApiClient:
|
|
|
391
401
|
require_auth: bool = True,
|
|
392
402
|
raise_exception: bool = True,
|
|
393
403
|
api_path: str | None = None,
|
|
404
|
+
public_api: bool = False,
|
|
394
405
|
**kwargs: Any,
|
|
395
406
|
) -> bytes | None:
|
|
396
407
|
"""Make a API request"""
|
|
397
|
-
path =
|
|
408
|
+
path = self.private_api_path
|
|
409
|
+
if api_path is not None:
|
|
410
|
+
path = api_path
|
|
411
|
+
elif public_api:
|
|
412
|
+
path = self.public_api_path
|
|
398
413
|
|
|
399
414
|
response = await self.request(
|
|
400
415
|
method,
|
|
401
416
|
f"{path}{url}",
|
|
402
417
|
require_auth=require_auth,
|
|
403
418
|
auto_close=False,
|
|
419
|
+
public_api=public_api,
|
|
404
420
|
**kwargs,
|
|
405
421
|
)
|
|
406
422
|
|
|
@@ -453,6 +469,7 @@ class BaseApiClient:
|
|
|
453
469
|
require_auth: bool = True,
|
|
454
470
|
raise_exception: bool = True,
|
|
455
471
|
api_path: str | None = None,
|
|
472
|
+
public_api: bool = False,
|
|
456
473
|
**kwargs: Any,
|
|
457
474
|
) -> list[Any] | dict[str, Any] | None:
|
|
458
475
|
data = await self.api_request_raw(
|
|
@@ -461,6 +478,7 @@ class BaseApiClient:
|
|
|
461
478
|
require_auth=require_auth,
|
|
462
479
|
raise_exception=raise_exception,
|
|
463
480
|
api_path=api_path,
|
|
481
|
+
public_api=public_api,
|
|
464
482
|
**kwargs,
|
|
465
483
|
)
|
|
466
484
|
|
|
@@ -480,6 +498,7 @@ class BaseApiClient:
|
|
|
480
498
|
method: str = "get",
|
|
481
499
|
require_auth: bool = True,
|
|
482
500
|
raise_exception: bool = True,
|
|
501
|
+
public_api: bool = False,
|
|
483
502
|
**kwargs: Any,
|
|
484
503
|
) -> dict[str, Any]:
|
|
485
504
|
data = await self.api_request(
|
|
@@ -487,6 +506,7 @@ class BaseApiClient:
|
|
|
487
506
|
method=method,
|
|
488
507
|
require_auth=require_auth,
|
|
489
508
|
raise_exception=raise_exception,
|
|
509
|
+
public_api=public_api,
|
|
490
510
|
**kwargs,
|
|
491
511
|
)
|
|
492
512
|
|
|
@@ -501,6 +521,7 @@ class BaseApiClient:
|
|
|
501
521
|
method: str = "get",
|
|
502
522
|
require_auth: bool = True,
|
|
503
523
|
raise_exception: bool = True,
|
|
524
|
+
public_api: bool = False,
|
|
504
525
|
**kwargs: Any,
|
|
505
526
|
) -> list[Any]:
|
|
506
527
|
data = await self.api_request(
|
|
@@ -508,6 +529,7 @@ class BaseApiClient:
|
|
|
508
529
|
method=method,
|
|
509
530
|
require_auth=require_auth,
|
|
510
531
|
raise_exception=raise_exception,
|
|
532
|
+
public_api=public_api,
|
|
511
533
|
**kwargs,
|
|
512
534
|
)
|
|
513
535
|
|
|
@@ -686,11 +708,11 @@ class BaseApiClient:
|
|
|
686
708
|
|
|
687
709
|
async def async_disconnect_ws(self) -> None:
|
|
688
710
|
"""Disconnect from Websocket."""
|
|
689
|
-
if self.
|
|
711
|
+
if self._private_websocket:
|
|
690
712
|
websocket = self._get_websocket()
|
|
691
713
|
websocket.stop()
|
|
692
714
|
await websocket.wait_closed()
|
|
693
|
-
self.
|
|
715
|
+
self._private_websocket = None
|
|
694
716
|
|
|
695
717
|
def _process_ws_message(self, msg: aiohttp.WSMessage) -> None:
|
|
696
718
|
raise NotImplementedError
|
|
@@ -727,6 +749,7 @@ class ProtectApiClient(BaseApiClient):
|
|
|
727
749
|
port: UFP HTTPS port
|
|
728
750
|
username: UFP username
|
|
729
751
|
password: UFP password
|
|
752
|
+
api_key: API key for UFP
|
|
730
753
|
verify_ssl: Verify HTTPS certificate (default: `True`)
|
|
731
754
|
session: Optional aiohttp session to use (default: generate one)
|
|
732
755
|
override_connection_host: Use `host` as your `connection_host` for RTSP stream instead of using the one provided by UniFi Protect.
|
|
@@ -754,6 +777,7 @@ class ProtectApiClient(BaseApiClient):
|
|
|
754
777
|
port: int,
|
|
755
778
|
username: str,
|
|
756
779
|
password: str,
|
|
780
|
+
api_key: str | None = None,
|
|
757
781
|
verify_ssl: bool = True,
|
|
758
782
|
session: aiohttp.ClientSession | None = None,
|
|
759
783
|
ws_timeout: int = 30,
|
|
@@ -773,6 +797,7 @@ class ProtectApiClient(BaseApiClient):
|
|
|
773
797
|
port=port,
|
|
774
798
|
username=username,
|
|
775
799
|
password=password,
|
|
800
|
+
api_key=api_key,
|
|
776
801
|
verify_ssl=verify_ssl,
|
|
777
802
|
session=session,
|
|
778
803
|
ws_timeout=ws_timeout,
|
|
@@ -1584,10 +1609,12 @@ class ProtectApiClient(BaseApiClient):
|
|
|
1584
1609
|
raise_exception=False,
|
|
1585
1610
|
)
|
|
1586
1611
|
|
|
1587
|
-
_LOGGER.debug(
|
|
1612
|
+
_LOGGER.debug(
|
|
1613
|
+
"Requesting camera video: %s%s %s", self.private_api_path, path, params
|
|
1614
|
+
)
|
|
1588
1615
|
r = await self.request(
|
|
1589
1616
|
"get",
|
|
1590
|
-
f"{self.
|
|
1617
|
+
f"{self.private_api_path}{path}",
|
|
1591
1618
|
auto_close=False,
|
|
1592
1619
|
timeout=0,
|
|
1593
1620
|
params=params,
|
|
@@ -2072,3 +2099,13 @@ class ProtectApiClient(BaseApiClient):
|
|
|
2072
2099
|
raise BadRequest("Failed to create API key")
|
|
2073
2100
|
|
|
2074
2101
|
return response["data"]["full_api_key"]
|
|
2102
|
+
|
|
2103
|
+
async def get_meta_info(self) -> MetaInfo:
|
|
2104
|
+
"""Get metadata about the NVR."""
|
|
2105
|
+
data = await self.api_request(
|
|
2106
|
+
url="/v1/meta/info",
|
|
2107
|
+
public_api=True,
|
|
2108
|
+
)
|
|
2109
|
+
if not isinstance(data, dict):
|
|
2110
|
+
raise NvrError("Failed to retrieve meta info from public API")
|
|
2111
|
+
return MetaInfo(**data)
|
uiprotect/cli/__init__.py
CHANGED
|
@@ -11,7 +11,7 @@ import orjson
|
|
|
11
11
|
import typer
|
|
12
12
|
from rich.progress import track
|
|
13
13
|
|
|
14
|
-
from uiprotect.api import ProtectApiClient
|
|
14
|
+
from uiprotect.api import MetaInfo, ProtectApiClient
|
|
15
15
|
|
|
16
16
|
from ..data import Version, WSPacket
|
|
17
17
|
from ..test_util import SampleDataGenerator
|
|
@@ -60,6 +60,15 @@ OPTION_PASSWORD = typer.Option(
|
|
|
60
60
|
hide_input=True,
|
|
61
61
|
envvar="UFP_PASSWORD",
|
|
62
62
|
)
|
|
63
|
+
OPTION_API_KEY = typer.Option(
|
|
64
|
+
...,
|
|
65
|
+
"--api-key",
|
|
66
|
+
"-k",
|
|
67
|
+
help="UniFi Protect API key",
|
|
68
|
+
prompt=True,
|
|
69
|
+
hide_input=True,
|
|
70
|
+
envvar="UFP_API_KEY",
|
|
71
|
+
)
|
|
63
72
|
OPTION_ADDRESS = typer.Option(
|
|
64
73
|
...,
|
|
65
74
|
"--address",
|
|
@@ -140,6 +149,7 @@ def main(
|
|
|
140
149
|
ctx: typer.Context,
|
|
141
150
|
username: str = OPTION_USERNAME,
|
|
142
151
|
password: str = OPTION_PASSWORD,
|
|
152
|
+
api_key: str = OPTION_API_KEY,
|
|
143
153
|
address: str = OPTION_ADDRESS,
|
|
144
154
|
port: int = OPTION_PORT,
|
|
145
155
|
verify: bool = OPTION_VERIFY,
|
|
@@ -155,6 +165,7 @@ def main(
|
|
|
155
165
|
port,
|
|
156
166
|
username,
|
|
157
167
|
password,
|
|
168
|
+
api_key,
|
|
158
169
|
verify_ssl=verify,
|
|
159
170
|
ignore_unadopted=not include_unadopted,
|
|
160
171
|
)
|
|
@@ -338,3 +349,19 @@ def create_api_key(
|
|
|
338
349
|
_setup_logger()
|
|
339
350
|
result = run_async(callback())
|
|
340
351
|
typer.echo(result)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@app.command()
|
|
355
|
+
def get_meta_info(ctx: typer.Context) -> None:
|
|
356
|
+
"""Get metadata about the current UniFi Protect instance."""
|
|
357
|
+
protect = cast(ProtectApiClient, ctx.obj.protect)
|
|
358
|
+
|
|
359
|
+
async def callback() -> MetaInfo:
|
|
360
|
+
meta = await protect.get_meta_info()
|
|
361
|
+
await protect.close_session()
|
|
362
|
+
return meta
|
|
363
|
+
|
|
364
|
+
_setup_logger()
|
|
365
|
+
|
|
366
|
+
result = run_async(callback())
|
|
367
|
+
typer.echo(result.model_dump_json())
|
uiprotect/data/nvr.py
CHANGED
|
@@ -71,6 +71,10 @@ DELETE_KEYS_THUMB = {"color", "vehicleType"}
|
|
|
71
71
|
DELETE_KEYS_EVENT = {"deletedAt", "category", "subCategory"}
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
class MetaInfo(ProtectBaseObject):
|
|
75
|
+
applicationVersion: str
|
|
76
|
+
|
|
77
|
+
|
|
74
78
|
class NVRLocation(UserLocation):
|
|
75
79
|
is_geofencing_enabled: bool
|
|
76
80
|
radius: int
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
uiprotect/__init__.py,sha256=Oz6i1tonIz4QWVnEPkbielJDJ3WQdwZVgYtjY4IwGAQ,636
|
|
2
2
|
uiprotect/__main__.py,sha256=C_bHCOkv5qj6WMy-6ELoY3Y6HDhLxOa1a30CzmbZhsg,462
|
|
3
3
|
uiprotect/_compat.py,sha256=HThmb1zQZCEssCxYYbQzFhJq8zYYlVaSnIEZabKc-6U,302
|
|
4
|
-
uiprotect/api.py,sha256=
|
|
5
|
-
uiprotect/cli/__init__.py,sha256=
|
|
4
|
+
uiprotect/api.py,sha256=R8-R3AmIxKDETndph2xelWOA0IDyM0VHl8ZLdftfAoI,73044
|
|
5
|
+
uiprotect/cli/__init__.py,sha256=4nvTO0tW5aVforlh--_NwmGvxTvpOfmAiso5-acY850,10075
|
|
6
6
|
uiprotect/cli/aiports.py,sha256=wpEr2w_hY18CGpFiQM2Yc0FiVwG_1l2CzZhZLGNigvI,1576
|
|
7
7
|
uiprotect/cli/backup.py,sha256=ZiS7RZnJGKI8TJKLW2cOUzkRM8nyTvE5Ov_jZZGtvSM,36708
|
|
8
8
|
uiprotect/cli/base.py,sha256=5-z-IS8g9iQqhR_YbjxaJAFiMMAY_7cCtNAtvLdRCoM,7524
|
|
@@ -20,7 +20,7 @@ uiprotect/data/base.py,sha256=CXIxaJCJNpavadn6uF7vlhI4wxkfD8MNBGgR-smRA7k,35401
|
|
|
20
20
|
uiprotect/data/bootstrap.py,sha256=ddNaKrTprN7Zq0ZE3O_F5whepUh6z9GqyrUWxLyZ0HE,23570
|
|
21
21
|
uiprotect/data/convert.py,sha256=xEN878_hm0HZZCVYGwJSxcSp2as9zpkvsemVIibReOA,2628
|
|
22
22
|
uiprotect/data/devices.py,sha256=akNyLQKCNI8SiA7oUW1cSvE-IZ9Oav8iv9PfESpjits,115839
|
|
23
|
-
uiprotect/data/nvr.py,sha256=
|
|
23
|
+
uiprotect/data/nvr.py,sha256=lRBCKwAw6GhV7NFluouZkZEWln_faAABfYJXMrTGDZI,47772
|
|
24
24
|
uiprotect/data/types.py,sha256=Hfm-Y3MSZtFgZ5AvEvhGqJP_YXPgYSUNybheW91RhOc,19284
|
|
25
25
|
uiprotect/data/user.py,sha256=Del5LUmt5uCfAQMI9-kl_GaKm085oTLjxmcCrlEKXxc,10526
|
|
26
26
|
uiprotect/data/websocket.py,sha256=m4EV1Qfh08eKOihy70ycViYgEQpeNSGZQJWdtGIYJDA,6791
|
|
@@ -32,8 +32,8 @@ uiprotect/test_util/__init__.py,sha256=HlQBgIgdtrvT-gQ5OWP92LbgVr_YzsD5NFImLRonU
|
|
|
32
32
|
uiprotect/test_util/anonymize.py,sha256=f-8ijU-_y9r-uAbhIPn0f0I6hzJpAkvJzc8UpWihObI,8478
|
|
33
33
|
uiprotect/utils.py,sha256=5Z30chqnQhQdtD1vabRtZXjBpeiJagL5KDuALQlWxk8,20559
|
|
34
34
|
uiprotect/websocket.py,sha256=tEyenqblNXHcjWYuf4oRP1E7buNwx6zoECMwpBr-jig,8191
|
|
35
|
-
uiprotect-7.
|
|
36
|
-
uiprotect-7.
|
|
37
|
-
uiprotect-7.
|
|
38
|
-
uiprotect-7.
|
|
39
|
-
uiprotect-7.
|
|
35
|
+
uiprotect-7.13.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
|
|
36
|
+
uiprotect-7.13.0.dist-info/METADATA,sha256=tfjdL8uSLRjqEI0NxX_AbRPV5NEP2KTS9EvFdg2b_8g,11109
|
|
37
|
+
uiprotect-7.13.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
38
|
+
uiprotect-7.13.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
|
|
39
|
+
uiprotect-7.13.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|