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 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.ws_url,
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:
@@ -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 model_key not in ModelType.values_set():
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
- url: str,
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.url = url
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
- _LOGGER.debug("Connecting WS to %s", self.url)
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
- self.url,
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", self.url)
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
  Metadata-Version: 2.1
2
2
  Name: uiprotect
3
- Version: 1.12.0
3
+ Version: 1.13.0
4
4
  Summary: Python API for Unifi Protect (Unofficial)
5
5
  Home-page: https://github.com/uilibs/uiprotect
6
6
  License: MIT
@@ -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=t3E1wMja0av6ENtvXJHGv70WC6NCCWbF_wFIJxOLS0I,66463
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=apIXKZHL6dbyXTT4C4lkyo4M-Nf_DwsVXoXBL5jcXVo,37574
18
- uiprotect/data/bootstrap.py,sha256=pAb-JC_AatYPldfefiMqdZ3_6Ftngcmd2asAMw66XLE,21486
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=IzDPyqbzrkOMREvahN-e2zdvVD0VABSCWy6jSoCwOT0,7299
33
- uiprotect-1.12.0.dist-info/LICENSE,sha256=INx18jhdbVXMEiiBANeKEbrbz57ckgzxk5uutmmcxGk,1111
34
- uiprotect-1.12.0.dist-info/METADATA,sha256=wPRYZLr1EpqqtUnuenaK3Z-iEdmDqA4YnHCvJG5NQAY,10985
35
- uiprotect-1.12.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
36
- uiprotect-1.12.0.dist-info/entry_points.txt,sha256=J78AUTPrTTxgI3s7SVgrmGqDP7piX2wuuEORzhDdVRA,47
37
- uiprotect-1.12.0.dist-info/RECORD,,
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,,