python-aidot-cameras 0.5.5__tar.gz → 0.5.7__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.
Files changed (29) hide show
  1. {python_aidot_cameras-0.5.5/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.5.7}/PKG-INFO +1 -1
  2. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/pyproject.toml +1 -1
  3. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/src/aidot/device_client.py +60 -19
  4. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7/src/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  5. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/LICENSE +0 -0
  6. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/README.md +0 -0
  7. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/setup.cfg +0 -0
  8. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/src/aidot/__init__.py +0 -0
  9. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/src/aidot/aes_utils.py +0 -0
  10. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/src/aidot/client.py +0 -0
  11. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/src/aidot/const.py +0 -0
  12. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/src/aidot/credentials.py +0 -0
  13. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/src/aidot/discover.py +0 -0
  14. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/src/aidot/exceptions.py +0 -0
  15. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/src/aidot/g711.py +0 -0
  16. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/src/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
  17. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/src/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  18. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/src/python_aidot_cameras.egg-info/requires.txt +0 -0
  19. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/src/python_aidot_cameras.egg-info/top_level.txt +0 -0
  20. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/tests/test_alarm_event.py +0 -0
  21. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/tests/test_highport_nomination.py +0 -0
  22. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/tests/test_motion_poll.py +0 -0
  23. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/tests/test_sdes_talk.py +0 -0
  24. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/tests/test_sdes_watchdog.py +0 -0
  25. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/tests/test_speak.py +0 -0
  26. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/tests/test_stream_cap.py +0 -0
  27. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/tests/test_talk.py +0 -0
  28. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/tests/test_terminal_ack.py +0 -0
  29. {python_aidot_cameras-0.5.5 → python_aidot_cameras-0.5.7}/tests/test_token_refresh.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 0.5.5
3
+ Version: 0.5.7
4
4
  Summary: Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)
5
5
  Author-email: cbrightly <chris.brightly@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-aidot-cameras"
7
- version = "0.5.5"
7
+ version = "0.5.7"
8
8
  description = "Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -4042,7 +4042,11 @@ class DeviceClient(object):
4042
4042
  except asyncio.CancelledError:
4043
4043
  return
4044
4044
 
4045
- async def start_keepalive(self, rtsp_push_url: Optional[str] = None) -> None:
4045
+ async def start_keepalive(
4046
+ self,
4047
+ rtsp_push_url: Optional[str] = None,
4048
+ fast_connect: Optional[bool] = None,
4049
+ ) -> None:
4046
4050
  """Start a persistent stream that keeps the camera session alive.
4047
4051
 
4048
4052
  For SDES cameras (A001064/A001513): opens an indefinite ffmpeg stream and
@@ -4054,8 +4058,15 @@ class DeviceClient(object):
4054
4058
  pulls (real play/pause video, no decode/re-encode). Without a serve URL
4055
4059
  it falls back to the on_frame keepalive loop (latest_jpeg / MJPEG).
4056
4060
 
4061
+ ``fast_connect`` enables LAN-direct mode (skip the ~2.5s livePlay/ICE-config
4062
+ waits + TURN relay; see _async_open_webrtc_stream_impl). ``None`` leaves it
4063
+ to the ``AIDOT_FAST_CONNECT`` env var; ``True``/``False`` override it (e.g.
4064
+ from a Home Assistant config-entry option, since HA OS can't set env vars).
4065
+
4057
4066
  Safe to call multiple times - does nothing if already running.
4058
4067
  """
4068
+ if fast_connect is not None:
4069
+ self._fast_connect_opt = fast_connect
4059
4070
  if self._stream_task is not None and not self._stream_task.done():
4060
4071
  return
4061
4072
  self._keepalive_rtsp_url = rtsp_push_url
@@ -5164,9 +5175,13 @@ class DeviceClient(object):
5164
5175
  # LAN host candidate connects in ~1 s. TRADE-OFF: no relay means cameras
5165
5176
  # on a different network segment / behind strict NAT cannot connect, so
5166
5177
  # this is opt-in and off by default.
5167
- _fast_connect = os.environ.get("AIDOT_FAST_CONNECT", "").strip().lower() in (
5168
- "1", "true", "yes", "on",
5169
- )
5178
+ # Prefer an explicit per-camera setting (e.g. HA config-entry option via
5179
+ # start_keepalive(fast_connect=...)); fall back to the env var otherwise.
5180
+ _fast_connect = getattr(self, "_fast_connect_opt", None)
5181
+ if _fast_connect is None:
5182
+ _fast_connect = os.environ.get("AIDOT_FAST_CONNECT", "").strip().lower() in (
5183
+ "1", "true", "yes", "on",
5184
+ )
5170
5185
 
5171
5186
  # Wake battery cameras via the cloud HTTP low-power endpoint before the
