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 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
- _websocket: Websocket | None = None
179
+ _private_websocket: Websocket | None = None
178
180
 
179
- api_path: str = "/proxy/protect/api/"
180
- ws_path: str = "/proxy/protect/ws/updates"
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.ws_path}")
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.ws_path}")
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._websocket is None:
277
- self._websocket = Websocket(
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._websocket
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 = api_path if api_path is not None else self.api_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._websocket:
711
+ if self._private_websocket:
690
712
  websocket = self._get_websocket()
691
713
  websocket.stop()
692
714
  await websocket.wait_closed()
693
- self._websocket = None
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("Requesting camera video: %s%s %s", self.api_path, path, params)
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.api_path}{path}",
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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: uiprotect
3
- Version: 7.11.0
3
+ Version: 7.13.0
4
4
  Summary: Python API for Unifi Protect (Unofficial)
5
5
  License: MIT
6
6
  Author: UI Protect Maintainers
@@ -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=hfTd_BYyF_B6Ehbe41U99_9CEEAd-eUwLJ0pOpvojV4,71642
5
- uiprotect/cli/__init__.py,sha256=B70Zdwq4cdw3wyu39uY0lMkjk5EoWLRIfVncuTOZ4AE,9423
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=CPmqp3wQouTAGQxUDFghCHsYVWEFSrga1Hf2_HmHDgA,47707
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.11.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
36
- uiprotect-7.11.0.dist-info/METADATA,sha256=DPwqWEaP1QzQerdlXwHJt_W2Ka4BnROs9HgteI6fJsA,11109
37
- uiprotect-7.11.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
38
- uiprotect-7.11.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
39
- uiprotect-7.11.0.dist-info/RECORD,,
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,,