uiprotect 1.12.0__py3-none-any.whl → 1.13.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/api.py +6 -8
- uiprotect/data/base.py +7 -2
- uiprotect/data/bootstrap.py +20 -10
- uiprotect/websocket.py +6 -5
- {uiprotect-1.12.0.dist-info → uiprotect-1.13.0.dist-info}/METADATA +1 -1
- {uiprotect-1.12.0.dist-info → uiprotect-1.13.0.dist-info}/RECORD +9 -9
- {uiprotect-1.12.0.dist-info → uiprotect-1.13.0.dist-info}/LICENSE +0 -0
- {uiprotect-1.12.0.dist-info → uiprotect-1.13.0.dist-info}/WHEEL +0 -0
- {uiprotect-1.12.0.dist-info → uiprotect-1.13.0.dist-info}/entry_points.txt +0 -0
uiprotect/api.py
CHANGED
|
@@ -228,6 +228,7 @@ class BaseApiClient:
|
|
|
228
228
|
|
|
229
229
|
@property
|
|
230
230
|
def ws_url(self) -> str:
|
|
231
|
+
"""Get Websocket URL."""
|
|
231
232
|
url = f"wss://{self._host}"
|
|
232
233
|
if self._port != 443:
|
|
233
234
|
url += f":{self._port}"
|
|
@@ -267,7 +268,7 @@ class BaseApiClient:
|
|
|
267
268
|
|
|
268
269
|
if self._websocket is None:
|
|
269
270
|
self._websocket = Websocket(
|
|
270
|
-
self.
|
|
271
|
+
self.get_websocket_url,
|
|
271
272
|
_auth,
|
|
272
273
|
verify=self._verify_ssl,
|
|
273
274
|
timeout=self._ws_timeout,
|
|
@@ -626,9 +627,6 @@ class BaseApiClient:
|
|
|
626
627
|
self._websocket = None
|
|
627
628
|
|
|
628
629
|
websocket = await self.get_websocket()
|
|
629
|
-
# important to make sure WS URL is always current
|
|
630
|
-
websocket.url = self.ws_url
|
|
631
|
-
|
|
632
630
|
if not websocket.is_connected:
|
|
633
631
|
self._last_ws_status = False
|
|
634
632
|
with contextlib.suppress(
|
|
@@ -638,6 +636,10 @@ class BaseApiClient:
|
|
|
638
636
|
):
|
|
639
637
|
await websocket.connect()
|
|
640
638
|
|
|
639
|
+
def get_websocket_url(self) -> str:
|
|
640
|
+
"""Get Websocket URL."""
|
|
641
|
+
return self.ws_url
|
|
642
|
+
|
|
641
643
|
async def async_disconnect_ws(self) -> None:
|
|
642
644
|
"""Disconnect from Websocket."""
|
|
643
645
|
if self._websocket is None:
|
|
@@ -861,10 +863,6 @@ class ProtectApiClient(BaseApiClient):
|
|
|
861
863
|
models=self._subscribed_models,
|
|
862
864
|
ignore_stats=self._ignore_stats,
|
|
863
865
|
)
|
|
864
|
-
# update websocket URL after every message to ensure the latest last_update_id
|
|
865
|
-
if self._websocket is not None:
|
|
866
|
-
self._websocket.url = self.ws_url
|
|
867
|
-
|
|
868
866
|
if processed_message is None:
|
|
869
867
|
return
|
|
870
868
|
|
uiprotect/data/base.py
CHANGED
|
@@ -61,6 +61,9 @@ RECENT_EVENT_MAX = timedelta(seconds=30)
|
|
|
61
61
|
EVENT_PING_INTERVAL = timedelta(seconds=3)
|
|
62
62
|
EVENT_PING_INTERVAL_SECONDS = EVENT_PING_INTERVAL.total_seconds()
|
|
63
63
|
|
|
64
|
+
_EMPTY_EVENT_PING_BACK: dict[Any, Any] = {}
|
|
65
|
+
|
|
66
|
+
|
|
64
67
|
_LOGGER = logging.getLogger(__name__)
|
|
65
68
|
|
|
66
69
|
|
|
@@ -788,7 +791,7 @@ class ProtectModelWithId(ProtectModel):
|
|
|
788
791
|
|
|
789
792
|
def _emit_message(self, updated: dict[str, Any]) -> None:
|
|
790
793
|
"""Emits fake WS message for ProtectApiClient to process."""
|
|
791
|
-
if updated
|
|
794
|
+
if _is_ping_back := updated is _EMPTY_EVENT_PING_BACK:
|
|
792
795
|
_LOGGER.debug("Event ping callback started for %s", self.id)
|
|
793
796
|
|
|
794
797
|
if self.model is None:
|
|
@@ -817,7 +820,9 @@ class ProtectModelWithId(ProtectModel):
|
|
|
817
820
|
|
|
818
821
|
message = self._api.bootstrap.process_ws_packet(
|
|
819
822
|
WSPacket(action_frame.packed + data_frame.packed),
|
|
823
|
+
is_ping_back=_is_ping_back,
|
|
820
824
|
)
|
|
825
|
+
|
|
821
826
|
if message is not None:
|
|
822
827
|
self._api.emit_message(message)
|
|
823
828
|
|
|
@@ -876,7 +881,7 @@ class ProtectDeviceModel(ProtectModelWithId):
|
|
|
876
881
|
self._callback_ping = loop.call_later(
|
|
877
882
|
EVENT_PING_INTERVAL_SECONDS,
|
|
878
883
|
self._emit_message,
|
|
879
|
-
|
|
884
|
+
_EMPTY_EVENT_PING_BACK,
|
|
880
885
|
)
|
|
881
886
|
|
|
882
887
|
async def set_name(self, name: str | None) -> None:
|
uiprotect/data/bootstrap.py
CHANGED
|
@@ -451,7 +451,15 @@ class Bootstrap(ProtectBaseObject):
|
|
|
451
451
|
action: dict[str, Any],
|
|
452
452
|
data: dict[str, Any],
|
|
453
453
|
ignore_stats: bool,
|
|
454
|
+
is_ping_back: bool,
|
|
454
455
|
) -> WSSubscriptionMessage | None:
|
|
456
|
+
"""
|
|
457
|
+
Process a device update packet.
|
|
458
|
+
|
|
459
|
+
If is_ping_back is True, the packet is an empty packet
|
|
460
|
+
that was generated internally as a result of an event
|
|
461
|
+
that will expire and result in a state change.
|
|
462
|
+
"""
|
|
455
463
|
remove_keys = (
|
|
456
464
|
STATS_AND_IGNORE_DEVICE_KEYS if ignore_stats else IGNORE_DEVICE_KEYS
|
|
457
465
|
)
|
|
@@ -462,7 +470,7 @@ class Bootstrap(ProtectBaseObject):
|
|
|
462
470
|
if model_type is ModelType.CAMERA and "lastMotion" in data:
|
|
463
471
|
del data["lastMotion"]
|
|
464
472
|
# nothing left to process
|
|
465
|
-
if not data:
|
|
473
|
+
if not data and not is_ping_back:
|
|
466
474
|
self._create_stat(packet, None, True)
|
|
467
475
|
return None
|
|
468
476
|
|
|
@@ -476,6 +484,12 @@ class Bootstrap(ProtectBaseObject):
|
|
|
476
484
|
|
|
477
485
|
obj = devices[action_id]
|
|
478
486
|
data = obj.unifi_dict_to_dict(data)
|
|
487
|
+
|
|
488
|
+
if not data and not is_ping_back:
|
|
489
|
+
# nothing left to process
|
|
490
|
+
self._create_stat(packet, None, True)
|
|
491
|
+
return None
|
|
492
|
+
|
|
479
493
|
old_obj = obj.copy()
|
|
480
494
|
obj = obj.update_from_dict(deepcopy(data))
|
|
481
495
|
|
|
@@ -513,6 +527,7 @@ class Bootstrap(ProtectBaseObject):
|
|
|
513
527
|
packet: WSPacket,
|
|
514
528
|
models: set[ModelType] | None = None,
|
|
515
529
|
ignore_stats: bool = False,
|
|
530
|
+
is_ping_back: bool = False,
|
|
516
531
|
) -> WSSubscriptionMessage | None:
|
|
517
532
|
"""Process a WS packet."""
|
|
518
533
|
action = packet.action_frame.data
|
|
@@ -521,17 +536,16 @@ class Bootstrap(ProtectBaseObject):
|
|
|
521
536
|
action = deepcopy(action)
|
|
522
537
|
data = deepcopy(data)
|
|
523
538
|
|
|
524
|
-
new_update_id: str = action["newUpdateId"]
|
|
539
|
+
new_update_id: str | None = action["newUpdateId"]
|
|
525
540
|
if new_update_id is not None:
|
|
526
541
|
self.last_update_id = new_update_id
|
|
527
542
|
|
|
528
543
|
model_key: str = action["modelKey"]
|
|
529
|
-
if
|
|
544
|
+
if (model_type := ModelType.from_string(model_key)) is ModelType.UNKNOWN:
|
|
530
545
|
_LOGGER.debug("Unknown model type: %s", model_key)
|
|
531
546
|
self._create_stat(packet, None, True)
|
|
532
547
|
return None
|
|
533
548
|
|
|
534
|
-
model_type = ModelType.from_string(model_key)
|
|
535
549
|
if models and model_type not in models:
|
|
536
550
|
self._create_stat(packet, None, True)
|
|
537
551
|
return None
|
|
@@ -540,7 +554,7 @@ class Bootstrap(ProtectBaseObject):
|
|
|
540
554
|
if action_action == "remove":
|
|
541
555
|
return self._process_remove_packet(model_type, packet)
|
|
542
556
|
|
|
543
|
-
if not data:
|
|
557
|
+
if not data and not is_ping_back:
|
|
544
558
|
self._create_stat(packet, None, True)
|
|
545
559
|
return None
|
|
546
560
|
|
|
@@ -552,11 +566,7 @@ class Bootstrap(ProtectBaseObject):
|
|
|
552
566
|
return self._process_nvr_update(packet, data, ignore_stats)
|
|
553
567
|
if model_type in ModelType.bootstrap_models_types_and_event_set:
|
|
554
568
|
return self._process_device_update(
|
|
555
|
-
model_type,
|
|
556
|
-
packet,
|
|
557
|
-
action,
|
|
558
|
-
data,
|
|
559
|
-
ignore_stats,
|
|
569
|
+
model_type, packet, action, data, ignore_stats, is_ping_back
|
|
560
570
|
)
|
|
561
571
|
except (ValidationError, ValueError) as err:
|
|
562
572
|
self._handle_ws_error(action, err)
|
uiprotect/websocket.py
CHANGED
|
@@ -45,7 +45,7 @@ class Websocket:
|
|
|
45
45
|
|
|
46
46
|
def __init__(
|
|
47
47
|
self,
|
|
48
|
-
|
|
48
|
+
get_url: Callable[[], str],
|
|
49
49
|
auth_callback: CALLBACK_TYPE,
|
|
50
50
|
*,
|
|
51
51
|
timeout: int = 30,
|
|
@@ -53,7 +53,7 @@ class Websocket:
|
|
|
53
53
|
verify: bool = True,
|
|
54
54
|
) -> None:
|
|
55
55
|
"""Init Websocket."""
|
|
56
|
-
self.
|
|
56
|
+
self.get_url = get_url
|
|
57
57
|
self.timeout_interval = timeout
|
|
58
58
|
self.backoff = backoff
|
|
59
59
|
self.verify = verify
|
|
@@ -85,14 +85,15 @@ class Websocket:
|
|
|
85
85
|
return True
|
|
86
86
|
|
|
87
87
|
async def _websocket_loop(self, start_event: asyncio.Event) -> None:
|
|
88
|
-
|
|
88
|
+
url = self.get_url()
|
|
89
|
+
_LOGGER.debug("Connecting WS to %s", url)
|
|
89
90
|
self._headers = await self._auth(self._should_reset_auth)
|
|
90
91
|
|
|
91
92
|
session = self._get_session()
|
|
92
93
|
# catch any and all errors for Websocket so we can clean up correctly
|
|
93
94
|
try:
|
|
94
95
|
self._ws_connection = await session.ws_connect(
|
|
95
|
-
|
|
96
|
+
url,
|
|
96
97
|
ssl=None if self.verify else False,
|
|
97
98
|
headers=self._headers,
|
|
98
99
|
)
|
|
@@ -104,7 +105,7 @@ class Websocket:
|
|
|
104
105
|
break
|
|
105
106
|
self._reset_timeout()
|
|
106
107
|
except ClientError:
|
|
107
|
-
_LOGGER.exception("Websocket disconnect error: %s",
|
|
108
|
+
_LOGGER.exception("Websocket disconnect error: %s", url)
|
|
108
109
|
finally:
|
|
109
110
|
_LOGGER.debug("Websocket disconnected")
|
|
110
111
|
self._increase_failure()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
uiprotect/__init__.py,sha256=llnQNtiBfwQG8IkQXovvFz4LZeFjrJx7XdmmUhu3a9E,289
|
|
2
2
|
uiprotect/__main__.py,sha256=C_bHCOkv5qj6WMy-6ELoY3Y6HDhLxOa1a30CzmbZhsg,462
|
|
3
|
-
uiprotect/api.py,sha256=
|
|
3
|
+
uiprotect/api.py,sha256=zVKTMieBqx3EM1sex5zzAj2J2k4pUlQQ_xF8nE6AS5s,66339
|
|
4
4
|
uiprotect/cli/__init__.py,sha256=sSLW9keVQOkgFcMW18HTDjRrt9sJ0KWjn9DJDA6f9Pc,8658
|
|
5
5
|
uiprotect/cli/backup.py,sha256=ZiS7RZnJGKI8TJKLW2cOUzkRM8nyTvE5Ov_jZZGtvSM,36708
|
|
6
6
|
uiprotect/cli/base.py,sha256=zpTm2kyJe_GLixnv3Uadke__iRLh64AEwQzp-2hqS7g,7730
|
|
@@ -14,8 +14,8 @@ uiprotect/cli/nvr.py,sha256=TwxEg2XT8jXAbOqv6gc7KFXELKadeItEDYweSL4_-e8,4260
|
|
|
14
14
|
uiprotect/cli/sensors.py,sha256=fQtcDJCVxs4VbAqcavgBy2ABiVxAW3GXtna6_XFBp2k,8153
|
|
15
15
|
uiprotect/cli/viewers.py,sha256=2cyrp104ffIvgT0wYGIO0G35QMkEbFe7fSVqLwDXQYQ,2171
|
|
16
16
|
uiprotect/data/__init__.py,sha256=OcfuJl2qXfHcj_mdnrHhzZ5tEIZrw8auziX5IE7dn-I,2938
|
|
17
|
-
uiprotect/data/base.py,sha256=
|
|
18
|
-
uiprotect/data/bootstrap.py,sha256=
|
|
17
|
+
uiprotect/data/base.py,sha256=yzGm5RCeo9AqmgArWC8O7LhBp_Qxz1XSel3xRyhNAbM,37718
|
|
18
|
+
uiprotect/data/bootstrap.py,sha256=wjRFVrv1rOFVDcij6ZYm7dzMe0iz6AqYciQozC-J2TI,21893
|
|
19
19
|
uiprotect/data/convert.py,sha256=8h6Il_DhMkPRDPj9F_rA2UZIlTuchS3BQD24peKpk2A,2185
|
|
20
20
|
uiprotect/data/devices.py,sha256=Nq3bOko5PFf5LvEBoD4JV8kmbq50laRdh3VHMWX7t-0,111809
|
|
21
21
|
uiprotect/data/nvr.py,sha256=XC4NO1c_Mom-hIpzj9ksKFcgKbHd6ToqWjkgzxJ1PJY,47636
|
|
@@ -29,9 +29,9 @@ uiprotect/stream.py,sha256=McV3XymKyjn-1uV5jdQHcpaDjqLS4zWyMASQ8ubcyb4,4924
|
|
|
29
29
|
uiprotect/test_util/__init__.py,sha256=d2g7afa0LSdixQ0kjEDYwafDFME_UlW2LzxpamZ2BC0,18556
|
|
30
30
|
uiprotect/test_util/anonymize.py,sha256=f-8ijU-_y9r-uAbhIPn0f0I6hzJpAkvJzc8UpWihObI,8478
|
|
31
31
|
uiprotect/utils.py,sha256=6OLY8hNiCzk418PjJJIlFW7jjPzVt1vxBKEzBSqMeTk,18418
|
|
32
|
-
uiprotect/websocket.py,sha256=
|
|
33
|
-
uiprotect-1.
|
|
34
|
-
uiprotect-1.
|
|
35
|
-
uiprotect-1.
|
|
36
|
-
uiprotect-1.
|
|
37
|
-
uiprotect-1.
|
|
32
|
+
uiprotect/websocket.py,sha256=JHI_2EZeRPqPyQopsBZS0dr3tu0HaTiqeLazfBXhW_8,7339
|
|
33
|
+
uiprotect-1.13.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
|
|
34
|
+
uiprotect-1.13.0.dist-info/METADATA,sha256=64mSJs-bol2iFi_YUobezZybOEwOovljmPod2f9ELiw,10985
|
|
35
|
+
uiprotect-1.13.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
36
|
+
uiprotect-1.13.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
|
|
37
|
+
uiprotect-1.13.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|