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.

Files changed (38) hide show
  1. {uiprotect-7.22.0 → uiprotect-7.23.0}/PKG-INFO +1 -1
  2. {uiprotect-7.22.0 → uiprotect-7.23.0}/pyproject.toml +2 -2
  3. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/api.py +364 -0
  4. {uiprotect-7.22.0 → uiprotect-7.23.0}/LICENSE +0 -0
  5. {uiprotect-7.22.0 → uiprotect-7.23.0}/README.md +0 -0
  6. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/__init__.py +0 -0
  7. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/__main__.py +0 -0
  8. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/_compat.py +0 -0
  9. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/__init__.py +0 -0
  10. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/aiports.py +0 -0
  11. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/backup.py +0 -0
  12. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/base.py +0 -0
  13. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/cameras.py +0 -0
  14. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/chimes.py +0 -0
  15. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/doorlocks.py +0 -0
  16. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/events.py +0 -0
  17. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/lights.py +0 -0
  18. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/liveviews.py +0 -0
  19. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/nvr.py +0 -0
  20. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/sensors.py +0 -0
  21. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/cli/viewers.py +0 -0
  22. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/data/__init__.py +0 -0
  23. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/data/base.py +0 -0
  24. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/data/bootstrap.py +0 -0
  25. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/data/convert.py +0 -0
  26. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/data/devices.py +0 -0
  27. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/data/nvr.py +0 -0
  28. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/data/types.py +0 -0
  29. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/data/user.py +0 -0
  30. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/data/websocket.py +0 -0
  31. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/exceptions.py +0 -0
  32. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/py.typed +0 -0
  33. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/release_cache.json +0 -0
  34. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/stream.py +0 -0
  35. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/test_util/__init__.py +0 -0
  36. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/test_util/anonymize.py +0 -0
  37. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/utils.py +0 -0
  38. {uiprotect-7.22.0 → uiprotect-7.23.0}/src/uiprotect/websocket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uiprotect
3
- Version: 7.22.0
3
+ Version: 7.23.0
4
4
  Summary: Python API for Unifi Protect (Unofficial)
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "uiprotect"
3
- version = "7.22.0"
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,<25.0.0.0"
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