uiprotect 7.14.1__py3-none-any.whl → 7.15.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
@@ -82,10 +82,7 @@ TOKEN_COOKIE_MAX_EXP_SECONDS = 60
82
82
  DEVICE_UPDATE_INTERVAL = 900
83
83
  # retry timeout for thumbnails/heatmaps
84
84
  RETRY_TIMEOUT = 10
85
- PROTECT_APT_URLS = [
86
- "https://apt.artifacts.ui.com/dists/stretch/release/binary-arm64/Packages",
87
- "https://apt.artifacts.ui.com/dists/bullseye/release/binary-arm64/Packages",
88
- ]
85
+
89
86
  TYPES_BUG_MESSAGE = """There is currently a bug in UniFi Protect that makes `start` / `end` not work if `types` is not provided. This means uiprotect has to iterate over all of the events matching the filters provided to return values.
90
87
 
91
88
  If your Protect instance has a lot of events, this request will take much longer then expected. It is recommended adding additional filters to speed the request up."""
@@ -172,6 +169,7 @@ class BaseApiClient:
172
169
  _last_token_cookie: Morsel[str] | None = None
173
170
  _last_token_cookie_decode: dict[str, Any] | None = None
174
171
  _session: aiohttp.ClientSession | None = None
172
+ _public_api_session: aiohttp.ClientSession | None = None
175
173
  _loaded_session: bool = False
176
174
  _cookiename = "TOKEN"
177
175
 
@@ -195,6 +193,7 @@ class BaseApiClient:
195
193
  api_key: str | None = None,
196
194
  verify_ssl: bool = True,
197
195
  session: aiohttp.ClientSession | None = None,
196
+ public_api_session: aiohttp.ClientSession | None = None,
198
197
  ws_timeout: int = 30,
199
198
  cache_dir: Path | None = None,
200
199
  config_dir: Path | None = None,
@@ -221,6 +220,9 @@ class BaseApiClient:
221
220
  if session is not None:
222
221
  self._session = session
223
222
 
223
+ if public_api_session is not None:
224
+ self._public_api_session = public_api_session
225
+
224
226
  self._update_url()
225
227
 
226
228
  def _update_cookiename(self, cookie: SimpleCookie) -> None:
@@ -264,6 +266,15 @@ class BaseApiClient:
264
266
 
265
267
  return self._session
266
268
 
269
+ async def get_public_api_session(self) -> aiohttp.ClientSession:
270
+ """Gets or creates current public API client session"""
271
+ if self._public_api_session is None or self._public_api_session.closed:
272
+ if self._public_api_session is not None and self._public_api_session.closed:
273
+ _LOGGER.debug("Public API session was closed, creating a new one")
274
+ self._public_api_session = aiohttp.ClientSession()
275
+
276
+ return self._public_api_session
277
+
267
278
  async def _auth_websocket(self, force: bool) -> dict[str, str] | None:
268
279
  """Authenticate for Websocket."""
269
280
  if force:
@@ -309,6 +320,12 @@ class BaseApiClient:
309
320
  self._session = None
310
321
  self._loaded_session = False
311
322
 
323
+ async def close_public_api_session(self) -> None:
324
+ """Closing and deletes public API client session"""
325
+ if self._public_api_session is not None:
326
+ await self._public_api_session.close()
327
+ self._public_api_session = None
328
+
312
329
  async def _cancel_update_task(self) -> None:
313
330
  if self._update_task:
314
331
  self._update_task.cancel()
@@ -348,7 +365,11 @@ class BaseApiClient:
348
365
  _LOGGER.debug("Request url: %s", request_url)
349
366
  if not self._verify_ssl:
350
367
  kwargs["ssl"] = False
351
- session = await self.get_session()
368
+
369
+ if public_api:
370
+ session = await self.get_public_api_session()
371
+ else:
372
+ session = await self.get_session()
352
373
 
353
374
  for attempt in range(2):
354
375
  try:
