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/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.v1.fields import SHAPE_DICT, SHAPE_LIST, SHAPE_SET, ModelField
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 int(round(duration.total_seconds() * 1000))
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: ModelField) -> Any:
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.type_
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
- shape = field.shape
215
- if shape == SHAPE_LIST and isinstance(value, list):
216
- return [convert_unifi_data(v, field) for v in value]
217
- if shape == SHAPE_SET and isinstance(value, list):
218
- return {convert_unifi_data(v, field) for v in value}
219
- if shape == SHAPE_DICT and isinstance(value, dict):
220
- return {k: convert_unifi_data(v, field) for k, v in value.items()}
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
- try:
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 or _is_enum_type(type_):
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
- # handle edge case for improperly formatted UUIDs
235
- # 00000000-0000-00 0- 000-000000000000
236
- if type_ is UUID and value == _BAD_UUID:
237
- return _EMPTY_UUID
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
- def serialize_unifi_obj(value: Any, levels: int = -1) -> Any:
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
- from uiprotect.data import Percent
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
- return ip_address(socket.gethostbyname(host))
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 dt as dt_util # type: ignore[import-not-found]
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.dict())
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.value if isinstance(value, Enum) else 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.value if isinstance(value, Enum) else 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
- value = getattr(obj, attr)
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 partial(get_top_level_attr, ufp_value)
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, Optional
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, Optional[dict[str, str]]]]
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, ssl=ssl, headers=self._headers, timeout=self.timeout
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.timeout)
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
- elif msg_type in _CLOSE_MESSAGE_TYPES:
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
1
+ Metadata-Version: 2.4
2
2
  Name: uiprotect
3
- Version: 3.8.0
3
+ Version: 7.32.0
4
4
  Summary: Python API for Unifi Protect (Unofficial)
5
- Home-page: https://github.com/uilibs/uiprotect
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 (>=23)
21
- Requires-Dist: aiohttp (>=3.9.0)
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: pydantic (>=1.10.17)
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 1.20+
139
- - Latest version of library is generally only tested against the two latest minor version. This is either two latest stable versions (such as 1.21.x and 2.0.x) or the latest EA version and stable version (such as 2.2.x EA and 2.1.x).
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=True \
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
- # change to false if you do not have a valid HTTPS certificate for your instance
204
- export UFP_SSL_VERIFY=True
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.0
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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,,