uiprotect 7.14.1__tar.gz → 7.15.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.14.1 → uiprotect-7.15.0}/PKG-INFO +3 -1
- {uiprotect-7.14.1 → uiprotect-7.15.0}/README.md +2 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/pyproject.toml +1 -1
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/api.py +28 -16
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/cli/__init__.py +6 -21
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/cli/backup.py +1 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/cli/base.py +1 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/data/bootstrap.py +2 -2
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/data/nvr.py +3 -25
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/test_util/__init__.py +1 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/utils.py +0 -2
- {uiprotect-7.14.1 → uiprotect-7.15.0}/LICENSE +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/__init__.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/__main__.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/_compat.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/cli/aiports.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/cli/cameras.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/cli/chimes.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/cli/doorlocks.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/cli/events.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/cli/lights.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/cli/liveviews.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/cli/nvr.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/cli/sensors.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/cli/viewers.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/data/__init__.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/data/base.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/data/convert.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/data/devices.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/data/types.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/data/user.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/data/websocket.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/exceptions.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/py.typed +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/release_cache.json +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/stream.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/test_util/anonymize.py +0 -0
- {uiprotect-7.14.1 → uiprotect-7.15.0}/src/uiprotect/websocket.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: uiprotect
|
|
3
|
-
Version: 7.
|
|
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
|
|
@@ -198,6 +198,8 @@ unsub()
|
|
|
198
198
|
|
|
199
199
|
## TODO / Planned / Not Implemented
|
|
200
200
|
|
|
201
|
+
Switching from Protect Private API to the New Public API
|
|
202
|
+
|
|
201
203
|
Generally any feature missing from the library is planned to be done eventually / nice to have with the following exceptions
|
|
202
204
|
|
|
203
205
|
### UniFi OS Features
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -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
|
|
16
|
+
from ..data import WSPacket
|
|
17
17
|
from ..test_util import SampleDataGenerator
|
|
18
|
-
from ..utils import
|
|
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()
|
|
@@ -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
|
-
"""
|
|
684
|
-
return
|
|
683
|
+
"""[DEPRECATED] Always returns False. Will be removed after HA 2025.8.0."""
|
|
684
|
+
return False
|
|
@@ -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
|
|
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
|
-
"""
|
|
1221
|
-
|
|
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."""
|
|
@@ -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
|
|
|
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
|