uiprotect 3.8.0__py3-none-any.whl → 7.32.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.
- uiprotect/__init__.py +1 -3
- uiprotect/_compat.py +13 -0
- uiprotect/api.py +975 -92
- uiprotect/cli/__init__.py +111 -24
- uiprotect/cli/aiports.py +58 -0
- uiprotect/cli/backup.py +5 -4
- uiprotect/cli/base.py +5 -5
- uiprotect/cli/cameras.py +152 -13
- uiprotect/cli/chimes.py +5 -6
- uiprotect/cli/doorlocks.py +2 -3
- uiprotect/cli/events.py +7 -8
- uiprotect/cli/lights.py +11 -3
- uiprotect/cli/liveviews.py +1 -2
- uiprotect/cli/sensors.py +2 -3
- uiprotect/cli/viewers.py +2 -3
- uiprotect/data/__init__.py +2 -0
- uiprotect/data/base.py +96 -97
- uiprotect/data/bootstrap.py +116 -45
- uiprotect/data/convert.py +17 -2
- uiprotect/data/devices.py +409 -164
- uiprotect/data/nvr.py +236 -118
- uiprotect/data/types.py +94 -59
- uiprotect/data/user.py +132 -13
- uiprotect/data/websocket.py +2 -1
- uiprotect/stream.py +13 -6
- uiprotect/test_util/__init__.py +47 -7
- uiprotect/test_util/anonymize.py +4 -5
- uiprotect/utils.py +99 -45
- uiprotect/websocket.py +11 -6
- {uiprotect-3.8.0.dist-info → uiprotect-7.32.0.dist-info}/METADATA +77 -21
- uiprotect-7.32.0.dist-info/RECORD +39 -0
- {uiprotect-3.8.0.dist-info → uiprotect-7.32.0.dist-info}/WHEEL +1 -1
- uiprotect-3.8.0.dist-info/RECORD +0 -37
- {uiprotect-3.8.0.dist-info → uiprotect-7.32.0.dist-info}/entry_points.txt +0 -0
- {uiprotect-3.8.0.dist-info → uiprotect-7.32.0.dist-info/licenses}/LICENSE +0 -0
uiprotect/utils.py
CHANGED
|
@@ -29,8 +29,7 @@ from uuid import UUID
|
|
|
29
29
|
|
|
30
30
|
import jwt
|
|
31
31
|
from aiohttp import ClientResponse
|
|
32
|
-
from pydantic.
|
|
33
|
-
from pydantic.v1.utils import to_camel
|
|
32
|
+
from pydantic.fields import FieldInfo
|
|
34
33
|
|
|
35
34
|
from .data.types import (
|
|
36
35
|
Color,
|
|
@@ -38,6 +37,7 @@ from .data.types import (
|
|
|
38
37
|
SmartDetectObjectType,
|
|
39
38
|
Version,
|
|
40
39
|
VideoMode,
|
|
40
|
+
get_field_type,
|
|
41
41
|
)
|
|
42
42
|
from .exceptions import NvrError
|
|
43
43
|
|
|
@@ -71,8 +71,6 @@ SNAKE_CASE_MATCH_3 = re.compile("([a-z0-9])([A-Z])")
|
|
|
71
71
|
|
|
72
72
|
_LOGGER = logging.getLogger(__name__)
|
|
73
73
|
|
|
74
|
-
RELEASE_CACHE = Path(__file__).parent / "release_cache.json"
|
|
75
|
-
|
|
76
74
|
_CREATE_TYPES = {IPv6Address, IPv4Address, UUID, Color, Decimal, Path, Version}
|
|
77
75
|
_BAD_UUID = "00000000-0000-00 0- 000-000000000000"
|
|
78
76
|
|
|
@@ -88,6 +86,11 @@ IP_TYPES = {
|
|
|
88
86
|
}
|
|
89
87
|
|
|
90
88
|
|
|
89
|
+
@lru_cache
|
|
90
|
+
def to_camel(string: str) -> str:
|
|
91
|
+
return "".join(word.capitalize() for word in string.split("_"))
|
|
92
|
+
|
|
93
|
+
|
|
91
94
|
def set_debug() -> None:
|
|
92
95
|
"""Sets ENV variable for UFP_DEBUG to on (True)"""
|
|
93
96
|
os.environ[DEBUG_ENV] = str(True)
|
|
@@ -146,7 +149,7 @@ def to_ms(duration: timedelta | None) -> int | None:
|
|
|
146
149
|
if duration is None:
|
|
147
150
|
return None
|
|
148
151
|
|
|
149
|
-
return
|
|
152
|
+
return round(duration.total_seconds() * 1000)
|
|
150
153
|
|
|
151
154
|
|
|
152
155
|
def utc_now() -> datetime:
|
|
@@ -204,49 +207,69 @@ def to_camel_case(name: str) -> str:
|
|
|
204
207
|
_EMPTY_UUID = UUID("0" * 32)
|
|
205
208
|
|
|
206
209
|
|
|
207
|
-
def convert_unifi_data(value: Any, field:
|
|
210
|
+
def convert_unifi_data(value: Any, field: FieldInfo) -> Any: # noqa: PLR0911, PLR0912
|
|
208
211
|
"""Converts value from UFP data into pydantic field class"""
|
|
209
|
-
type_ = field.
|
|
212
|
+
origin, type_ = get_field_type(field.annotation) # type: ignore[arg-type]
|
|
210
213
|
|
|
211
214
|
if type_ is Any:
|
|
212
215
|
return value
|
|
213
216
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
217
|
+
if origin is not None:
|
|
218
|
+
if origin is list and isinstance(value, list):
|
|
219
|
+
return [convert_unifi_data(v, field) for v in value]
|
|
220
|
+
if origin is set and isinstance(value, list):
|
|
221
|
+
return {convert_unifi_data(v, field) for v in value}
|
|
222
|
+
if origin is dict and isinstance(value, dict):
|
|
223
|
+
return {k: convert_unifi_data(v, field) for k, v in value.items()}
|
|
221
224
|
|
|
222
225
|
if value is not None:
|
|
223
226
|
if type_ in IP_TYPES:
|
|
224
|
-
|
|
225
|
-
return ip_address(value)
|
|
226
|
-
except ValueError:
|
|
227
|
-
return value
|
|
227
|
+
return _cached_ip_address(value)
|
|
228
228
|
if type_ is datetime:
|
|
229
229
|
return from_js_time(value)
|
|
230
|
-
if type_ in _CREATE_TYPES
|
|
230
|
+
if type_ in _CREATE_TYPES:
|
|
231
231
|
# cannot do this check too soon because some types cannot be used in isinstance
|
|
232
232
|
if isinstance(value, type_):
|
|
233
233
|
return value
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
234
|
+
if type_ is UUID:
|
|
235
|
+
if not value:
|
|
236
|
+
return None
|
|
237
|
+
# handle edge case for improperly formatted UUIDs
|
|
238
|
+
# 00000000-0000-00 0- 000-000000000000
|
|
239
|
+
if value == _BAD_UUID:
|
|
240
|
+
return _EMPTY_UUID
|
|
241
|
+
if (type_ is IPv4Address) and value == "":
|
|
242
|
+
return None
|
|
243
|
+
return type_(value)
|
|
244
|
+
if _is_enum_type(type_):
|
|
245
|
+
if _is_from_string_enum(type_):
|
|
246
|
+
return type_.from_string(value)
|
|
238
247
|
return type_(value)
|
|
239
248
|
|
|
240
249
|
return value
|
|
241
250
|
|
|
242
251
|
|
|
252
|
+
@lru_cache
|
|
253
|
+
def _cached_ip_address(value: str) -> IPv4Address | IPv6Address | str:
|
|
254
|
+
try:
|
|
255
|
+
return ip_address(value)
|
|
256
|
+
except ValueError:
|
|
257
|
+
return value
|
|
258
|
+
|
|
259
|
+
|
|
243
260
|
@lru_cache
|
|
244
261
|
def _is_enum_type(type_: Any) -> bool:
|
|
245
262
|
"""Checks if type is an Enum."""
|
|
246
263
|
return isclass(type_) and issubclass(type_, Enum)
|
|
247
264
|
|
|
248
265
|
|
|
249
|
-
|
|
266
|
+
@lru_cache
|
|
267
|
+
def _is_from_string_enum(type_: Any) -> bool:
|
|
268
|
+
"""Checks if Enum has from_string method."""
|
|
269
|
+
return hasattr(type_, "from_string")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def serialize_unifi_obj(value: Any, levels: int = -1) -> Any: # noqa: PLR0911
|
|
250
273
|
"""Serializes UFP data"""
|
|
251
274
|
if unifi_dict := getattr(value, "unifi_dict", None):
|
|
252
275
|
value = unifi_dict()
|
|
@@ -282,9 +305,7 @@ def serialize_dict(data: dict[str, Any], levels: int = -1) -> dict[str, Any]:
|
|
|
282
305
|
|
|
283
306
|
def serialize_coord(coord: CoordType) -> int | float:
|
|
284
307
|
"""Serializes UFP zone coordinate"""
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if not isinstance(coord, Percent):
|
|
308
|
+
if not isinstance(coord, float):
|
|
288
309
|
return coord
|
|
289
310
|
|
|
290
311
|
if math.isclose(coord, 0) or math.isclose(coord, 1):
|
|
@@ -338,13 +359,41 @@ def convert_video_modes(items: Iterable[str]) -> list[VideoMode]:
|
|
|
338
359
|
return types
|
|
339
360
|
|
|
340
361
|
|
|
341
|
-
def ip_from_host(host: str) -> IPv4Address | IPv6Address:
|
|
362
|
+
async def ip_from_host(host: str) -> IPv4Address | IPv6Address:
|
|
363
|
+
"""
|
|
364
|
+
Resolve hostname to IP address (IPv4 or IPv6).
|
|
365
|
+
|
|
366
|
+
Raises:
|
|
367
|
+
ValueError: If host cannot be resolved to IP address
|
|
368
|
+
|
|
369
|
+
"""
|
|
342
370
|
try:
|
|
343
371
|
return ip_address(host)
|
|
344
372
|
except ValueError:
|
|
345
373
|
pass
|
|
346
374
|
|
|
347
|
-
|
|
375
|
+
try:
|
|
376
|
+
loop = asyncio.get_running_loop()
|
|
377
|
+
addr_info = await loop.getaddrinfo(host, None)
|
|
378
|
+
ip_str = addr_info[0][4][0]
|
|
379
|
+
except (socket.gaierror, OSError) as err:
|
|
380
|
+
raise ValueError(f"Cannot resolve hostname '{host}' to IP address") from err
|
|
381
|
+
|
|
382
|
+
return ip_address(ip_str)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def format_host_for_url(host: IPv4Address | IPv6Address | str) -> str:
|
|
386
|
+
"""Format host for URLs. IPv6 addresses are wrapped in brackets."""
|
|
387
|
+
if isinstance(host, str):
|
|
388
|
+
try:
|
|
389
|
+
parsed_host = ip_address(host)
|
|
390
|
+
except ValueError:
|
|
391
|
+
return host
|
|
392
|
+
host = parsed_host
|
|
393
|
+
|
|
394
|
+
if isinstance(host, IPv6Address):
|
|
395
|
+
return f"[{host}]"
|
|
396
|
+
return str(host)
|
|
348
397
|
|
|
349
398
|
|
|
350
399
|
def dict_diff(orig: dict[str, Any] | None, new: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -400,7 +449,7 @@ def print_ws_stat_summary(
|
|
|
400
449
|
) -> None:
|
|
401
450
|
# typer<0.4.1 is incompatible with click>=8.1.0
|
|
402
451
|
# allows only the CLI interface to break if both are installed
|
|
403
|
-
import typer
|
|
452
|
+
import typer # noqa: PLC0415
|
|
404
453
|
|
|
405
454
|
if output is None:
|
|
406
455
|
output = typer.echo if typer is not None else print
|
|
@@ -495,7 +544,7 @@ def format_duration(duration: timedelta) -> str:
|
|
|
495
544
|
|
|
496
545
|
|
|
497
546
|
def _set_timezone(tz: tzinfo | str) -> tzinfo:
|
|
498
|
-
global TIMEZONE_GLOBAL
|
|
547
|
+
global TIMEZONE_GLOBAL # noqa: PLW0603
|
|
499
548
|
|
|
500
549
|
if isinstance(tz, str):
|
|
501
550
|
tz = zoneinfo.ZoneInfo(tz)
|
|
@@ -511,7 +560,9 @@ def get_local_timezone() -> tzinfo:
|
|
|
511
560
|
return TIMEZONE_GLOBAL
|
|
512
561
|
|
|
513
562
|
try:
|
|
514
|
-
from homeassistant.util import
|
|
563
|
+
from homeassistant.util import ( # noqa: PLC0415
|
|
564
|
+
dt as dt_util, # type: ignore[import-not-found]
|
|
565
|
+
)
|
|
515
566
|
|
|
516
567
|
return _set_timezone(dt_util.DEFAULT_TIME_ZONE)
|
|
517
568
|
except ImportError:
|
|
@@ -548,9 +599,9 @@ def local_datetime(dt: datetime | None = None) -> datetime:
|
|
|
548
599
|
|
|
549
600
|
|
|
550
601
|
def log_event(event: Event) -> None:
|
|
551
|
-
from uiprotect.data import EventType
|
|
602
|
+
from uiprotect.data import EventType # noqa: PLC0415
|
|
552
603
|
|
|
553
|
-
_LOGGER.debug("event WS msg: %s", event.
|
|
604
|
+
_LOGGER.debug("event WS msg: %s", event.model_dump())
|
|
554
605
|
if "smart" not in event.type.value:
|
|
555
606
|
return
|
|
556
607
|
|
|
@@ -634,7 +685,7 @@ def get_nested_attr(attrs: tuple[str, ...], obj: Any) -> Any:
|
|
|
634
685
|
for key in attrs:
|
|
635
686
|
if (value := getattr(value, key, _SENTINEL)) is _SENTINEL:
|
|
636
687
|
return None
|
|
637
|
-
return value
|
|
688
|
+
return value
|
|
638
689
|
|
|
639
690
|
|
|
640
691
|
def get_nested_attr_as_bool(attrs: tuple[str, ...], obj: Any) -> bool:
|
|
@@ -643,25 +694,18 @@ def get_nested_attr_as_bool(attrs: tuple[str, ...], obj: Any) -> bool:
|
|
|
643
694
|
for key in attrs:
|
|
644
695
|
if (value := getattr(value, key, _SENTINEL)) is _SENTINEL:
|
|
645
696
|
return False
|
|
646
|
-
return bool(value
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
def get_top_level_attr(attr: str, obj: Any) -> Any:
|
|
650
|
-
"""Fetch a top level attribute."""
|
|
651
|
-
value = getattr(obj, attr)
|
|
652
|
-
return value.value if isinstance(value, Enum) else value
|
|
697
|
+
return bool(value)
|
|
653
698
|
|
|
654
699
|
|
|
655
700
|
def get_top_level_attr_as_bool(attr: str, obj: Any) -> Any:
|
|
656
701
|
"""Fetch a top level attribute as a bool."""
|
|
657
|
-
|
|
658
|
-
return bool(value.value if isinstance(value, Enum) else value)
|
|
702
|
+
return bool(getattr(obj, attr))
|
|
659
703
|
|
|
660
704
|
|
|
661
705
|
def make_value_getter(ufp_value: str) -> Callable[[T], Any]:
|
|
662
706
|
"""Return a function to get a value from a Protect device."""
|
|
663
707
|
if "." not in ufp_value:
|
|
664
|
-
return
|
|
708
|
+
return attrgetter(ufp_value)
|
|
665
709
|
return partial(get_nested_attr, tuple(ufp_value.split(".")))
|
|
666
710
|
|
|
667
711
|
|
|
@@ -677,3 +721,13 @@ def make_required_getter(ufp_required_field: str) -> Callable[[T], bool]:
|
|
|
677
721
|
if "." not in ufp_required_field:
|
|
678
722
|
return partial(get_top_level_attr_as_bool, ufp_required_field)
|
|
679
723
|
return partial(get_nested_attr_as_bool, tuple(ufp_required_field.split(".")))
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
@lru_cache
|
|
727
|
+
def timedelta_total_seconds(td: timedelta) -> float:
|
|
728
|
+
return td.total_seconds()
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def pybool_to_json_bool(value: bool) -> str:
|
|
732
|
+
"""Convert a Python bool to a JSON boolean string ('true'/'false')."""
|
|
733
|
+
return "true" if value else "false"
|
uiprotect/websocket.py
CHANGED
|
@@ -8,8 +8,9 @@ import logging
|
|
|
8
8
|
from collections.abc import Awaitable, Callable, Coroutine
|
|
9
9
|
from enum import Enum
|
|
10
10
|
from http import HTTPStatus
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any
|
|
12
12
|
|
|
13
|
+
import aiohttp
|
|
13
14
|
from aiohttp import (
|
|
14
15
|
ClientError,
|
|
15
16
|
ClientSession,
|
|
@@ -23,7 +24,7 @@ from yarl import URL
|
|
|
23
24
|
from .exceptions import NotAuthorized, NvrError
|
|
24
25
|
|
|
25
26
|
_LOGGER = logging.getLogger(__name__)
|
|
26
|
-
AuthCallbackType = Callable[..., Coroutine[Any, Any,
|
|
27
|
+
AuthCallbackType = Callable[..., Coroutine[Any, Any, dict[str, str] | None]]
|
|
27
28
|
GetSessionCallbackType = Callable[[], Awaitable[ClientSession]]
|
|
28
29
|
UpdateBootstrapCallbackType = Callable[[], None]
|
|
29
30
|
_CLOSE_MESSAGE_TYPES = {WSMsgType.CLOSE, WSMsgType.CLOSING, WSMsgType.CLOSED}
|
|
@@ -55,10 +56,12 @@ class Websocket:
|
|
|
55
56
|
timeout: float = 30.0,
|
|
56
57
|
backoff: int = 10,
|
|
57
58
|
verify: bool = True,
|
|
59
|
+
receive_timeout: float | None = None,
|
|
58
60
|
) -> None:
|
|
59
61
|
"""Init Websocket."""
|
|
60
62
|
self.get_url = get_url
|
|
61
63
|
self.timeout = timeout
|
|
64
|
+
self.receive_timeout = receive_timeout
|
|
62
65
|
self.backoff = backoff
|
|
63
66
|
self.verify = verify
|
|
64
67
|
self._get_session = get_session
|
|
@@ -117,22 +120,24 @@ class Websocket:
|
|
|
117
120
|
async def _websocket_inner_loop(self, url: URL) -> None:
|
|
118
121
|
_LOGGER.debug("Connecting WS to %s", url)
|
|
119
122
|
await self._attempt_auth(False)
|
|
120
|
-
ssl = None if self.verify else False
|
|
121
123
|
msg: WSMessage | None = None
|
|
122
124
|
self._seen_non_close_message = False
|
|
123
125
|
session = await self._get_session()
|
|
124
126
|
# catch any and all errors for Websocket so we can clean up correctly
|
|
125
127
|
try:
|
|
126
128
|
self._ws_connection = await session.ws_connect(
|
|
127
|
-
url,
|
|
129
|
+
url,
|
|
130
|
+
ssl=self.verify,
|
|
131
|
+
headers=self._headers,
|
|
132
|
+
timeout=aiohttp.ClientWSTimeout(ws_close=self.timeout),
|
|
128
133
|
)
|
|
129
134
|
while True:
|
|
130
|
-
msg = await self._ws_connection.receive(self.
|
|
135
|
+
msg = await self._ws_connection.receive(self.receive_timeout)
|
|
131
136
|
msg_type = msg.type
|
|
132
137
|
if msg_type is WSMsgType.ERROR:
|
|
133
138
|
_LOGGER.exception("Error from Websocket: %s", msg.data)
|
|
134
139
|
break
|
|
135
|
-
|
|
140
|
+
if msg_type in _CLOSE_MESSAGE_TYPES:
|
|
136
141
|
_LOGGER.debug("Websocket closed: %s", msg)
|
|
137
142
|
break
|
|
138
143
|
|
|
@@ -1,32 +1,37 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: uiprotect
|
|
3
|
-
Version:
|
|
3
|
+
Version: 7.32.0
|
|
4
4
|
Summary: Python API for Unifi Protect (Unofficial)
|
|
5
|
-
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Author: UI Protect Maintainers
|
|
7
8
|
Author-email: ui@koston.org
|
|
8
9
|
Requires-Python: >=3.10
|
|
9
10
|
Classifier: Development Status :: 5 - Production/Stable
|
|
10
11
|
Classifier: Intended Audience :: Developers
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Natural Language :: English
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
20
|
Classifier: Topic :: Software Development :: Build Tools
|
|
19
21
|
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
-
Requires-Dist: aiofiles (>=
|
|
21
|
-
Requires-Dist: aiohttp (>=3.
|
|
22
|
+
Requires-Dist: aiofiles (>=24)
|
|
23
|
+
Requires-Dist: aiohttp (>=3.10.0)
|
|
22
24
|
Requires-Dist: aioshutil (>=1.3)
|
|
23
25
|
Requires-Dist: async-timeout (>=3.0.1)
|
|
26
|
+
Requires-Dist: convertertools (>=0.5.0)
|
|
24
27
|
Requires-Dist: dateparser (>=1.1.0)
|
|
25
28
|
Requires-Dist: orjson (>=3.9.15)
|
|
26
29
|
Requires-Dist: packaging (>=23)
|
|
27
30
|
Requires-Dist: pillow (>=10)
|
|
28
31
|
Requires-Dist: platformdirs (>=4)
|
|
29
|
-
Requires-Dist:
|
|
32
|
+
Requires-Dist: propcache (>=0.0.0)
|
|
33
|
+
Requires-Dist: pydantic (>=2.10.0)
|
|
34
|
+
Requires-Dist: pydantic-extra-types (>=2.10.1)
|
|
30
35
|
Requires-Dist: pyjwt (>=2.6)
|
|
31
36
|
Requires-Dist: rich (>=10)
|
|
32
37
|
Requires-Dist: typer (>=0.12.3)
|
|
@@ -89,6 +94,31 @@ Install this via pip (or your favorite package manager):
|
|
|
89
94
|
|
|
90
95
|
`pip install uiprotect`
|
|
91
96
|
|
|
97
|
+
## Developer Setup
|
|
98
|
+
|
|
99
|
+
The recommended way to develop is using the provided **devcontainer** with VS Code:
|
|
100
|
+
|
|
101
|
+
1. Install [VS Code](https://code.visualstudio.com/) and the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
|
102
|
+
2. Open the project in VS Code
|
|
103
|
+
3. When prompted, click "Reopen in Container" (or use Command Palette: "Dev Containers: Reopen in Container")
|
|
104
|
+
4. The devcontainer will automatically set up Python, Poetry, pre-commit hooks, and all dependencies
|
|
105
|
+
|
|
106
|
+
Alternatively, if you want to develop natively without devcontainer:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Install dependencies
|
|
110
|
+
poetry install --with dev
|
|
111
|
+
|
|
112
|
+
# Install pre-commit hooks
|
|
113
|
+
poetry run pre-commit install --install-hooks
|
|
114
|
+
|
|
115
|
+
# Run tests
|
|
116
|
+
poetry run pytest
|
|
117
|
+
|
|
118
|
+
# Run pre-commit checks manually
|
|
119
|
+
poetry run pre-commit run --all-files
|
|
120
|
+
```
|
|
121
|
+
|
|
92
122
|
## History
|
|
93
123
|
|
|
94
124
|
This project was split off from `pyunifiprotect` because that project changed its license to one that would not be accepted in Home Assistant. This project is committed to keeping the MIT license.
|
|
@@ -119,14 +149,6 @@ The API is not documented by Ubiquiti, so there might be misses and/or frequent
|
|
|
119
149
|
|
|
120
150
|
The module is primarily written for the purpose of being used in Home Assistant core [integration for UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect) but might be used for other purposes also.
|
|
121
151
|
|
|
122
|
-
## Smart Detections now Require Remote Access to enable
|
|
123
|
-
|
|
124
|
-
Smart Detections (person, vehicle, animal, face), a feature that previously could be used with local only console, [now requires you to enable remote access to enable](https://community.ui.com/questions/Cannot-enable-Smart-Detections/e3d50641-5c00-4607-9723-453cda557e35#answer/1d146426-89aa-4022-a0ae-fd5000846028).
|
|
125
|
-
|
|
126
|
-
Enabling Remote Access may grant other users access to your console [due to the fact Ubiquiti can reconfigure access controls at any time](https://community.ui.com/questions/Bug-Fix-Cloud-Access-Misconfiguration/fe8d4479-e187-4471-bf95-b2799183ceb7).
|
|
127
|
-
|
|
128
|
-
If you are not okay with the feature being locked behind Remote Access, [let Ubiquiti know](https://community.ui.com/questions/Cannot-enable-Smart-Detections/e3d50641-5c00-4607-9723-453cda557e35).
|
|
129
|
-
|
|
130
152
|
## Documentation
|
|
131
153
|
|
|
132
154
|
[Full documentation for the project](https://uiprotect.readthedocs.io/).
|
|
@@ -135,8 +157,8 @@ If you are not okay with the feature being locked behind Remote Access, [let Ubi
|
|
|
135
157
|
|
|
136
158
|
If you want to install `uiprotect` natively, the below are the requirements:
|
|
137
159
|
|
|
138
|
-
- [UniFi Protect](https://ui.com/camera-security) version
|
|
139
|
-
-
|
|
160
|
+
- [UniFi Protect](https://ui.com/camera-security) version 6.0+
|
|
161
|
+
- Only UniFi Protect version 6 and newer are supported. The library is generally tested against the latest stable version and the latest EA version.
|
|
140
162
|
- [Python](https://www.python.org/) 3.10+
|
|
141
163
|
- POSIX compatible system
|
|
142
164
|
- Library is only tested on Linux, specifically the latest Debian version available for the official Python Docker images, but there is no reason the library should not work on any Linux distro or macOS.
|
|
@@ -174,7 +196,7 @@ function uiprotect() {
|
|
|
174
196
|
-e UFP_PASSWORD=YOUR_PASSWORD_HERE \
|
|
175
197
|
-e UFP_ADDRESS=YOUR_IP_ADDRESS \
|
|
176
198
|
-e UFP_PORT=443 \
|
|
177
|
-
-e UFP_SSL_VERIFY=
|
|
199
|
+
-e UFP_SSL_VERIFY=false \
|
|
178
200
|
-e TZ=America/New_York \
|
|
179
201
|
-v $PWD:/data ghcr.io/uilibs/uiprotect:latest "$@"
|
|
180
202
|
}
|
|
@@ -200,13 +222,42 @@ export UFP_USERNAME=YOUR_USERNAME_HERE
|
|
|
200
222
|
export UFP_PASSWORD=YOUR_PASSWORD_HERE
|
|
201
223
|
export UFP_ADDRESS=YOUR_IP_ADDRESS
|
|
202
224
|
export UFP_PORT=443
|
|
203
|
-
#
|
|
204
|
-
export UFP_SSL_VERIFY=
|
|
225
|
+
# set to true if you have a valid HTTPS certificate for your instance
|
|
226
|
+
export UFP_SSL_VERIFY=false
|
|
227
|
+
|
|
228
|
+
# Alternatively, use an API key for authentication (required for public API operations)
|
|
229
|
+
export UFP_API_KEY=YOUR_API_KEY_HERE
|
|
205
230
|
|
|
206
231
|
uiprotect --help
|
|
207
232
|
uiprotect nvr
|
|
208
233
|
```
|
|
209
234
|
|
|
235
|
+
#### Available CLI Commands
|
|
236
|
+
|
|
237
|
+
**Top-level commands:**
|
|
238
|
+
|
|
239
|
+
- `uiprotect shell` - Start an interactive Python shell with the API client
|
|
240
|
+
- `uiprotect create-api-key <name>` - Create a new API key for authentication
|
|
241
|
+
- `uiprotect get-meta-info` - Get metadata information
|
|
242
|
+
- `uiprotect generate-sample-data` - Generate sample data for testing
|
|
243
|
+
- `uiprotect profile-ws` - Profile WebSocket performance
|
|
244
|
+
- `uiprotect decode-ws-msg` - Decode WebSocket messages
|
|
245
|
+
|
|
246
|
+
**Device management commands:**
|
|
247
|
+
|
|
248
|
+
- `uiprotect nvr` - NVR information and settings
|
|
249
|
+
- `uiprotect events` - Event management and export
|
|
250
|
+
- `uiprotect cameras` - Camera management
|
|
251
|
+
- `uiprotect lights` - Light device management
|
|
252
|
+
- `uiprotect sensors` - Sensor management
|
|
253
|
+
- `uiprotect viewers` - Viewer management
|
|
254
|
+
- `uiprotect liveviews` - Live view configuration
|
|
255
|
+
- `uiprotect chimes` - Chime management
|
|
256
|
+
- `uiprotect doorlocks` - Door lock management
|
|
257
|
+
- `uiprotect aiports` - AI port management
|
|
258
|
+
|
|
259
|
+
For more details on any command, use `uiprotect <command> --help`.
|
|
260
|
+
|
|
210
261
|
### Python
|
|
211
262
|
|
|
212
263
|
UniFi Protect itself is 100% async, so as such this library is primarily designed to be used in an async context.
|
|
@@ -216,8 +267,12 @@ The main interface for the library is the `uiprotect.ProtectApiClient`:
|
|
|
216
267
|
```python
|
|
217
268
|
from uiprotect import ProtectApiClient
|
|
218
269
|
|
|
270
|
+
# Initialize with username/password
|
|
219
271
|
protect = ProtectApiClient(host, port, username, password, verify_ssl=True)
|
|
220
272
|
|
|
273
|
+
# Or with API key (required for public API operations)
|
|
274
|
+
protect = ProtectApiClient(host, port, username, password, api_key=api_key, verify_ssl=True)
|
|
275
|
+
|
|
221
276
|
await protect.update() # this will initialize the protect .bootstrap and open a Websocket connection for updates
|
|
222
277
|
|
|
223
278
|
# get names of your cameras
|
|
@@ -237,6 +292,8 @@ unsub()
|
|
|
237
292
|
|
|
238
293
|
## TODO / Planned / Not Implemented
|
|
239
294
|
|
|
295
|
+
Switching from Protect Private API to the New Public API
|
|
296
|
+
|
|
240
297
|
Generally any feature missing from the library is planned to be done eventually / nice to have with the following exceptions
|
|
241
298
|
|
|
242
299
|
### UniFi OS Features
|
|
@@ -251,5 +308,4 @@ Anything that is strictly a UniFi OS feature. If it is ever done, it will be in
|
|
|
251
308
|
Some features that require an Ubiquiti Account or "Remote Access" to be enabled are currently not implemented. Examples include:
|
|
252
309
|
|
|
253
310
|
- Stream sharing
|
|
254
|
-
- Face detection
|
|
255
311
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
uiprotect/__init__.py,sha256=Oz6i1tonIz4QWVnEPkbielJDJ3WQdwZVgYtjY4IwGAQ,636
|
|
2
|
+
uiprotect/__main__.py,sha256=C_bHCOkv5qj6WMy-6ELoY3Y6HDhLxOa1a30CzmbZhsg,462
|
|
3
|
+
uiprotect/_compat.py,sha256=HThmb1zQZCEssCxYYbQzFhJq8zYYlVaSnIEZabKc-6U,302
|
|
4
|
+
uiprotect/api.py,sha256=5wOjGvi4NfevJWooHD12X-hmjI9l2BW1yrbIyT_Cocg,101631
|
|
5
|
+
uiprotect/cli/__init__.py,sha256=BvmuccQA16q4__YXnig4vhwMqfeXwEH1V3kNiCmroRU,11905
|
|
6
|
+
uiprotect/cli/aiports.py,sha256=22sC-OVkUFfBGJR2oID8QzWsk4GQfLEmam06-LBP2Z0,1545
|
|
7
|
+
uiprotect/cli/backup.py,sha256=lC44FujSYgVBUs32CbsY8pBejO4qScy6U94UzO-l2fc,36742
|
|
8
|
+
uiprotect/cli/base.py,sha256=FojWPuLHlZ3kpn7WZTQjCbWwweWtP-xDpmtT88xAcds,7565
|
|
9
|
+
uiprotect/cli/cameras.py,sha256=sdlhrQj2o63Se9hgZeiZs7HIMJrtaPH702ExIGYcTEU,21328
|
|
10
|
+
uiprotect/cli/chimes.py,sha256=5-ARR0hVsmg8EvtCQMllCvIsHHjB81hfVdgG8Y6ZEOw,5283
|
|
11
|
+
uiprotect/cli/doorlocks.py,sha256=zoRYE0IsnEI0x7t8aBj08GFZeDNnDGe5IrUFpP6Mxqk,3489
|
|
12
|
+
uiprotect/cli/events.py,sha256=x2a9-18Bt-SPqz1xwNH4CjKAtvbIrpeOiUwSQj065BA,7137
|
|
13
|
+
uiprotect/cli/lights.py,sha256=U7K-YHg2nnsfZfcpjJr5RB0UUVbd3Vn5nlDIA6ng6yo,3530
|
|
14
|
+
uiprotect/cli/liveviews.py,sha256=wJLJh33UVqSOB6UpQhR3tO--CXxGTtJz_WBhPLZLPkc,1832
|
|
15
|
+
uiprotect/cli/nvr.py,sha256=TwxEg2XT8jXAbOqv6gc7KFXELKadeItEDYweSL4_-e8,4260
|
|
16
|
+
uiprotect/cli/sensors.py,sha256=crz_R52X7EFKQtBgL2QzacThExOOoN5NubERQuw5jpk,8119
|
|
17
|
+
uiprotect/cli/viewers.py,sha256=IgJpOzwdo9HVs55Osf3uC5d0raeU19WFIW-RfrnnOug,2137
|
|
18
|
+
uiprotect/data/__init__.py,sha256=audwJBjxRiYdNPeYlP6iofFIOq3gyQzh6VpDsOCM2dQ,2964
|
|
19
|
+
uiprotect/data/base.py,sha256=_xFBNq6Uwd8u1gRy8Z3K4-qnhDTWy0L8F6m7qSBzvPw,35533
|
|
20
|
+
uiprotect/data/bootstrap.py,sha256=ZZD8f1uz2nOWogedFQJWvLuFllcdaoYAYbL4uWEDdG4,23853
|
|
21
|
+
uiprotect/data/convert.py,sha256=xEN878_hm0HZZCVYGwJSxcSp2as9zpkvsemVIibReOA,2628
|
|
22
|
+
uiprotect/data/devices.py,sha256=8ntKqhtRNSE6eL9HYWyqHrAnlBt4yNTY2DUMf3au1z4,120633
|
|
23
|
+
uiprotect/data/nvr.py,sha256=53PGBaduKyLp1mFFiBaBpzlX0IMGrgsmTa8r-hFsX2k,51060
|
|
24
|
+
uiprotect/data/types.py,sha256=szB5vOzLaiJm0o3Lhdtaoawq54kNtiOFMWKjfSVuy4o,19869
|
|
25
|
+
uiprotect/data/user.py,sha256=Del5LUmt5uCfAQMI9-kl_GaKm085oTLjxmcCrlEKXxc,10526
|
|
26
|
+
uiprotect/data/websocket.py,sha256=m4EV1Qfh08eKOihy70ycViYgEQpeNSGZQJWdtGIYJDA,6791
|
|
27
|
+
uiprotect/exceptions.py,sha256=kgn0cRM6lTtgLza09SDa3ZiX6ue1QqHCOogQ4qu6KTQ,965
|
|
28
|
+
uiprotect/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
+
uiprotect/release_cache.json,sha256=NamnSFy78hOWY0DPO87J9ELFCAN6NnVquv8gQO75ZG4,386
|
|
30
|
+
uiprotect/stream.py,sha256=ls65vMOXF4IlJ5axewFITfhcaTh_ihaFeCkCTfhy0Nk,5168
|
|
31
|
+
uiprotect/test_util/__init__.py,sha256=W57cVs3F6lv1F7wZd7In4UMo66l1JU68J3CeHr2fieY,20276
|
|
32
|
+
uiprotect/test_util/anonymize.py,sha256=GTtl-SSFS0gjhWK9Jlrk70RB78w6_spYKa-VM0jhAD4,8517
|
|
33
|
+
uiprotect/utils.py,sha256=id5_3jbseiJfULH0g0bCkU-9wHEyM5I-TgqyPuOUlHU,21533
|
|
34
|
+
uiprotect/websocket.py,sha256=BedfEVLWhiTP5Il4ULerw2cUNGlr2OLyb_91QOh3iSs,8176
|
|
35
|
+
uiprotect-7.32.0.dist-info/METADATA,sha256=eDJdE_Xq85TZ5xuhiF8dFKc9jYw6HEPC8tMcve3KGkY,12409
|
|
36
|
+
uiprotect-7.32.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
37
|
+
uiprotect-7.32.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
|
|
38
|
+
uiprotect-7.32.0.dist-info/licenses/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
|
|
39
|
+
uiprotect-7.32.0.dist-info/RECORD,,
|
uiprotect-3.8.0.dist-info/RECORD
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
uiprotect/__init__.py,sha256=UdpRSSLSy7pdDfTKf0zRIfy6KRGt_Jv-fMzYWgibbG4,686
|
|
2
|
-
uiprotect/__main__.py,sha256=C_bHCOkv5qj6WMy-6ELoY3Y6HDhLxOa1a30CzmbZhsg,462
|
|
3
|
-
uiprotect/api.py,sha256=8zfSeqDKArG2pbvImqibQx4HrMi_y06fYXrgcwYztCc,67585
|
|
4
|
-
uiprotect/cli/__init__.py,sha256=1MO8rJmjjAsfVx2x01gn5DJo8B64xdPGo6gRVJbWd18,8868
|
|
5
|
-
uiprotect/cli/backup.py,sha256=ZiS7RZnJGKI8TJKLW2cOUzkRM8nyTvE5Ov_jZZGtvSM,36708
|
|
6
|
-
uiprotect/cli/base.py,sha256=k-_qGuNT7br0iV0KE5F4wYXF75iyLLjBEckTqxC71xM,7591
|
|
7
|
-
uiprotect/cli/cameras.py,sha256=YvvMccQEYG3Wih0Ix8tan1R1vfaJ6cogg6YKWLzMUV8,16973
|
|
8
|
-
uiprotect/cli/chimes.py,sha256=XANn21bQVkestkKOm9HjxSM8ZGrRrqvUXLouaQ3LTqs,5326
|
|
9
|
-
uiprotect/cli/doorlocks.py,sha256=Go_Tn68bAcmrRAnUIi4kBiR7ciKQsu_R150ubPTjUAs,3523
|
|
10
|
-
uiprotect/cli/events.py,sha256=D5SRejKzsPpKlZ9O2J4wkJRigFRVEymiLyU8VQ43fqI,7186
|
|
11
|
-
uiprotect/cli/lights.py,sha256=RxP1ebYEn2o5812OfrovmJLaNuIDoSNWiX1FvCbcdDw,3314
|
|
12
|
-
uiprotect/cli/liveviews.py,sha256=GU5z-ZLRBXHyspDKiJpiv-kbaBcvxK_-K70rPoqx2Ms,1863
|
|
13
|
-
uiprotect/cli/nvr.py,sha256=TwxEg2XT8jXAbOqv6gc7KFXELKadeItEDYweSL4_-e8,4260
|
|
14
|
-
uiprotect/cli/sensors.py,sha256=fQtcDJCVxs4VbAqcavgBy2ABiVxAW3GXtna6_XFBp2k,8153
|
|
15
|
-
uiprotect/cli/viewers.py,sha256=2cyrp104ffIvgT0wYGIO0G35QMkEbFe7fSVqLwDXQYQ,2171
|
|
16
|
-
uiprotect/data/__init__.py,sha256=OcfuJl2qXfHcj_mdnrHhzZ5tEIZrw8auziX5IE7dn-I,2938
|
|
17
|
-
uiprotect/data/base.py,sha256=fSD5H3jAp8M-0VbvOsFkohJwbzuUaytQxxF4nbWOVKg,35122
|
|
18
|
-
uiprotect/data/bootstrap.py,sha256=DQ25j2h3AZWv52kM0dF8_kU73INmZDEMiEvSoE_CvZQ,20981
|
|
19
|
-
uiprotect/data/convert.py,sha256=8h6Il_DhMkPRDPj9F_rA2UZIlTuchS3BQD24peKpk2A,2185
|
|
20
|
-
uiprotect/data/devices.py,sha256=DrHjLmIbkOneWSMoqN6W8EL9zdspY8YM2RM8_dHA2RI,110185
|
|
21
|
-
uiprotect/data/nvr.py,sha256=kPEfFNi_gQHLBEA1JOgTjfnDb6BuPDDjZy6PMzLPR60,46749
|
|
22
|
-
uiprotect/data/types.py,sha256=3CocULpkdTgF4is1nIEDYIlwf2EOkNNM7L4kJ7NkAwM,17654
|
|
23
|
-
uiprotect/data/user.py,sha256=YvgXJKV4_y-bm0eySWz9f_ie9aR5lpVn17t9H0Pix8I,6998
|
|
24
|
-
uiprotect/data/websocket.py,sha256=5-yM6yr8NrxKJjBPQlGVXXQUTcksF-UavligKYjJQ3k,6770
|
|
25
|
-
uiprotect/exceptions.py,sha256=kgn0cRM6lTtgLza09SDa3ZiX6ue1QqHCOogQ4qu6KTQ,965
|
|
26
|
-
uiprotect/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
-
uiprotect/release_cache.json,sha256=NamnSFy78hOWY0DPO87J9ELFCAN6NnVquv8gQO75ZG4,386
|
|
28
|
-
uiprotect/stream.py,sha256=McV3XymKyjn-1uV5jdQHcpaDjqLS4zWyMASQ8ubcyb4,4924
|
|
29
|
-
uiprotect/test_util/__init__.py,sha256=whiOUb5LfDLNT3AQG6ISiKtAqO2JnhCIdFavhWDK46M,18718
|
|
30
|
-
uiprotect/test_util/anonymize.py,sha256=f-8ijU-_y9r-uAbhIPn0f0I6hzJpAkvJzc8UpWihObI,8478
|
|
31
|
-
uiprotect/utils.py,sha256=G0WkMXpCky2Cc4jynFDFFxAcVaZX4F01OXKa6cx9pho,20121
|
|
32
|
-
uiprotect/websocket.py,sha256=D5DZrMzo434ecp8toNxOB5HM193kVwYw42yEcg99yMw,8029
|
|
33
|
-
uiprotect-3.8.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
|
|
34
|
-
uiprotect-3.8.0.dist-info/METADATA,sha256=AEGF6orwjbmm9wVo992183me_Rhl5Nyk6Uz0BNaETlA,10969
|
|
35
|
-
uiprotect-3.8.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
36
|
-
uiprotect-3.8.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
|
|
37
|
-
uiprotect-3.8.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|