uiprotect 7.22.0__tar.gz → 7.23.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of uiprotect might be problematic. Click here for more details.
- {uiprotect-7.22.0 → uiprotect-7.23.0}/PKG-INFO +1 -1
- {uiprotect-7.22.0 → uiprotect-7.23.0}/pyproject.toml +2 -2
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/api.py +364 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/LICENSE +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/README.md +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/__init__.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/__main__.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/_compat.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/__init__.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/aiports.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/backup.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/base.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/cameras.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/chimes.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/doorlocks.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/events.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/lights.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/liveviews.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/nvr.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/sensors.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/viewers.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/data/__init__.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/data/base.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/data/bootstrap.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/data/convert.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/data/devices.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/data/nvr.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/data/types.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/data/user.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/data/websocket.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/exceptions.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/py.typed +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/release_cache.json +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/stream.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/test_util/__init__.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/test_util/anonymize.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/utils.py +0 -0
- {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/websocket.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "uiprotect"
|
|
3
|
-
version = "7.
|
|
3
|
+
version = "7.23.0"
|
|
4
4
|
license = "MIT"
|
|
5
5
|
description = "Python API for Unifi Protect (Unofficial)"
|
|
6
6
|
authors = [{ name = "UI Protect Maintainers", email = "ui@koston.org" }]
|
|
@@ -60,7 +60,7 @@ pytest-benchmark = ">=4,<6"
|
|
|
60
60
|
pytest-sugar = "^1.1.1"
|
|
61
61
|
pytest-timeout = "^2.4.0"
|
|
62
62
|
pytest-xdist = "^3.7.0"
|
|
63
|
-
types-aiofiles = ">=23.2.0.20240403,<
|
|
63
|
+
types-aiofiles = ">=23.2.0.20240403,<26.0.0.0"
|
|
64
64
|
types-dateparser = "^1.2.2.20250809"
|
|
65
65
|
mypy = "^1.18.2"
|
|
66
66
|
|
|
@@ -54,6 +54,7 @@ from .data import (
|
|
|
54
54
|
SmartDetectTrack,
|
|
55
55
|
Version,
|
|
56
56
|
Viewer,
|
|
57
|
+
WSAction,
|
|
57
58
|
WSPacket,
|
|
58
59
|
WSSubscriptionMessage,
|
|
59
60
|
create_from_unifi_dict,
|
|
@@ -217,10 +218,14 @@ class BaseApiClient:
|
|
|
217
218
|
|
|
218
219
|
headers: dict[str, str] | None = None
|
|
219
220
|
_private_websocket: Websocket | None = None
|
|
221
|
+
_events_websocket: Websocket | None = None
|
|
222
|
+
_devices_websocket: Websocket | None = None
|
|
220
223
|
|
|
221
224
|
private_api_path: str = "/proxy/protect/api/"
|
|
222
225
|
public_api_path: str = "/proxy/protect/integration"
|
|
223
226
|
private_ws_path: str = "/proxy/protect/ws/updates"
|
|
227
|
+
events_ws_path: str = "/proxy/protect/integration/v1/subscribe/events"
|
|
228
|
+
devices_ws_path: str = "/proxy/protect/integration/v1/subscribe/devices"
|
|
224
229
|
|
|
225
230
|
cache_dir: Path
|
|
226
231
|
config_dir: Path
|
|
@@ -276,9 +281,17 @@ class BaseApiClient:
|
|
|
276
281
|
if self._port != 443:
|
|
277
282
|
self._url = URL(f"https://{self._host}:{self._port}")
|
|
278
283
|
self._ws_url = URL(f"wss://{self._host}:{self._port}{self.private_ws_path}")
|
|
284
|
+
self._events_ws_url = URL(
|
|
285
|
+
f"https://{self._host}:{self._port}{self.events_ws_path}"
|
|
286
|
+
)
|
|
287
|
+
self._devices_ws_url = URL(
|
|
288
|
+
f"https://{self._host}:{self._port}{self.devices_ws_path}"
|
|
289
|
+
)
|
|
279
290
|
else:
|
|
280
291
|
self._url = URL(f"https://{self._host}")
|
|
281
292
|
self._ws_url = URL(f"wss://{self._host}{self.private_ws_path}")
|
|
293
|
+
self._events_ws_url = URL(f"https://{self._host}{self.events_ws_path}")
|
|
294
|
+
self._devices_ws_url = URL(f"https://{self._host}{self.devices_ws_path}")
|
|
282
295
|
|
|
283
296
|
self.base_url = str(self._url)
|
|
284
297
|
|
|
@@ -294,6 +307,16 @@ class BaseApiClient:
|
|
|
294
307
|
"""Get Websocket URL."""
|
|
295
308
|
return str(self._ws_url_object)
|
|
296
309
|
|
|
310
|
+
@property
|
|
311
|
+
def events_ws_url(self) -> str:
|
|
312
|
+
"""Get Events Websocket URL."""
|
|
313
|
+
return str(self._events_ws_url)
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def devices_ws_url(self) -> str:
|
|
317
|
+
"""Get Devices Websocket URL."""
|
|
318
|
+
return str(self._devices_ws_url)
|
|
319
|
+
|
|
297
320
|
@property
|
|
298
321
|
def config_file(self) -> Path:
|
|
299
322
|
return self.config_dir / "unifi_protect.json"
|
|
@@ -329,6 +352,15 @@ class BaseApiClient:
|
|
|
329
352
|
await self.ensure_authenticated()
|
|
330
353
|
return self.headers
|
|
331
354
|
|
|
355
|
+
async def _auth_public_api_websocket(
|
|
356
|
+
self, force: bool = False
|
|
357
|
+
) -> dict[str, str] | None:
|
|
358
|
+
"""Authenticate for Public API Websocket."""
|
|
359
|
+
if self._api_key is None:
|
|
360
|
+
raise NotAuthorized("API key is required for public API WebSocket")
|
|
361
|
+
|
|
362
|
+
return {"X-API-KEY": self._api_key}
|
|
363
|
+
|
|
332
364
|
def _get_websocket(self) -> Websocket:
|
|
333
365
|
"""Gets or creates current Websocket."""
|
|
334
366
|
if self._private_websocket is None:
|
|
@@ -345,6 +377,38 @@ class BaseApiClient:
|
|
|
345
377
|
)
|
|
346
378
|
return self._private_websocket
|
|
347
379
|
|
|
380
|
+
def _get_events_websocket(self) -> Websocket:
|
|
381
|
+
"""Gets or creates current Events Websocket."""
|
|
382
|
+
if self._events_websocket is None:
|
|
383
|
+
self._events_websocket = Websocket(
|
|
384
|
+
lambda: self._events_ws_url,
|
|
385
|
+
self._auth_public_api_websocket,
|
|
386
|
+
lambda: None,
|
|
387
|
+
self.get_public_api_session,
|
|
388
|
+
self._process_events_ws_message,
|
|
389
|
+
self._on_events_websocket_state_change,
|
|
390
|
+
verify=self._verify_ssl,
|
|
391
|
+
timeout=self._ws_timeout,
|
|
392
|
+
receive_timeout=self._ws_receive_timeout,
|
|
393
|
+
)
|
|
394
|
+
return self._events_websocket
|
|
395
|
+
|
|
396
|
+
def _get_devices_websocket(self) -> Websocket:
|
|
397
|
+
"""Gets or creates current Devices Websocket."""
|
|
398
|
+
if self._devices_websocket is None:
|
|
399
|
+
self._devices_websocket = Websocket(
|
|
400
|
+
lambda: self._devices_ws_url,
|
|
401
|
+
self._auth_public_api_websocket,
|
|
402
|
+
lambda: None,
|
|
403
|
+
self.get_public_api_session,
|
|
404
|
+
self._process_devices_ws_message,
|
|
405
|
+
self._on_devices_websocket_state_change,
|
|
406
|
+
verify=self._verify_ssl,
|
|
407
|
+
timeout=self._ws_timeout,
|
|
408
|
+
receive_timeout=self._ws_receive_timeout,
|
|
409
|
+
)
|
|
410
|
+
return self._devices_websocket
|
|
411
|
+
|
|
348
412
|
def _update_bootstrap_soon(self) -> None:
|
|
349
413
|
"""Update bootstrap soon."""
|
|
350
414
|
_LOGGER.debug("Updating bootstrap soon")
|
|
@@ -781,10 +845,28 @@ class BaseApiClient:
|
|
|
781
845
|
websocket.stop()
|
|
782
846
|
await websocket.wait_closed()
|
|
783
847
|
self._private_websocket = None
|
|
848
|
+
if self._events_websocket:
|
|
849
|
+
events_websocket = self._get_events_websocket()
|
|
850
|
+
events_websocket.stop()
|
|
851
|
+
await events_websocket.wait_closed()
|
|
852
|
+
self._events_websocket = None
|
|
853
|
+
if self._devices_websocket:
|
|
854
|
+
devices_websocket = self._get_devices_websocket()
|
|
855
|
+
devices_websocket.stop()
|
|
856
|
+
await devices_websocket.wait_closed()
|
|
857
|
+
self._devices_websocket = None
|
|
784
858
|
|
|
785
859
|
def _process_ws_message(self, msg: aiohttp.WSMessage) -> None:
|
|
786
860
|
raise NotImplementedError
|
|
787
861
|
|
|
862
|
+
def _process_events_ws_message(self, msg: aiohttp.WSMessage) -> None:
|
|
863
|
+
"""Process events websocket message - to be implemented by subclass."""
|
|
864
|
+
raise NotImplementedError
|
|
865
|
+
|
|
866
|
+
def _process_devices_ws_message(self, msg: aiohttp.WSMessage) -> None:
|
|
867
|
+
"""Process devices websocket message - to be implemented by subclass."""
|
|
868
|
+
raise NotImplementedError
|
|
869
|
+
|
|
788
870
|
def _get_last_update_id(self) -> str | None:
|
|
789
871
|
raise NotImplementedError
|
|
790
872
|
|
|
@@ -795,6 +877,14 @@ class BaseApiClient:
|
|
|
795
877
|
"""Websocket state changed."""
|
|
796
878
|
_LOGGER.debug("Websocket state changed: %s", state)
|
|
797
879
|
|
|
880
|
+
def _on_events_websocket_state_change(self, state: WebsocketState) -> None:
|
|
881
|
+
"""Events websocket state changed."""
|
|
882
|
+
_LOGGER.debug("Events websocket state changed: %s", state)
|
|
883
|
+
|
|
884
|
+
def _on_devices_websocket_state_change(self, state: WebsocketState) -> None:
|
|
885
|
+
"""Devices websocket state changed."""
|
|
886
|
+
_LOGGER.debug("Devices websocket state changed: %s", state)
|
|
887
|
+
|
|
798
888
|
|
|
799
889
|
class ProtectApiClient(BaseApiClient):
|
|
800
890
|
"""
|
|
@@ -832,7 +922,11 @@ class ProtectApiClient(BaseApiClient):
|
|
|
832
922
|
_subscribed_models: set[ModelType]
|
|
833
923
|
_ignore_stats: bool
|
|
834
924
|
_ws_subscriptions: list[Callable[[WSSubscriptionMessage], None]]
|
|
925
|
+
_events_ws_subscriptions: list[Callable[[WSSubscriptionMessage], None]]
|
|
926
|
+
_devices_ws_subscriptions: list[Callable[[WSSubscriptionMessage], None]]
|
|
835
927
|
_ws_state_subscriptions: list[Callable[[WebsocketState], None]]
|
|
928
|
+
_events_ws_state_subscriptions: list[Callable[[WebsocketState], None]]
|
|
929
|
+
_devices_ws_state_subscriptions: list[Callable[[WebsocketState], None]]
|
|
836
930
|
_bootstrap: Bootstrap | None = None
|
|
837
931
|
_last_update_dt: datetime | None = None
|
|
838
932
|
_connection_host: IPv4Address | IPv6Address | str | None = None
|
|
@@ -881,7 +975,11 @@ class ProtectApiClient(BaseApiClient):
|
|
|
881
975
|
self._subscribed_models = subscribed_models or set()
|
|
882
976
|
self._ignore_stats = ignore_stats
|
|
883
977
|
self._ws_subscriptions = []
|
|
978
|
+
self._events_ws_subscriptions = []
|
|
979
|
+
self._devices_ws_subscriptions = []
|
|
884
980
|
self._ws_state_subscriptions = []
|
|
981
|
+
self._events_ws_state_subscriptions = []
|
|
982
|
+
self._devices_ws_state_subscriptions = []
|
|
885
983
|
self.ignore_unadopted = ignore_unadopted
|
|
886
984
|
self._update_lock = asyncio.Lock()
|
|
887
985
|
|
|
@@ -989,6 +1087,62 @@ class ProtectApiClient(BaseApiClient):
|
|
|
989
1087
|
except Exception:
|
|
990
1088
|
_LOGGER.exception("Exception while running subscription handler")
|
|
991
1089
|
|
|
1090
|
+
def emit_events_message(self, msg: WSSubscriptionMessage) -> None:
|
|
1091
|
+
"""Emit message to all events subscriptions."""
|
|
1092
|
+
if _LOGGER.isEnabledFor(logging.DEBUG):
|
|
1093
|
+
if msg.new_obj is not None:
|
|
1094
|
+
_LOGGER.debug(
|
|
1095
|
+
"emitting events message: %s:%s:%s:%s",
|
|
1096
|
+
msg.action,
|
|
1097
|
+
msg.new_obj.model,
|
|
1098
|
+
msg.new_obj.id,
|
|
1099
|
+
list(msg.changed_data),
|
|
1100
|
+
)
|
|
1101
|
+
elif msg.old_obj is not None:
|
|
1102
|
+
_LOGGER.debug(
|
|
1103
|
+
"emitting events message: %s:%s:%s",
|
|
1104
|
+
msg.action,
|
|
1105
|
+
msg.old_obj.model,
|
|
1106
|
+
msg.old_obj.id,
|
|
1107
|
+
)
|
|
1108
|
+
else:
|
|
1109
|
+
_LOGGER.debug("emitting events message: %s", msg.action)
|
|
1110
|
+
|
|
1111
|
+
for sub in self._events_ws_subscriptions:
|
|
1112
|
+
try:
|
|
1113
|
+
sub(msg)
|
|
1114
|
+
except Exception:
|
|
1115
|
+
_LOGGER.exception("Exception while running events subscription handler")
|
|
1116
|
+
|
|
1117
|
+
def emit_devices_message(self, msg: WSSubscriptionMessage) -> None:
|
|
1118
|
+
"""Emit message to all devices subscriptions."""
|
|
1119
|
+
if _LOGGER.isEnabledFor(logging.DEBUG):
|
|
1120
|
+
if msg.new_obj is not None:
|
|
1121
|
+
_LOGGER.debug(
|
|
1122
|
+
"emitting devices message: %s:%s:%s:%s",
|
|
1123
|
+
msg.action,
|
|
1124
|
+
msg.new_obj.model,
|
|
1125
|
+
msg.new_obj.id,
|
|
1126
|
+
list(msg.changed_data),
|
|
1127
|
+
)
|
|
1128
|
+
elif msg.old_obj is not None:
|
|
1129
|
+
_LOGGER.debug(
|
|
1130
|
+
"emitting devices message: %s:%s:%s",
|
|
1131
|
+
msg.action,
|
|
1132
|
+
msg.old_obj.model,
|
|
1133
|
+
msg.old_obj.id,
|
|
1134
|
+
)
|
|
1135
|
+
else:
|
|
1136
|
+
_LOGGER.debug("emitting devices message: %s", msg.action)
|
|
1137
|
+
|
|
1138
|
+
for sub in self._devices_ws_subscriptions:
|
|
1139
|
+
try:
|
|
1140
|
+
sub(msg)
|
|
1141
|
+
except Exception:
|
|
1142
|
+
_LOGGER.exception(
|
|
1143
|
+
"Exception while running devices subscription handler"
|
|
1144
|
+
)
|
|
1145
|
+
|
|
992
1146
|
def _get_last_update_id(self) -> str | None:
|
|
993
1147
|
if self._bootstrap is None:
|
|
994
1148
|
return None
|
|
@@ -1006,6 +1160,110 @@ class ProtectApiClient(BaseApiClient):
|
|
|
1006
1160
|
|
|
1007
1161
|
self.emit_message(processed_message)
|
|
1008
1162
|
|
|
1163
|
+
def _process_events_ws_message(self, msg: aiohttp.WSMessage) -> None:
|
|
1164
|
+
"""Process events websocket message (Public API - JSON format)."""
|
|
1165
|
+
if msg.type != aiohttp.WSMsgType.TEXT:
|
|
1166
|
+
_LOGGER.debug("Ignoring non-text websocket message: %s", msg.type)
|
|
1167
|
+
return
|
|
1168
|
+
|
|
1169
|
+
try:
|
|
1170
|
+
data = orjson.loads(msg.data)
|
|
1171
|
+
action_type = data.get("type") # "update", "add", "remove"
|
|
1172
|
+
item = data.get("item", {})
|
|
1173
|
+
model_key = item.get("modelKey")
|
|
1174
|
+
|
|
1175
|
+
if not action_type or not model_key:
|
|
1176
|
+
_LOGGER.debug("Invalid public API websocket message: %s", data)
|
|
1177
|
+
return
|
|
1178
|
+
|
|
1179
|
+
# Create a WSSubscriptionMessage similar to private WS
|
|
1180
|
+
model_type = ModelType.from_string(model_key)
|
|
1181
|
+
|
|
1182
|
+
if model_type is ModelType.UNKNOWN:
|
|
1183
|
+
_LOGGER.debug("Unknown model type in public API message: %s", model_key)
|
|
1184
|
+
return
|
|
1185
|
+
|
|
1186
|
+
# Create proper objects from the data
|
|
1187
|
+
new_obj: ProtectModelWithId | None = None
|
|
1188
|
+
old_obj: ProtectModelWithId | None = None
|
|
1189
|
+
update_id = item.get("id", "")
|
|
1190
|
+
|
|
1191
|
+
if action_type in ("add", "update"):
|
|
1192
|
+
try:
|
|
1193
|
+
new_obj = cast(
|
|
1194
|
+
ProtectModelWithId, create_from_unifi_dict(item, api=self)
|
|
1195
|
+
)
|
|
1196
|
+
except Exception:
|
|
1197
|
+
_LOGGER.debug(
|
|
1198
|
+
"Could not create object from public API data: %s", item
|
|
1199
|
+
)
|
|
1200
|
+
|
|
1201
|
+
msg_obj = WSSubscriptionMessage(
|
|
1202
|
+
action=WSAction(action_type),
|
|
1203
|
+
new_update_id=update_id,
|
|
1204
|
+
changed_data=item,
|
|
1205
|
+
new_obj=new_obj,
|
|
1206
|
+
old_obj=old_obj,
|
|
1207
|
+
)
|
|
1208
|
+
|
|
1209
|
+
self.emit_events_message(msg_obj)
|
|
1210
|
+
except Exception as e:
|
|
1211
|
+
_LOGGER.exception(
|
|
1212
|
+
"Error processing public API events websocket message: %s", e
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
def _process_devices_ws_message(self, msg: aiohttp.WSMessage) -> None:
|
|
1216
|
+
"""Process devices websocket message (Public API - JSON format)."""
|
|
1217
|
+
if msg.type != aiohttp.WSMsgType.TEXT:
|
|
1218
|
+
_LOGGER.debug("Ignoring non-text websocket message: %s", msg.type)
|
|
1219
|
+
return
|
|
1220
|
+
|
|
1221
|
+
try:
|
|
1222
|
+
data = orjson.loads(msg.data)
|
|
1223
|
+
action_type = data.get("type") # "update", "add", "remove"
|
|
1224
|
+
item = data.get("item", {})
|
|
1225
|
+
model_key = item.get("modelKey")
|
|
1226
|
+
|
|
1227
|
+
if not action_type or not model_key:
|
|
1228
|
+
_LOGGER.debug("Invalid public API websocket message: %s", data)
|
|
1229
|
+
return
|
|
1230
|
+
|
|
1231
|
+
# Create a WSSubscriptionMessage similar to private WS
|
|
1232
|
+
model_type = ModelType.from_string(model_key)
|
|
1233
|
+
|
|
1234
|
+
if model_type is ModelType.UNKNOWN:
|
|
1235
|
+
_LOGGER.debug("Unknown model type in public API message: %s", model_key)
|
|
1236
|
+
return
|
|
1237
|
+
|
|
1238
|
+
# Create proper objects from the data
|
|
1239
|
+
new_obj: ProtectModelWithId | None = None
|
|
1240
|
+
old_obj: ProtectModelWithId | None = None
|
|
1241
|
+
update_id = item.get("id", "")
|
|
1242
|
+
|
|
1243
|
+
if action_type in ("add", "update"):
|
|
1244
|
+
try:
|
|
1245
|
+
new_obj = cast(
|
|
1246
|
+
ProtectModelWithId, create_from_unifi_dict(item, api=self)
|
|
1247
|
+
)
|
|
1248
|
+
except Exception:
|
|
1249
|
+
_LOGGER.debug(
|
|
1250
|
+
"Could not create object from public API data: %s", item
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
msg_obj = WSSubscriptionMessage(
|
|
1254
|
+
action=WSAction(action_type),
|
|
1255
|
+
new_update_id=update_id,
|
|
1256
|
+
changed_data=item,
|
|
1257
|
+
new_obj=new_obj,
|
|
1258
|
+
old_obj=old_obj,
|
|
1259
|
+
)
|
|
1260
|
+
|
|
1261
|
+
self.emit_devices_message(msg_obj)
|
|
1262
|
+
except Exception as e:
|
|
1263
|
+
_LOGGER.exception(
|
|
1264
|
+
"Error processing public API devices websocket message: %s", e
|
|
1265
|
+
)
|
|
1266
|
+
|
|
1009
1267
|
async def _get_event_paginate(
|
|
1010
1268
|
self,
|
|
1011
1269
|
params: dict[str, Any],
|
|
@@ -1251,6 +1509,34 @@ class ProtectApiClient(BaseApiClient):
|
|
|
1251
1509
|
self._get_websocket().start()
|
|
1252
1510
|
return partial(self._unsubscribe_websocket, ws_callback)
|
|
1253
1511
|
|
|
1512
|
+
def subscribe_events_websocket(
|
|
1513
|
+
self,
|
|
1514
|
+
ws_callback: Callable[[WSSubscriptionMessage], None],
|
|
1515
|
+
) -> Callable[[], None]:
|
|
1516
|
+
"""
|
|
1517
|
+
Subscribe to events websocket events.
|
|
1518
|
+
|
|
1519
|
+
Returns a callback that will unsubscribe.
|
|
1520
|
+
"""
|
|
1521
|
+
_LOGGER.debug("Adding events subscription: %s", ws_callback)
|
|
1522
|
+
self._events_ws_subscriptions.append(ws_callback)
|
|
1523
|
+
self._get_events_websocket().start()
|
|
1524
|
+
return partial(self._unsubscribe_events_websocket, ws_callback)
|
|
1525
|
+
|
|
1526
|
+
def subscribe_devices_websocket(
|
|
1527
|
+
self,
|
|
1528
|
+
ws_callback: Callable[[WSSubscriptionMessage], None],
|
|
1529
|
+
) -> Callable[[], None]:
|
|
1530
|
+
"""
|
|
1531
|
+
Subscribe to devices websocket events.
|
|
1532
|
+
|
|
1533
|
+
Returns a callback that will unsubscribe.
|
|
1534
|
+
"""
|
|
1535
|
+
_LOGGER.debug("Adding devices subscription: %s", ws_callback)
|
|
1536
|
+
self._devices_ws_subscriptions.append(ws_callback)
|
|
1537
|
+
self._get_devices_websocket().start()
|
|
1538
|
+
return partial(self._unsubscribe_devices_websocket, ws_callback)
|
|
1539
|
+
|
|
1254
1540
|
def _unsubscribe_websocket(
|
|
1255
1541
|
self,
|
|
1256
1542
|
ws_callback: Callable[[WSSubscriptionMessage], None],
|
|
@@ -1261,6 +1547,26 @@ class ProtectApiClient(BaseApiClient):
|
|
|
1261
1547
|
if not self._ws_subscriptions:
|
|
1262
1548
|
self._get_websocket().stop()
|
|
1263
1549
|
|
|
1550
|
+
def _unsubscribe_events_websocket(
|
|
1551
|
+
self,
|
|
1552
|
+
ws_callback: Callable[[WSSubscriptionMessage], None],
|
|
1553
|
+
) -> None:
|
|
1554
|
+
"""Unsubscribe to events websocket events."""
|
|
1555
|
+
_LOGGER.debug("Removing events subscription: %s", ws_callback)
|
|
1556
|
+
self._events_ws_subscriptions.remove(ws_callback)
|
|
1557
|
+
if not self._events_ws_subscriptions:
|
|
1558
|
+
self._get_events_websocket().stop()
|
|
1559
|
+
|
|
1560
|
+
def _unsubscribe_devices_websocket(
|
|
1561
|
+
self,
|
|
1562
|
+
ws_callback: Callable[[WSSubscriptionMessage], None],
|
|
1563
|
+
) -> None:
|
|
1564
|
+
"""Unsubscribe to devices websocket events."""
|
|
1565
|
+
_LOGGER.debug("Removing devices subscription: %s", ws_callback)
|
|
1566
|
+
self._devices_ws_subscriptions.remove(ws_callback)
|
|
1567
|
+
if not self._devices_ws_subscriptions:
|
|
1568
|
+
self._get_devices_websocket().stop()
|
|
1569
|
+
|
|
1264
1570
|
def subscribe_websocket_state(
|
|
1265
1571
|
self,
|
|
1266
1572
|
ws_callback: Callable[[WebsocketState], None],
|
|
@@ -1273,6 +1579,30 @@ class ProtectApiClient(BaseApiClient):
|
|
|
1273
1579
|
self._ws_state_subscriptions.append(ws_callback)
|
|
1274
1580
|
return partial(self._unsubscribe_websocket_state, ws_callback)
|
|
1275
1581
|
|
|
1582
|
+
def subscribe_events_websocket_state(
|
|
1583
|
+
self,
|
|
1584
|
+
ws_callback: Callable[[WebsocketState], None],
|
|
1585
|
+
) -> Callable[[], None]:
|
|
1586
|
+
"""
|
|
1587
|
+
Subscribe to events websocket state changes.
|
|
1588
|
+
|
|
1589
|
+
Returns a callback that will unsubscribe.
|
|
1590
|
+
"""
|
|
1591
|
+
self._events_ws_state_subscriptions.append(ws_callback)
|
|
1592
|
+
return partial(self._unsubscribe_events_websocket_state, ws_callback)
|
|
1593
|
+
|
|
1594
|
+
def subscribe_devices_websocket_state(
|
|
1595
|
+
self,
|
|
1596
|
+
ws_callback: Callable[[WebsocketState], None],
|
|
1597
|
+
) -> Callable[[], None]:
|
|
1598
|
+
"""
|
|
1599
|
+
Subscribe to devices websocket state changes.
|
|
1600
|
+
|
|
1601
|
+
Returns a callback that will unsubscribe.
|
|
1602
|
+
"""
|
|
1603
|
+
self._devices_ws_state_subscriptions.append(ws_callback)
|
|
1604
|
+
return partial(self._unsubscribe_devices_websocket_state, ws_callback)
|
|
1605
|
+
|
|
1276
1606
|
def _unsubscribe_websocket_state(
|
|
1277
1607
|
self,
|
|
1278
1608
|
ws_callback: Callable[[WebsocketState], None],
|
|
@@ -1280,6 +1610,20 @@ class ProtectApiClient(BaseApiClient):
|
|
|
1280
1610
|
"""Unsubscribe to websocket state changes."""
|
|
1281
1611
|
self._ws_state_subscriptions.remove(ws_callback)
|
|
1282
1612
|
|
|
1613
|
+
def _unsubscribe_events_websocket_state(
|
|
1614
|
+
self,
|
|
1615
|
+
ws_callback: Callable[[WebsocketState], None],
|
|
1616
|
+
) -> None:
|
|
1617
|
+
"""Unsubscribe to events websocket state changes."""
|
|
1618
|
+
self._events_ws_state_subscriptions.remove(ws_callback)
|
|
1619
|
+
|
|
1620
|
+
def _unsubscribe_devices_websocket_state(
|
|
1621
|
+
self,
|
|
1622
|
+
ws_callback: Callable[[WebsocketState], None],
|
|
1623
|
+
) -> None:
|
|
1624
|
+
"""Unsubscribe to devices websocket state changes."""
|
|
1625
|
+
self._devices_ws_state_subscriptions.remove(ws_callback)
|
|
1626
|
+
|
|
1283
1627
|
def _on_websocket_state_change(self, state: WebsocketState) -> None:
|
|
1284
1628
|
"""Websocket state changed."""
|
|
1285
1629
|
super()._on_websocket_state_change(state)
|
|
@@ -1289,6 +1633,26 @@ class ProtectApiClient(BaseApiClient):
|
|
|
1289
1633
|
except Exception:
|
|
1290
1634
|
_LOGGER.exception("Exception while running websocket state handler")
|
|
1291
1635
|
|
|
1636
|
+
def _on_events_websocket_state_change(self, state: WebsocketState) -> None:
|
|
1637
|
+
"""Events Websocket state changed."""
|
|
1638
|
+
for sub in self._events_ws_state_subscriptions:
|
|
1639
|
+
try:
|
|
1640
|
+
sub(state)
|
|
1641
|
+
except Exception:
|
|
1642
|
+
_LOGGER.exception(
|
|
1643
|
+
"Exception while running events websocket state handler"
|
|
1644
|
+
)
|
|
1645
|
+
|
|
1646
|
+
def _on_devices_websocket_state_change(self, state: WebsocketState) -> None:
|
|
1647
|
+
"""Devices Websocket state changed."""
|
|
1648
|
+
for sub in self._devices_ws_state_subscriptions:
|
|
1649
|
+
try:
|
|
1650
|
+
sub(state)
|
|
1651
|
+
except Exception:
|
|
1652
|
+
_LOGGER.exception(
|
|
1653
|
+
"Exception while running devices websocket state handler"
|
|
1654
|
+
)
|
|
1655
|
+
|
|
1292
1656
|
async def get_bootstrap(self) -> Bootstrap:
|
|
1293
1657
|
"""
|
|
1294
1658
|
Gets bootstrap object from UFP instance
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|