5172
5187
  # handshake (matches the app, which fires the HTTP wake so a sleeping camera
@@ -5938,25 +5953,40 @@ class DeviceClient(object):
5938
5953
  _status(f"livePlayReq sent peerid={peer_id}")
5939
5954
  # Wait for the broker to echo livePlayReq back (confirms delivery).
5940
5955
  # Proceed as soon as the echo arrives; fall through after 0.5 s.
5941
- try:
5942
- await asyncio.wait_for(liveplay_echo_ev.wait(), timeout=0.5)
5943
- except asyncio.TimeoutError:
5944
- pass
5956
+ # AIDOT_FAST_CONNECT skips this too (the echo often just times out,
5957
+ # costing a flat 0.5 s); we proceed straight to SDP/ICE.
5958
+ if not _fast_connect:
5959
+ try:
5960
+ await asyncio.wait_for(liveplay_echo_ev.wait(), timeout=0.5)
5961
+ except asyncio.TimeoutError:
5962
+ pass
5945
5963
  # livePlayResp carries camera-side accept/reject. If the camera
5946
5964
  # rejects start-play, continuing to SDP/ICE causes large STUN churn
5947
5965
  # but no media. Fail fast with the concrete code.
5948
- try:
5949
- _lp_resp = await asyncio.wait_for(
5950
- asyncio.shield(liveplay_resp_fut), timeout=2.0
5951
- )
5952
- _lp_code = int(_lp_resp.get("code", 200))
5953
- _lp_on = int(_lp_resp.get("livePlay", 1))
5954
- if _lp_code not in (0, 200) or _lp_on == 0:
5955
- raise RuntimeError(
5956
- f"livePlay rejected by camera (code={_lp_code}, livePlay={_lp_on})"
5966
+ # MEASURED (2026-06-07, live A/B): this up-to-2 s wait is the DOMINANT
5967
+ # cold-start cost (camera-awake → ICE-servers ≈ 2.5 s = 0.5 s echo +
5968
+ # 2.0 s here). The official app does NOT wait for/parse livePlayResp
5969
+ # (parity-confirmed). AIDOT_FAST_CONNECT skips it and proceeds to
5970
+ # SDP/ICE immediately — losing fast-fail on rejection (rare; ICE then
5971
+ # fails instead) in exchange for ~2 s off every LAN connect.
5972
+ if not _fast_connect:
5973
+ try:
5974
+ _lp_resp = await asyncio.wait_for(
5975
+ asyncio.shield(liveplay_resp_fut), timeout=2.0
5957
5976
  )
5958
- except asyncio.TimeoutError:
5959
- pass
5977
+ _lp_code = int(_lp_resp.get("code", 200))
5978
+ _lp_on = int(_lp_resp.get("livePlay", 1))
5979
+ if _lp_code not in (0, 200) or _lp_on == 0:
5980
+ raise RuntimeError(
5981
+ f"livePlay rejected by camera (code={_lp_code}, livePlay={_lp_on})"
5982
+ )
5983
+ except asyncio.TimeoutError:
5984
+ pass
5985
+ else:
5986
+ _status(
5987
+ "AIDOT_FAST_CONNECT: skipping livePlayResp wait (~2s) -"
5988
+ " proceeding to SDP/ICE (app-parity, no fast-fail on reject)"
5989
+ )
5960
5990
  # Short extra wait for getIceConfigResp - the server may only respond
5961
5991
  # once a live camera session is active (i.e. after livePlayReq).
5962
5992
  # Waiting here ensures TURN credentials arrive before RTCPeerConnection
@@ -8614,6 +8644,17 @@ class DeviceClient(object):
8614
8644
  import tempfile
8615
8645
  import json
8616
8646
 
8647
+ # _open_sdes_stream runs in its own scope, so recompute the fast-connect
8648
+ # flag here (same precedence as the parent open path: explicit per-camera
8649
+ # option set via start_keepalive, else the AIDOT_FAST_CONNECT env var).
8650
+ # Without this the SDES path NameErrors on every open (regression fixed
8651
+ # 2026-06-07: the SDES TURN-skip gates reference _fast_connect).
8652
+ _fast_connect = getattr(self, "_fast_connect_opt", None)
8653
+ if _fast_connect is None:
8654
+ _fast_connect = os.environ.get("AIDOT_FAST_CONNECT", "").strip().lower() in (
8655
+ "1", "true", "yes", "on",
8656
+ )
8657
+
8617
8658
  user_id = user_id or str(self.user_id)
8618
8659
 
8619
8660
  # Models confirmed (2026-05-02) to send TUTK-framed data instead of
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 0.5.5
3
+ Version: 0.5.7
4
4
  Summary: Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)
5
5
  Author-email: cbrightly <chris.brightly@gmail.com>
6
6
  License-Expression: MIT