uiprotect 7.12.0__tar.gz → 7.13.0__tar.gz
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-7.12.0 → uiprotect-7.13.0}/PKG-INFO +1 -1
- {uiprotect-7.12.0 → uiprotect-7.13.0}/pyproject.toml +1 -1
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/api.py +46 -15
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/__init__.py +28 -1
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/data/nvr.py +4 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/LICENSE +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/README.md +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/__init__.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/__main__.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/_compat.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/aiports.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/backup.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/base.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/cameras.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/chimes.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/doorlocks.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/events.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/lights.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/liveviews.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/nvr.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/sensors.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/cli/viewers.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/data/__init__.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/data/base.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/data/bootstrap.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/data/convert.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/data/devices.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/data/types.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/data/user.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/data/websocket.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/exceptions.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/py.typed +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/release_cache.json +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/stream.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/test_util/__init__.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/test_util/anonymize.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/utils.py +0 -0
- {uiprotect-7.12.0 → uiprotect-7.13.0}/src/uiprotect/websocket.py +0 -0
|
@@ -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
|
|
@@ -175,10 +176,11 @@ class BaseApiClient:
|
|
|
175
176
|
_cookiename = "TOKEN"
|
|
176
177
|
|
|
177
178
|
headers: dict[str, str] | None = None
|
|
178
|
-
|
|
179
|
+
_private_websocket: Websocket | None = None
|
|
179
180
|
|
|
180
|
-
|
|
181
|
-
|
|
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"
|
|
182
184
|
|
|
183
185
|
cache_dir: Path
|
|
184
186
|
config_dir: Path
|
|
@@ -229,10 +231,10 @@ class BaseApiClient:
|
|
|
229
231
|
"""Updates the url after changing _host or _port."""
|
|
230
232
|
if self._port != 443:
|
|
231
233
|
self._url = URL(f"https://{self._host}:{self._port}")
|
|
232
|
-
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}")
|
|
233
235
|
else:
|
|
234
236
|
self._url = URL(f"https://{self._host}")
|
|
235
|
-
self._ws_url = URL(f"wss://{self._host}{self.
|
|
237
|
+
self._ws_url = URL(f"wss://{self._host}{self.private_ws_path}")
|
|
236
238
|
|
|
237
239
|
self.base_url = str(self._url)
|
|
238
240
|
|
|
@@ -276,8 +278,8 @@ class BaseApiClient:
|
|
|
276
278
|
|
|
277
279
|
def _get_websocket(self) -> Websocket:
|
|
278
280
|
"""Gets or creates current Websocket."""
|
|
279
|
-
if self.
|
|
280
|
-
self.
|
|
281
|
+
if self._private_websocket is None:
|
|
282
|
+
self._private_websocket = Websocket(
|
|
281
283
|
self._get_websocket_url,
|
|
282
284
|
self._auth_websocket,
|
|
283
285
|
self._update_bootstrap_soon,
|
|
@@ -288,7 +290,7 @@ class BaseApiClient:
|
|
|
288
290
|
timeout=self._ws_timeout,
|
|
289
291
|
receive_timeout=self._ws_receive_timeout,
|
|
290
292
|
)
|
|
291
|
-
return self.
|
|
293
|
+
return self._private_websocket
|
|
292
294
|
|
|
293
295
|
def _update_bootstrap_soon(self) -> None:
|
|
294
296
|
"""Update bootstrap soon."""
|
|
@@ -328,16 +330,21 @@ class BaseApiClient:
|
|
|
328
330
|
url: str,
|
|
329
331
|
require_auth: bool = False,
|
|
330
332
|
auto_close: bool = True,
|
|
333
|
+
public_api: bool = False,
|
|
331
334
|
**kwargs: Any,
|
|
332
335
|
) -> aiohttp.ClientResponse:
|
|
333
336
|
"""Make a request to UniFi Protect"""
|
|
334
|
-
if require_auth:
|
|
337
|
+
if require_auth and not public_api:
|
|
335
338
|
await self.ensure_authenticated()
|
|
336
339
|
|
|
337
340
|
request_url = self._url.join(
|
|
338
341
|
URL(SplitResult("", "", url, "", ""), encoded=True)
|
|
339
342
|
)
|
|
340
|
-
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}
|
|
341
348
|
_LOGGER.debug("Request url: %s", request_url)
|
|
342
349
|
if not self._verify_ssl:
|
|
343
350
|
kwargs["ssl"] = False
|
|
@@ -394,16 +401,22 @@ class BaseApiClient:
|
|
|
394
401
|
require_auth: bool = True,
|
|
395
402
|
raise_exception: bool = True,
|
|
396
403
|
api_path: str | None = None,
|
|
404
|
+
public_api: bool = False,
|
|
397
405
|
**kwargs: Any,
|
|
398
406
|
) -> bytes | None:
|
|
399
407
|
"""Make a API request"""
|
|
400
|
-
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
|
|
401
413
|
|
|
402
414
|
response = await self.request(
|
|
403
415
|
method,
|
|
404
416
|
f"{path}{url}",
|
|
405
417
|
require_auth=require_auth,
|
|
406
418
|
auto_close=False,
|
|
419
|
+
public_api=public_api,
|
|
407
420
|
**kwargs,
|
|
408
421
|
)
|
|
409
422
|
|
|
@@ -456,6 +469,7 @@ class BaseApiClient:
|
|
|
456
469
|
require_auth: bool = True,
|
|
457
470
|
raise_exception: bool = True,
|
|
458
471
|
api_path: str | None = None,
|
|
472
|
+
public_api: bool = False,
|
|
459
473
|
**kwargs: Any,
|
|
460
474
|
) -> list[Any] | dict[str, Any] | None:
|
|
461
475
|
data = await self.api_request_raw(
|
|
@@ -464,6 +478,7 @@ class BaseApiClient:
|
|
|
464
478
|
require_auth=require_auth,
|
|
465
479
|
raise_exception=raise_exception,
|
|
466
480
|
api_path=api_path,
|
|
481
|
+
public_api=public_api,
|
|
467
482
|
**kwargs,
|
|
468
483
|
)
|
|
469
484
|
|
|
@@ -483,6 +498,7 @@ class BaseApiClient:
|
|
|
483
498
|
method: str = "get",
|
|
484
499
|
require_auth: bool = True,
|
|
485
500
|
raise_exception: bool = True,
|
|
501
|
+
public_api: bool = False,
|
|
486
502
|
**kwargs: Any,
|
|
487
503
|
) -> dict[str, Any]:
|
|
488
504
|
data = await self.api_request(
|
|
@@ -490,6 +506,7 @@ class BaseApiClient:
|
|
|
490
506
|
method=method,
|
|
491
507
|
require_auth=require_auth,
|
|
492
508
|
raise_exception=raise_exception,
|
|
509
|
+
public_api=public_api,
|
|
493
510
|
**kwargs,
|
|
494
511
|
)
|
|
495
512
|
|
|
@@ -504,6 +521,7 @@ class BaseApiClient:
|
|
|
504
521
|
method: str = "get",
|
|
505
522
|
require_auth: bool = True,
|
|
506
523
|
raise_exception: bool = True,
|
|
524
|
+
public_api: bool = False,
|
|
507
525
|
**kwargs: Any,
|
|
508
526
|
) -> list[Any]:
|
|
509
527
|
data = await self.api_request(
|
|
@@ -511,6 +529,7 @@ class BaseApiClient:
|
|
|
511
529
|
method=method,
|
|
512
530
|
require_auth=require_auth,
|
|
513
531
|
raise_exception=raise_exception,
|
|
532
|
+
public_api=public_api,
|
|
514
533
|
**kwargs,
|
|
515
534
|
)
|
|
516
535
|
|
|
@@ -689,11 +708,11 @@ class BaseApiClient:
|
|
|
689
708
|
|
|
690
709
|
async def async_disconnect_ws(self) -> None:
|
|
691
710
|
"""Disconnect from Websocket."""
|
|
692
|
-
if self.
|
|
711
|
+
if self._private_websocket:
|
|
693
712
|
websocket = self._get_websocket()
|
|
694
713
|
websocket.stop()
|
|
695
714
|
await websocket.wait_closed()
|
|
696
|
-
self.
|
|
715
|
+
self._private_websocket = None
|
|
697
716
|
|
|
698
717
|
def _process_ws_message(self, msg: aiohttp.WSMessage) -> None:
|
|
699
718
|
raise NotImplementedError
|
|
@@ -1590,10 +1609,12 @@ class ProtectApiClient(BaseApiClient):
|
|
|
1590
1609
|
raise_exception=False,
|
|
1591
1610
|
)
|
|
1592
1611
|
|
|
1593
|
-
_LOGGER.debug(
|
|
1612
|
+
_LOGGER.debug(
|
|
1613
|
+
"Requesting camera video: %s%s %s", self.private_api_path, path, params
|
|
1614
|
+
)
|
|
1594
1615
|
r = await self.request(
|
|
1595
1616
|
"get",
|
|
1596
|
-
f"{self.
|
|
1617
|
+
f"{self.private_api_path}{path}",
|
|
1597
1618
|
auto_close=False,
|
|
1598
1619
|
timeout=0,
|
|
1599
1620
|
params=params,
|
|
@@ -2078,3 +2099,13 @@ class ProtectApiClient(BaseApiClient):
|
|
|
2078
2099
|
raise BadRequest("Failed to create API key")
|
|
2079
2100
|
|
|
2080
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)
|
|
@@ -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())
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|