uiprotect 0.1.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/__init__.py +13 -0
- uiprotect/__main__.py +24 -0
- uiprotect/api.py +1936 -0
- uiprotect/cli/__init__.py +314 -0
- uiprotect/cli/backup.py +1103 -0
- uiprotect/cli/base.py +238 -0
- uiprotect/cli/cameras.py +574 -0
- uiprotect/cli/chimes.py +180 -0
- uiprotect/cli/doorlocks.py +125 -0
- uiprotect/cli/events.py +258 -0
- uiprotect/cli/lights.py +119 -0
- uiprotect/cli/liveviews.py +65 -0
- uiprotect/cli/nvr.py +154 -0
- uiprotect/cli/sensors.py +278 -0
- uiprotect/cli/viewers.py +76 -0
- uiprotect/data/__init__.py +157 -0
- uiprotect/data/base.py +1116 -0
- uiprotect/data/bootstrap.py +634 -0
- uiprotect/data/convert.py +77 -0
- uiprotect/data/devices.py +3384 -0
- uiprotect/data/nvr.py +1520 -0
- uiprotect/data/types.py +630 -0
- uiprotect/data/user.py +236 -0
- uiprotect/data/websocket.py +236 -0
- uiprotect/exceptions.py +41 -0
- uiprotect/py.typed +0 -0
- uiprotect/release_cache.json +1 -0
- uiprotect/stream.py +166 -0
- uiprotect/test_util/__init__.py +531 -0
- uiprotect/test_util/anonymize.py +257 -0
- uiprotect/utils.py +610 -0
- uiprotect/websocket.py +225 -0
- uiprotect-0.1.0.dist-info/LICENSE +23 -0
- uiprotect-0.1.0.dist-info/METADATA +245 -0
- uiprotect-0.1.0.dist-info/RECORD +37 -0
- uiprotect-0.1.0.dist-info/WHEEL +4 -0
- uiprotect-0.1.0.dist-info/entry_points.txt +3 -0
uiprotect/websocket.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""UniFi Protect Websockets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Callable, Coroutine
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from aiohttp import (
|
|
12
|
+
ClientError,
|
|
13
|
+
ClientSession,
|
|
14
|
+
ClientWebSocketResponse,
|
|
15
|
+
WSMessage,
|
|
16
|
+
WSMsgType,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from .utils import asyncio_timeout
|
|
20
|
+
|
|
21
|
+
_LOGGER = logging.getLogger(__name__)
|
|
22
|
+
CALLBACK_TYPE = Callable[..., Coroutine[Any, Any, Optional[dict[str, str]]]]
|
|
23
|
+
RECENT_FAILURE_CUT_OFF = 30
|
|
24
|
+
RECENT_FAILURE_THRESHOLD = 2
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Websocket:
|
|
28
|
+
"""UniFi Protect Websocket manager."""
|
|
29
|
+
|
|
30
|
+
url: str
|
|
31
|
+
verify: bool
|
|
32
|
+
timeout_interval: int
|
|
33
|
+
backoff: int
|
|
34
|
+
_auth: CALLBACK_TYPE
|
|
35
|
+
_timeout: float
|
|
36
|
+
_ws_subscriptions: list[Callable[[WSMessage], None]]
|
|
37
|
+
_connect_lock: asyncio.Lock
|
|
38
|
+
|
|
39
|
+
_headers: dict[str, str] | None = None
|
|
40
|
+
_websocket_loop_task: asyncio.Task[None] | None = None
|
|
41
|
+
_timer_task: asyncio.Task[None] | None = None
|
|
42
|
+
_ws_connection: ClientWebSocketResponse | None = None
|
|
43
|
+
_last_connect: float = -1000
|
|
44
|
+
_recent_failures: int = 0
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
url: str,
|
|
49
|
+
auth_callback: CALLBACK_TYPE,
|
|
50
|
+
*,
|
|
51
|
+
timeout: int = 30,
|
|
52
|
+
backoff: int = 10,
|
|
53
|
+
verify: bool = True,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Init Websocket."""
|
|
56
|
+
self.url = url
|
|
57
|
+
self.timeout_interval = timeout
|
|
58
|
+
self.backoff = backoff
|
|
59
|
+
self.verify = verify
|
|
60
|
+
self._auth = auth_callback
|
|
61
|
+
self._timeout = time.monotonic()
|
|
62
|
+
self._ws_subscriptions = []
|
|
63
|
+
self._connect_lock = asyncio.Lock()
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def is_connected(self) -> bool:
|
|
67
|
+
"""Check if Websocket connected."""
|
|
68
|
+
return self._ws_connection is not None
|
|
69
|
+
|
|
70
|
+
def _get_session(self) -> ClientSession:
|
|
71
|
+
# for testing, to make easier to mock
|
|
72
|
+
return ClientSession()
|
|
73
|
+
|
|
74
|
+
def _process_message(self, msg: WSMessage) -> bool:
|
|
75
|
+
if msg.type == WSMsgType.ERROR:
|
|
76
|
+
_LOGGER.exception("Error from Websocket: %s", msg.data)
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
for sub in self._ws_subscriptions:
|
|
80
|
+
try:
|
|
81
|
+
sub(msg)
|
|
82
|
+
except Exception:
|
|
83
|
+
_LOGGER.exception("Error processing websocket message")
|
|
84
|
+
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
async def _websocket_loop(self, start_event: asyncio.Event) -> None:
|
|
88
|
+
_LOGGER.debug("Connecting WS to %s", self.url)
|
|
89
|
+
self._headers = await self._auth(self._should_reset_auth)
|
|
90
|
+
|
|
91
|
+
session = self._get_session()
|
|
92
|
+
# catch any and all errors for Websocket so we can clean up correctly
|
|
93
|
+
try:
|
|
94
|
+
self._ws_connection = await session.ws_connect(
|
|
95
|
+
self.url,
|
|
96
|
+
ssl=None if self.verify else False,
|
|
97
|
+
headers=self._headers,
|
|
98
|
+
)
|
|
99
|
+
start_event.set()
|
|
100
|
+
|
|
101
|
+
self._reset_timeout()
|
|
102
|
+
async for msg in self._ws_connection:
|
|
103
|
+
if not self._process_message(msg):
|
|
104
|
+
break
|
|
105
|
+
self._reset_timeout()
|
|
106
|
+
except ClientError:
|
|
107
|
+
_LOGGER.exception("Websocket disconnect error: %s")
|
|
108
|
+
finally:
|
|
109
|
+
_LOGGER.debug("Websocket disconnected")
|
|
110
|
+
self._increase_failure()
|
|
111
|
+
self._cancel_timeout()
|
|
112
|
+
if self._ws_connection is not None and not self._ws_connection.closed:
|
|
113
|
+
await self._ws_connection.close()
|
|
114
|
+
if not session.closed:
|
|
115
|
+
await session.close()
|
|
116
|
+
self._ws_connection = None
|
|
117
|
+
# make sure event does not timeout
|
|
118
|
+
start_event.set()
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def has_recent_connect(self) -> bool:
|
|
122
|
+
"""Check if Websocket has recent connection."""
|
|
123
|
+
return time.monotonic() - RECENT_FAILURE_CUT_OFF <= self._last_connect
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def _should_reset_auth(self) -> bool:
|
|
127
|
+
if self.has_recent_connect:
|
|
128
|
+
if self._recent_failures > RECENT_FAILURE_THRESHOLD:
|
|
129
|
+
return True
|
|
130
|
+
else:
|
|
131
|
+
self._recent_failures = 0
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
def _increase_failure(self) -> None:
|
|
135
|
+
if self.has_recent_connect:
|
|
136
|
+
self._recent_failures += 1
|
|
137
|
+
else:
|
|
138
|
+
self._recent_failures = 1
|
|
139
|
+
|
|
140
|
+
async def _do_timeout(self) -> bool:
|
|
141
|
+
_LOGGER.debug("WS timed out")
|
|
142
|
+
return await self.reconnect()
|
|
143
|
+
|
|
144
|
+
async def _timeout_loop(self) -> None:
|
|
145
|
+
while True:
|
|
146
|
+
now = time.monotonic()
|
|
147
|
+
if now > self._timeout:
|
|
148
|
+
_LOGGER.debug("WS timed out")
|
|
149
|
+
if not await self.reconnect():
|
|
150
|
+
_LOGGER.debug("WS could not reconnect")
|
|
151
|
+
continue
|
|
152
|
+
sleep_time = self._timeout - now
|
|
153
|
+
_LOGGER.debug("WS Timeout loop sleep %s", sleep_time)
|
|
154
|
+
await asyncio.sleep(sleep_time)
|
|
155
|
+
|
|
156
|
+
def _reset_timeout(self) -> None:
|
|
157
|
+
self._timeout = time.monotonic() + self.timeout_interval
|
|
158
|
+
|
|
159
|
+
if self._timer_task is None:
|
|
160
|
+
self._timer_task = asyncio.create_task(self._timeout_loop())
|
|
161
|
+
|
|
162
|
+
def _cancel_timeout(self) -> None:
|
|
163
|
+
if self._timer_task:
|
|
164
|
+
self._timer_task.cancel()
|
|
165
|
+
|
|
166
|
+
async def connect(self) -> bool:
|
|
167
|
+
"""Connect the websocket."""
|
|
168
|
+
if self._connect_lock.locked():
|
|
169
|
+
_LOGGER.debug("Another connect is already happening")
|
|
170
|
+
return False
|
|
171
|
+
try:
|
|
172
|
+
async with asyncio_timeout(0.1):
|
|
173
|
+
await self._connect_lock.acquire()
|
|
174
|
+
except (TimeoutError, asyncio.TimeoutError, asyncio.CancelledError):
|
|
175
|
+
_LOGGER.debug("Failed to get connection lock")
|
|
176
|
+
|
|
177
|
+
start_event = asyncio.Event()
|
|
178
|
+
_LOGGER.debug("Scheduling WS connect...")
|
|
179
|
+
self._websocket_loop_task = asyncio.create_task(
|
|
180
|
+
self._websocket_loop(start_event),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
async with asyncio_timeout(self.timeout_interval):
|
|
185
|
+
await start_event.wait()
|
|
186
|
+
except (TimeoutError, asyncio.TimeoutError, asyncio.CancelledError):
|
|
187
|
+
_LOGGER.warning("Timed out while waiting for Websocket to connect")
|
|
188
|
+
await self.disconnect()
|
|
189
|
+
|
|
190
|
+
self._connect_lock.release()
|
|
191
|
+
if self._ws_connection is None:
|
|
192
|
+
_LOGGER.debug("Failed to connect to Websocket")
|
|
193
|
+
return False
|
|
194
|
+
_LOGGER.debug("Connected to Websocket successfully")
|
|
195
|
+
self._last_connect = time.monotonic()
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
async def disconnect(self) -> None:
|
|
199
|
+
"""Disconnect the websocket."""
|
|
200
|
+
_LOGGER.debug("Disconnecting websocket...")
|
|
201
|
+
if self._ws_connection is None:
|
|
202
|
+
return
|
|
203
|
+
await self._ws_connection.close()
|
|
204
|
+
self._ws_connection = None
|
|
205
|
+
|
|
206
|
+
async def reconnect(self) -> bool:
|
|
207
|
+
"""Reconnect the websocket."""
|
|
208
|
+
_LOGGER.debug("Reconnecting websocket...")
|
|
209
|
+
await self.disconnect()
|
|
210
|
+
await asyncio.sleep(self.backoff)
|
|
211
|
+
return await self.connect()
|
|
212
|
+
|
|
213
|
+
def subscribe(self, ws_callback: Callable[[WSMessage], None]) -> Callable[[], None]:
|
|
214
|
+
"""
|
|
215
|
+
Subscribe to raw websocket messages.
|
|
216
|
+
|
|
217
|
+
Returns a callback that will unsubscribe.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
def _unsub_ws_callback() -> None:
|
|
221
|
+
self._ws_subscriptions.remove(ws_callback)
|
|
222
|
+
|
|
223
|
+
_LOGGER.debug("Adding subscription: %s", ws_callback)
|
|
224
|
+
self._ws_subscriptions.append(ws_callback)
|
|
225
|
+
return _unsub_ws_callback
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
MIT License
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2024 UI Protect Maintainers
|
|
5
|
+
Copyright (c) 2020 Bjarne Riis
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: uiprotect
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python API for Unifi Protect (Unofficial)
|
|
5
|
+
Home-page: https://github.com/uilibs/uiprotect
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: UI Protect Maintainers
|
|
8
|
+
Author-email: ui@koston.org
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Natural Language :: English
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Requires-Dist: aiofiles (>=23)
|
|
21
|
+
Requires-Dist: aiohttp (>=3.9.0)
|
|
22
|
+
Requires-Dist: aioshutil (>=1.3)
|
|
23
|
+
Requires-Dist: async-timeout (>=3.0.1)
|
|
24
|
+
Requires-Dist: dateparser (>=1.1.0)
|
|
25
|
+
Requires-Dist: orjson (>=3.9)
|
|
26
|
+
Requires-Dist: packaging (>=23)
|
|
27
|
+
Requires-Dist: pillow (>=10)
|
|
28
|
+
Requires-Dist: platformdirs (>=4)
|
|
29
|
+
Requires-Dist: pydantic (!=1.9.1)
|
|
30
|
+
Requires-Dist: pyjwt (>=2.6)
|
|
31
|
+
Requires-Dist: rich (>=10)
|
|
32
|
+
Requires-Dist: typer[all] (>0.6)
|
|
33
|
+
Requires-Dist: yarl (>=1.9)
|
|
34
|
+
Project-URL: Bug Tracker, https://github.com/uilibs/uiprotect/issues
|
|
35
|
+
Project-URL: Changelog, https://github.com/uilibs/uiprotect/blob/main/CHANGELOG.md
|
|
36
|
+
Project-URL: Documentation, https://uiprotect.readthedocs.io
|
|
37
|
+
Project-URL: Repository, https://github.com/uilibs/uiprotect
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# Unofficial UniFi Protect Python API and CLI
|
|
41
|
+
|
|
42
|
+
<p align="center">
|
|
43
|
+
<a href="https://github.com/uilibs/uiprotect/actions/workflows/ci.yml?query=branch%3Amain">
|
|
44
|
+
<img src="https://img.shields.io/github/actions/workflow/status/uilibs/uiprotect/ci.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >
|
|
45
|
+
</a>
|
|
46
|
+
<a href="https://uiprotect.readthedocs.io">
|
|
47
|
+
<img src="https://img.shields.io/readthedocs/uiprotect.svg?logo=read-the-docs&logoColor=fff&style=flat-square" alt="Documentation Status">
|
|
48
|
+
</a>
|
|
49
|
+
<a href="https://codecov.io/gh/uilibs/uiprotect">
|
|
50
|
+
<img src="https://img.shields.io/codecov/c/github/uilibs/uiprotect.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">
|
|
51
|
+
</a>
|
|
52
|
+
</p>
|
|
53
|
+
<p align="center">
|
|
54
|
+
<a href="https://python-poetry.org/">
|
|
55
|
+
<img src="https://img.shields.io/endpoint?url=https://python-poetry.org/badge/v0.json" alt="Poetry">
|
|
56
|
+
</a>
|
|
57
|
+
<a href="https://github.com/astral-sh/ruff">
|
|
58
|
+
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff">
|
|
59
|
+
</a>
|
|
60
|
+
<a href="https://github.com/pre-commit/pre-commit">
|
|
61
|
+
<img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square" alt="pre-commit">
|
|
62
|
+
</a>
|
|
63
|
+
</p>
|
|
64
|
+
<p align="center">
|
|
65
|
+
<a href="https://pypi.org/project/uiprotect/">
|
|
66
|
+
<img src="https://img.shields.io/pypi/v/uiprotect.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
|
|
67
|
+
</a>
|
|
68
|
+
<img src="https://img.shields.io/pypi/pyversions/uiprotect.svg?style=flat-square&logo=python&logoColor=fff" alt="Supported Python versions">
|
|
69
|
+
<img src="https://img.shields.io/pypi/l/uiprotect.svg?style=flat-square" alt="License">
|
|
70
|
+
</p>
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
**Documentation**: <a href="https://uiprotect.readthedocs.io" target="_blank">https://uiprotect.readthedocs.io </a>
|
|
75
|
+
|
|
76
|
+
**Source Code**: <a href="https://github.com/uilibs/uiprotect" target="_blank">https://github.com/uilibs/uiprotect </a>
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
Python API for Unifi Protect (Unofficial)
|
|
81
|
+
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
Install this via pip (or your favorite package manager):
|
|
85
|
+
|
|
86
|
+
`pip install uiprotect`
|
|
87
|
+
|
|
88
|
+
## Credits
|
|
89
|
+
|
|
90
|
+
- Bjarne Riis ([@briis](https://github.com/briis/)) for the original pyunifiprotect package
|
|
91
|
+
- Christopher Bailey ([@AngellusMortis](https://github.com/AngellusMortis/)) for the maintaining the pyunifiprotect package
|
|
92
|
+
|
|
93
|
+
## Contributors ✨
|
|
94
|
+
|
|
95
|
+
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
|
96
|
+
|
|
97
|
+
<!-- prettier-ignore-start -->
|
|
98
|
+
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
|
99
|
+
<!-- markdownlint-disable -->
|
|
100
|
+
<!-- markdownlint-enable -->
|
|
101
|
+
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
|
102
|
+
<!-- prettier-ignore-end -->
|
|
103
|
+
|
|
104
|
+
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
|
105
|
+
|
|
106
|
+
`uiprotect` is an unofficial API for UniFi Protect. There is no affiliation with Ubiquiti.
|
|
107
|
+
|
|
108
|
+
This module communicates with UniFi Protect surveillance software installed on a UniFi OS Console such as a Ubiquiti CloudKey+ or UniFi Dream Machine Pro.
|
|
109
|
+
|
|
110
|
+
The API is not documented by Ubiquiti, so there might be misses and/or frequent changes in this module, as Ubiquiti evolves the software.
|
|
111
|
+
|
|
112
|
+
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.
|
|
113
|
+
|
|
114
|
+
## Smart Detections now Require Remote Access to enable
|
|
115
|
+
|
|
116
|
+
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).
|
|
117
|
+
|
|
118
|
+
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).
|
|
119
|
+
|
|
120
|
+
If you are not okay with the feature being locked behind Remote Access access, [let Ubiquiti know](https://community.ui.com/questions/Cannot-enable-Smart-Detections/e3d50641-5c00-4607-9723-453cda557e35).
|
|
121
|
+
|
|
122
|
+
## Documentation
|
|
123
|
+
|
|
124
|
+
[Full documentation for the project](https://uilibs.github.io/uiprotect/).
|
|
125
|
+
|
|
126
|
+
## Requirements
|
|
127
|
+
|
|
128
|
+
If you want to install `uiprotect` natively, the below are the requirements:
|
|
129
|
+
|
|
130
|
+
- [UniFi Protect](https://ui.com/camera-security) version 1.20+
|
|
131
|
+
- 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).
|
|
132
|
+
- [Python](https://www.python.org/) 3.9+
|
|
133
|
+
- POSIX compatible system
|
|
134
|
+
- Library is only test 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.
|
|
135
|
+
- [ffmpeg](https://ffmpeg.org/)
|
|
136
|
+
- ffmpeg is primarily only for streaming audio to Protect cameras, this can be considered a soft requirement
|
|
137
|
+
|
|
138
|
+
Alternatively you can use the [provided Docker container](#using-docker-container), in which case the only requirement is [Docker](https://docs.docker.com/desktop/) or another OCI compatible orchestrator (such as Kubernetes or podman).
|
|
139
|
+
|
|
140
|
+
Windows is **not supported**. If you need to use `uiprotect` on Windows, use Docker Desktop and the provided docker container or [WSL](https://docs.microsoft.com/en-us/windows/wsl/install).
|
|
141
|
+
|
|
142
|
+
## Install
|
|
143
|
+
|
|
144
|
+
### From PyPi
|
|
145
|
+
|
|
146
|
+
`uiprotect` is available on PyPi:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
pip install uiprotect
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### From Github
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pip install git+https://github.com/uilibs/uiprotect.git#egg=uiprotect
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Using Docker Container
|
|
159
|
+
|
|
160
|
+
A Docker container is also provided so you do not need to install/manage Python as well. You can add the following to your `.bashrc` or similar.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
function unifi-protect() {
|
|
164
|
+
docker run --rm -it \
|
|
165
|
+
-e UFP_USERNAME=YOUR_USERNAME_HERE \
|
|
166
|
+
-e UFP_PASSWORD=YOUR_PASSWORD_HERE \
|
|
167
|
+
-e UFP_ADDRESS=YOUR_IP_ADDRESS \
|
|
168
|
+
-e UFP_PORT=443 \
|
|
169
|
+
-e UFP_SSL_VERIFY=True \
|
|
170
|
+
-e TZ=America/New_York \
|
|
171
|
+
-v $PWD:/data ghcr.io/uilibs/uiprotect:latest "$@"
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Some notes about the Docker version since it is running inside of a container:
|
|
176
|
+
|
|
177
|
+
- You can update at any time using the command `docker pull ghcr.io/uilibs/uiprotect:latest`
|
|
178
|
+
- Your local current working directory (`$PWD`) will automatically be mounted to `/data` inside of the container. For commands that output files, this is the _only_ path you can write to and have the file persist.
|
|
179
|
+
- The container supports `linux/amd64` and `linux/arm64` natively. This means it will also work well on MacOS or Windows using Docker Desktop.
|
|
180
|
+
- `TZ` should be the [Olson timezone name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for the timezone your UniFi Protect instance is in.
|
|
181
|
+
- For more details on `TZ` and other environment variables, check the [command line docs](https://uilibs.github.io/uiprotect/latest/cli/)
|
|
182
|
+
|
|
183
|
+
## Quickstart
|
|
184
|
+
|
|
185
|
+
### CLI
|
|
186
|
+
|
|
187
|
+
!!! warning "About Ubiquiti SSO accounts"
|
|
188
|
+
Ubiquiti SSO accounts are not supported and actively discouraged from being used. There is no option to use MFA. You are expected to use local access user. `uiprotect` is not designed to allow you to use your owner account to access the your console or to be used over the public Internet as both pose a security risk.
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
export UFP_USERNAME=YOUR_USERNAME_HERE
|
|
192
|
+
export UFP_PASSWORD=YOUR_PASSWORD_HERE
|
|
193
|
+
export UFP_ADDRESS=YOUR_IP_ADDRESS
|
|
194
|
+
export UFP_PORT=443
|
|
195
|
+
# change to false if you do not have a valid HTTPS Certificate for your instance
|
|
196
|
+
export UFP_SSL_VERIFY=True
|
|
197
|
+
|
|
198
|
+
unifi-protect --help
|
|
199
|
+
unifi-protect nvr
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Python
|
|
203
|
+
|
|
204
|
+
UniFi Protect itself is 100% async, so as such this library is primarily designed to be used in an async context.
|
|
205
|
+
|
|
206
|
+
The main interface for the library is the `uiprotect.ProtectApiClient`:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from uiprotect import ProtectApiClient
|
|
210
|
+
|
|
211
|
+
protect = ProtectApiClient(host, port, username, password, verify_ssl=True)
|
|
212
|
+
|
|
213
|
+
await protect.update() # this will initialize the protect .bootstrap and open a Websocket connection for updates
|
|
214
|
+
|
|
215
|
+
# get names of your cameras
|
|
216
|
+
for camera in protect.bootstrap.cameras.values():
|
|
217
|
+
print(camera.name)
|
|
218
|
+
|
|
219
|
+
# subscribe to Websocket for updates to UFP
|
|
220
|
+
def callback(msg: WSSubscriptionMessage):
|
|
221
|
+
# do stuff
|
|
222
|
+
|
|
223
|
+
unsub = protect.subscribe_websocket(callback)
|
|
224
|
+
|
|
225
|
+
# remove subscription
|
|
226
|
+
unsub()
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## TODO / Planned / Not Implemented
|
|
231
|
+
|
|
232
|
+
Generally any feature missing from the library is planned to be done eventually / nice to have with the following exceptions
|
|
233
|
+
|
|
234
|
+
### UniFi OS Features
|
|
235
|
+
|
|
236
|
+
Anything that is strictly a UniFi OS feature. If it ever done, it will be in a separate library that interacts with this one. Examples include:
|
|
237
|
+
|
|
238
|
+
- Managing RAID and disks
|
|
239
|
+
- Creating and managing users
|
|
240
|
+
|
|
241
|
+
Examples include:
|
|
242
|
+
|
|
243
|
+
- Stream sharing
|
|
244
|
+
- Smart Detections, including person, vehicle, animals license plate and faces
|
|
245
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
uiprotect/__init__.py,sha256=tzcxK6cOIIkFEtST4Zjq3bafYTFWtwnaL92DgypjFi8,307
|
|
2
|
+
uiprotect/__main__.py,sha256=T69KE5W4zek6qeNEL8_Fq2DEfBc04SqSuIOJpiW4ydE,471
|
|
3
|
+
uiprotect/api.py,sha256=mmAmPBs0Zev_2KWLO_l5QU8pFOsFzuYt-OZMvfV3mq0,65694
|
|
4
|
+
uiprotect/cli/__init__.py,sha256=tmwtYrhM8rjOw8noKD6NALl_2Nu3xIycDuQyEKD5oMk,8832
|
|
5
|
+
uiprotect/cli/backup.py,sha256=Mtjd2f0w7zjIeLkJILKxiBnZJYXypzC9gpyAmBZ8sq0,36746
|
|
6
|
+
uiprotect/cli/base.py,sha256=8EqGb7tJulq_IHGrZuRtevfpX7l08Hc6yizBU1snZp8,7627
|
|
7
|
+
uiprotect/cli/cameras.py,sha256=mVLJ5FjZPSbpNXvtd7Yiz17zMDSdLWT0p6Wcaqpt0GQ,16996
|
|
8
|
+
uiprotect/cli/chimes.py,sha256=92kNiZN7FpuCF7CqK9oLPt2jQ96tNhTjP1JcOdJAKhU,5350
|
|
9
|
+
uiprotect/cli/doorlocks.py,sha256=XMftrrK0O0bWZHOuTwg9m4p9U7gC0FzHFb72bSnlUEg,3547
|
|
10
|
+
uiprotect/cli/events.py,sha256=Ni_igTOdOf11pklpNEocBdwd3EW6O0wD8Iz-lN2zMJA,7225
|
|
11
|
+
uiprotect/cli/lights.py,sha256=EgJsQ37c3QzSKoYf6h-rCaQoiftNh2igY1bTB_tQz14,3338
|
|
12
|
+
uiprotect/cli/liveviews.py,sha256=TrLAqG2_GpNoWyw9IRY-4mJmlFwxGZxUuIdi-YU_NuM,1887
|
|
13
|
+
uiprotect/cli/nvr.py,sha256=PVpUitWzP9HLy2yoj0HP-b-4Te5AFCrahUAmrQJ4z-w,4276
|
|
14
|
+
uiprotect/cli/sensors.py,sha256=CE77weDkYepo9eEs1J0ModIs3pWNWmesjAq3rlmiez0,8177
|
|
15
|
+
uiprotect/cli/viewers.py,sha256=SZlolZH3kkcWKjATrA37TCVZYRpF0t4cCccrC8vIv-M,2195
|
|
16
|
+
uiprotect/data/__init__.py,sha256=38Kb1DVi4kv9FTTwugatPNSWLiIp6uvUlTMZRQp2jTY,3050
|
|
17
|
+
uiprotect/data/base.py,sha256=k65F05OHN2l-nlQ5bVeXxTSYvvGj7pZz_jfe2KmWOSk,37423
|
|
18
|
+
uiprotect/data/bootstrap.py,sha256=d3SmJa4SJkosNc_Ve_ySPnRxtIfxXdioTmAoczuz3YQ,21738
|
|
19
|
+
uiprotect/data/convert.py,sha256=f8QkwZnlNbV_W-YognlDvZ1gQKp5yFuC50396hcu6DU,2127
|
|
20
|
+
uiprotect/data/devices.py,sha256=RMucQXXAyN6VwLvFZwsxauStwXZkdQGUITSZ9h_iQXY,112765
|
|
21
|
+
uiprotect/data/nvr.py,sha256=Sr1lxmmtmQm6wje-UwY0i18cGUys8f8kvJmlFhapst4,47485
|
|
22
|
+
uiprotect/data/types.py,sha256=5eLL2jBl1eHiMwxbL6l0qngsmhg1F0dBrAS5GtDVdgg,15427
|
|
23
|
+
uiprotect/data/user.py,sha256=c-rslssNHhHOYplPE5lGGfuDexvI0mVYhbb6Xyi5dgo,7022
|
|
24
|
+
uiprotect/data/websocket.py,sha256=HKAze9b9soOS03_2zixRTR7jlFIA4ph9wMZEo15q6NY,6107
|
|
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=V4aJfVWpSUsWE1PQrXH8F7obQQi1ukPAZ7PzwABjt0I,4942
|
|
29
|
+
uiprotect/test_util/__init__.py,sha256=sSEXu6_pwdYNQSCYtftpX1Dy1S8XYOvhrpECYRxeKJE,18596
|
|
30
|
+
uiprotect/test_util/anonymize.py,sha256=AGYELhDC4BrdK0deI6zh5jFp3SuM_HvAWLeoxFHSiwg,8486
|
|
31
|
+
uiprotect/utils.py,sha256=evW60TmyccHPsifBUukqiXGd0QHnYibqaJhvowIDh-M,17841
|
|
32
|
+
uiprotect/websocket.py,sha256=iMTdchymaCgVHsmY1bRbxkcymqt6WQircIHYNxCu178,7289
|
|
33
|
+
uiprotect-0.1.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
|
|
34
|
+
uiprotect-0.1.0.dist-info/METADATA,sha256=aPwnx0VBKI4VScd1F65pHzKYWgo5He1GdIK8Cgzbbwk,10588
|
|
35
|
+
uiprotect-0.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
36
|
+
uiprotect-0.1.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
|
|
37
|
+
uiprotect-0.1.0.dist-info/RECORD,,
|