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/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&amp;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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ uiprotect=uiprotect.cli:app
3
+