@@ -780,6 +801,7 @@ class ProtectApiClient(BaseApiClient):
780
801
  api_key: str | None = None,
781
802
  verify_ssl: bool = True,
782
803
  session: aiohttp.ClientSession | None = None,
804
+ public_api_session: aiohttp.ClientSession | None = None,
783
805
  ws_timeout: int = 30,
784
806
  cache_dir: Path | None = None,
785
807
  config_dir: Path | None = None,
@@ -800,6 +822,7 @@ class ProtectApiClient(BaseApiClient):
800
822
  api_key=api_key,
801
823
  verify_ssl=verify_ssl,
802
824
  session=session,
825
+ public_api_session=public_api_session,
803
826
  ws_timeout=ws_timeout,
804
827
  ws_receive_timeout=ws_receive_timeout,
805
828
  cache_dir=cache_dir,
@@ -1915,17 +1938,6 @@ class ProtectApiClient(BaseApiClient):
1915
1938
 
1916
1939
  return versions
1917
1940
 
1918
- async def get_release_versions(self) -> set[Version]:
1919
- """Get all release versions for UniFi Protect"""
1920
- versions: set[Version] = set()
1921
- for url in PROTECT_APT_URLS:
1922
- try:
1923
- versions |= await self._get_versions_from_api(url)
1924
- except NvrError:
1925
- _LOGGER.warning("Failed to retrieve release versions from online.")
1926
-
1927
- return versions
1928
-
1929
1941
  async def relative_move_ptz_camera(
1930
1942
  self,
1931
1943
  device_id: str,
uiprotect/cli/__init__.py CHANGED
@@ -13,9 +13,9 @@ from rich.progress import track
13
13
 
14
14
  from uiprotect.api import MetaInfo, ProtectApiClient
15
15
 
16
- from ..data import Version, WSPacket
16
+ from ..data import WSPacket
17
17
  from ..test_util import SampleDataGenerator
18
- from ..utils import RELEASE_CACHE, get_local_timezone, run_async
18
+ from ..utils import get_local_timezone, run_async
19
19
  from ..utils import profile_ws as profile_ws_job
20
20
  from .aiports import app as aiports_app
21
21
  from .base import CliContext, OutputFormatEnum
@@ -173,6 +173,7 @@ def main(
173
173
  async def update() -> None:
174
174
  protect._bootstrap = await protect.get_bootstrap()
175
175
  await protect.close_session()
176
+ await protect.close_public_api_session()
176
177
 
177
178
  run_async(update())
178
179
  ctx.obj = CliContext(protect=protect, output_format=output_format)
@@ -286,6 +287,7 @@ def profile_ws(
286
287
  unsub()
287
288
  await protect.async_disconnect_ws()
288
289
  await protect.close_session()
290
+ await protect.close_public_api_session()
289
291
 
290
292
  _setup_logger()
291
293
 
@@ -314,25 +316,6 @@ def decode_ws_msg(
314
316
  typer.echo(orjson.dumps(response).decode("utf-8"))
315
317
 
316
318
 
317
- @app.command()
318
- def release_versions(ctx: typer.Context) -> None:
319
- """Updates the release version cache on disk."""
320
- protect = cast(ProtectApiClient, ctx.obj.protect)
321
-
322
- async def callback() -> set[Version]:
323
- versions = await protect.get_release_versions()
324
- await protect.close_session()
325
- return versions
326
-
327
- _setup_logger()
328
-
329
- versions = run_async(callback())
330
- output = orjson.dumps(sorted([str(v) for v in versions]))
331
-
332
- Path(RELEASE_CACHE).write_bytes(output)
333
- typer.echo(output.decode("utf-8"))
334
-
335
-
336
319
  @app.command()
337
320
  def create_api_key(
338
321
  ctx: typer.Context,
@@ -344,6 +327,7 @@ def create_api_key(
344
327
  async def callback() -> str:
345
328
  api_key = await protect.create_api_key(name)
346
329
  await protect.close_session()
330
+ await protect.close_public_api_session()
347
331
  return api_key
348
332
 
349
333
  _setup_logger()
@@ -359,6 +343,7 @@ def get_meta_info(ctx: typer.Context) -> None:
359
343
  async def callback() -> MetaInfo:
360
344
  meta = await protect.get_meta_info()
361
345
  await protect.close_session()
346
+ await protect.close_public_api_session()
362
347
  return meta
363
348
 
364
349
  _setup_logger()
uiprotect/cli/backup.py CHANGED
@@ -1064,6 +1064,7 @@ async def _events(
1064
1064
  finally:
1065
1065
  _LOGGER.debug("Cleaning up Protect connection/database...")
1066
1066
  await ctx.protect.close_session()
1067
+ await ctx.protect.close_public_api_session()
1067
1068
  await ctx.db_engine.dispose()
1068
1069
 
1069
1070
 
uiprotect/cli/base.py CHANGED
@@ -36,6 +36,7 @@ def run(ctx: typer.Context, func: Coroutine[Any, Any, T]) -> T:
36
36
  async def callback() -> T:
37
37
  return_value = await func
38
38
  await ctx.obj.protect.close_session()
39
+ await ctx.obj.protect.close_public_api_session()
39
40
  return return_value
40
41
 
41
42
  try:
@@ -680,5 +680,5 @@ class Bootstrap(ProtectBaseObject):
680
680
  _LOGGER.debug("Successfully refresh model: %s %s", model_type, device_id)
681
681
 
682
682
  async def get_is_prerelease(self) -> bool:
683
- """Get if current version of Protect is a prerelease version."""
684
- return await self.nvr.get_is_prerelease()
683
+ """[DEPRECATED] Always returns False. Will be removed after HA 2025.8.0."""
684
+ return False
uiprotect/data/nvr.py CHANGED
@@ -15,12 +15,11 @@ from uuid import UUID
15
15
 
16
16
  import aiofiles
17
17
  import orjson
18
- from aiofiles import os as aos
19
18
  from convertertools import pop_dict_set_if_none, pop_dict_tuple
20
19
  from pydantic.fields import PrivateAttr
21
20
 
22
21
  from ..exceptions import BadRequest, NotAuthorized
23
- from ..utils import RELEASE_CACHE, convert_to_datetime
22
+ from ..utils import convert_to_datetime
24
23
  from .base import (
25
24
  ProtectBaseObject,
26
25
  ProtectDeviceModel,
@@ -1217,29 +1216,8 @@ class NVR(ProtectDeviceModel):
1217
1216
  return versions
1218
1217
 
1219
1218
  async def get_is_prerelease(self) -> bool:
1220
- """Get if current version of Protect is a prerelease version."""
1221
- # only EA versions have `-beta` in versions
1222
- if self.version.is_prerelease:
1223
- return True
1224
-
1225
- # 2.6.14 is an EA version that looks like a release version
1226
- cache_file_path = self._api.cache_dir / "release_cache.json"
1227
- versions = await self._read_cache_file(
1228
- cache_file_path,
1229
- ) or await self._read_cache_file(RELEASE_CACHE)
1230
- if versions is None or self.version not in versions:
1231
- versions = await self._api.get_release_versions()
1232
- try:
1233
- _LOGGER.debug("Fetching releases from APT repos...")
1234
- tmp = self._api.cache_dir / "release_cache.tmp.json"
1235
- await aos.makedirs(self._api.cache_dir, exist_ok=True)
1236
- async with aiofiles.open(tmp, "wb") as cache_file:
1237
- await cache_file.write(orjson.dumps([str(v) for v in versions]))
1238
- await aos.rename(tmp, cache_file_path)
1239
- except Exception:
1240
- _LOGGER.warning("Failed write cache file.")
1241
-
1242
- return self.version not in versions
1219
+ """[DEPRECATED] Always returns False. Will be removed after HA 2025.8.0."""
1220
+ return False
1243
1221
 
1244
1222
  async def set_smart_detections(self, value: bool) -> None:
1245
1223
  """Set if smart detections are enabled."""
@@ -142,6 +142,7 @@ class SampleDataGenerator:
142
142
 
143
143
  if close_session:
144
144
  await self.client.close_session()
145
+ await self.client.close_public_api_session()
145
146
 
146
147
  await self.write_json_file("sample_constants", self.constants, anonymize=False)
147
148
 
uiprotect/utils.py CHANGED
@@ -74,8 +74,6 @@ SNAKE_CASE_MATCH_3 = re.compile("([a-z0-9])([A-Z])")
74
74
 
75
75
  _LOGGER = logging.getLogger(__name__)
76
76
 
77
- RELEASE_CACHE = Path(__file__).parent / "release_cache.json"
78
-
79
77
  _CREATE_TYPES = {IPv6Address, IPv4Address, UUID, Color, Decimal, Path, Version}
80
78
  _BAD_UUID = "00000000-0000-00 0- 000-000000000000"
81
79
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: uiprotect
3
- Version: 7.14.1
3
+ Version: 7.15.0
4
4
  Summary: Python API for Unifi Protect (Unofficial)
5
5
  License: MIT
6
6
  Author: UI Protect Maintainers
@@ -241,6 +241,8 @@ unsub()
241
241
 
242
242
  ## TODO / Planned / Not Implemented
243
243
 
244
+ Switching from Protect Private API to the New Public API
245
+
244
246
  Generally any feature missing from the library is planned to be done eventually / nice to have with the following exceptions
245
247
 
246
248
  ### UniFi OS Features
@@ -1,11 +1,11 @@
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=R8-R3AmIxKDETndph2xelWOA0IDyM0VHl8ZLdftfAoI,73044
5
- uiprotect/cli/__init__.py,sha256=4nvTO0tW5aVforlh--_NwmGvxTvpOfmAiso5-acY850,10075
4
+ uiprotect/api.py,sha256=9n0iMjds9sSBShURytvlJezNMD-LL3T8E0OzPX243a8,73640
5
+ uiprotect/cli/__init__.py,sha256=rVfmLd4Al4-FQZxq7QrYlfr46fBGZv6Aw5huFtmCn58,9707
6
6
  uiprotect/cli/aiports.py,sha256=wpEr2w_hY18CGpFiQM2Yc0FiVwG_1l2CzZhZLGNigvI,1576
7
- uiprotect/cli/backup.py,sha256=ZiS7RZnJGKI8TJKLW2cOUzkRM8nyTvE5Ov_jZZGtvSM,36708
8
- uiprotect/cli/base.py,sha256=5-z-IS8g9iQqhR_YbjxaJAFiMMAY_7cCtNAtvLdRCoM,7524
7
+ uiprotect/cli/backup.py,sha256=9Coo5K4YTvQIrvGVZ-NYUajGnoMIyPhK_juxQxNkdO0,36761
8
+ uiprotect/cli/base.py,sha256=SFwgIeliu1_l07IGMerSNPPKvV2g3-Amoc9YiCGmaVA,7581
9
9
  uiprotect/cli/cameras.py,sha256=YvvMccQEYG3Wih0Ix8tan1R1vfaJ6cogg6YKWLzMUV8,16973
10
10
  uiprotect/cli/chimes.py,sha256=XANn21bQVkestkKOm9HjxSM8ZGrRrqvUXLouaQ3LTqs,5326
11
11
  uiprotect/cli/doorlocks.py,sha256=Go_Tn68bAcmrRAnUIi4kBiR7ciKQsu_R150ubPTjUAs,3523
@@ -17,10 +17,10 @@ uiprotect/cli/sensors.py,sha256=fQtcDJCVxs4VbAqcavgBy2ABiVxAW3GXtna6_XFBp2k,8153
17
17
  uiprotect/cli/viewers.py,sha256=2cyrp104ffIvgT0wYGIO0G35QMkEbFe7fSVqLwDXQYQ,2171
18
18
  uiprotect/data/__init__.py,sha256=audwJBjxRiYdNPeYlP6iofFIOq3gyQzh6VpDsOCM2dQ,2964
19
19
  uiprotect/data/base.py,sha256=CXIxaJCJNpavadn6uF7vlhI4wxkfD8MNBGgR-smRA7k,35401
20
- uiprotect/data/bootstrap.py,sha256=ddNaKrTprN7Zq0ZE3O_F5whepUh6z9GqyrUWxLyZ0HE,23570
20
+ uiprotect/data/bootstrap.py,sha256=r0T1qD6ZFC86pWK-70kx5MxugrN31XqVxUQ6BfB78O4,23552
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=lRBCKwAw6GhV7NFluouZkZEWln_faAABfYJXMrTGDZI,47772
23
+ uiprotect/data/nvr.py,sha256=HnknmMmUxe1aPA0PHJZSiEwpvo1phtHBp3wXsU6v_iI,46681
24
24
  uiprotect/data/types.py,sha256=PIf08tkkLzLPu8IHlEfv5Jqy7sdyhcbLkBgoV21Kyv0,19313
25
25
  uiprotect/data/user.py,sha256=Del5LUmt5uCfAQMI9-kl_GaKm085oTLjxmcCrlEKXxc,10526
26
26
  uiprotect/data/websocket.py,sha256=m4EV1Qfh08eKOihy70ycViYgEQpeNSGZQJWdtGIYJDA,6791
@@ -28,12 +28,12 @@ uiprotect/exceptions.py,sha256=kgn0cRM6lTtgLza09SDa3ZiX6ue1QqHCOogQ4qu6KTQ,965
28
28
  uiprotect/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
29
  uiprotect/release_cache.json,sha256=NamnSFy78hOWY0DPO87J9ELFCAN6NnVquv8gQO75ZG4,386
30
30
  uiprotect/stream.py,sha256=ls65vMOXF4IlJ5axewFITfhcaTh_ihaFeCkCTfhy0Nk,5168
31
- uiprotect/test_util/__init__.py,sha256=HlQBgIgdtrvT-gQ5OWP92LbgVr_YzsD5NFImLRonUZk,19320
31
+ uiprotect/test_util/__init__.py,sha256=TLTQrqyuLcIxp7bZTk3nIZ0U9znoMTLu7QyxrJKaLo0,19377
32
32
  uiprotect/test_util/anonymize.py,sha256=f-8ijU-_y9r-uAbhIPn0f0I6hzJpAkvJzc8UpWihObI,8478
33
- uiprotect/utils.py,sha256=A2yoNa0qkDKEZOlhrCgaCdu5XI9EVhCPDpwH1Keg8H4,20649
33
+ uiprotect/utils.py,sha256=2fLwXN0oz9dcVTFexzLp8jj97lcYrxLZkeEUGf5ehtU,20587
34
34
  uiprotect/websocket.py,sha256=tEyenqblNXHcjWYuf4oRP1E7buNwx6zoECMwpBr-jig,8191
35
- uiprotect-7.14.1.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
36
- uiprotect-7.14.1.dist-info/METADATA,sha256=FOBzUt9Ec6J9YxRP0HujOO5eN_FaQfEGVVv0wnlmT7A,11109
37
- uiprotect-7.14.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
38
- uiprotect-7.14.1.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
39
- uiprotect-7.14.1.dist-info/RECORD,,
35
+ uiprotect-7.15.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
36
+ uiprotect-7.15.0.dist-info/METADATA,sha256=aR6aiVRJwN58_dEeklFE6Uq9wEdFxHOmt_sk5CIZYa4,11167
37
+ uiprotect-7.15.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
38
+ uiprotect-7.15.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
39
+ uiprotect-7.15.0.dist-info/RECORD,,