python-aidot-cameras 0.11.9__tar.gz → 0.11.11__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 (133) hide show
  1. {python_aidot_cameras-0.11.9/python_aidot_cameras.egg-info → python_aidot_cameras-0.11.11}/PKG-INFO +1 -1
  2. python_aidot_cameras-0.11.11/aidot/__init__.py +54 -0
  3. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/camera/protocol.py +38 -8
  4. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/camera/webrtc_open.py +5 -2
  5. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/pyproject.toml +1 -1
  6. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  7. python_aidot_cameras-0.11.11/tests/test_logging_caps.py +31 -0
  8. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_mqtt_session_reconnect.py +50 -0
  9. python_aidot_cameras-0.11.9/aidot/__init__.py +0 -46
  10. python_aidot_cameras-0.11.9/tests/test_logging_caps.py +0 -22
  11. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/LICENSE +0 -0
  12. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/MANIFEST.in +0 -0
  13. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/README.md +0 -0
  14. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/__main__.py +0 -0
  15. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/__init__.py +0 -0
  16. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/__init__.py +0 -0
  17. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/clock.py +0 -0
  18. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/codecs/__init__.py +0 -0
  19. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/codecs/base.py +0 -0
  20. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/codecs/g711.py +0 -0
  21. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/codecs/g722.py +0 -0
  22. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/codecs/h264.py +0 -0
  23. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/codecs/opus.py +0 -0
  24. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/codecs/vpx.py +0 -0
  25. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/contrib/__init__.py +0 -0
  26. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/contrib/media.py +0 -0
  27. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/contrib/signaling.py +0 -0
  28. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/events.py +0 -0
  29. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/exceptions.py +0 -0
  30. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/jitterbuffer.py +0 -0
  31. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/mediastreams.py +0 -0
  32. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rate.py +0 -0
  33. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcconfiguration.py +0 -0
  34. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcdatachannel.py +0 -0
  35. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcdtlstransport.py +0 -0
  36. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcicetransport.py +0 -0
  37. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcpeerconnection.py +0 -0
  38. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcrtpparameters.py +0 -0
  39. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  40. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcrtpsender.py +0 -0
  41. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  42. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcsctptransport.py +0 -0
  43. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcsessiondescription.py +0 -0
  44. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtp.py +0 -0
  45. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/sdp.py +0 -0
  46. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/stats.py +0 -0
  47. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/utils.py +0 -0
  48. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/aes_utils.py +0 -0
  49. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/camera/__init__.py +0 -0
  50. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/camera/client.py +0 -0
  51. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/camera/constants.py +0 -0
  52. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/camera/controls.py +0 -0
  53. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/camera/go2rtc.py +0 -0
  54. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/camera/lan_control.py +0 -0
  55. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/camera/models.py +0 -0
  56. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/camera/playback.py +0 -0
  57. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/camera/sdes.py +0 -0
  58. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/camera/sdes_open.py +0 -0
  59. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/camera/tutk.py +0 -0
  60. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/camera/webrtc.py +0 -0
  61. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/client.py +0 -0
  62. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/const.py +0 -0
  63. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/credentials.py +0 -0
  64. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/device_client.py +0 -0
  65. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/discover.py +0 -0
  66. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/exceptions.py +0 -0
  67. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/g711.py +0 -0
  68. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/login_const.py +0 -0
  69. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/models/__init__.py +0 -0
  70. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/models/device_client_model.py +0 -0
  71. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/models/device_model.py +0 -0
  72. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/models/discover_model.py +0 -0
  73. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/aidot/py.typed +0 -0
  74. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
  75. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  76. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  77. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/python_aidot_cameras.egg-info/requires.txt +0 -0
  78. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  79. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/setup.cfg +0 -0
  80. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_aioice_compat.py +0 -0
  81. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_alarm_event.py +0 -0
  82. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_backoff.py +0 -0
  83. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_deferred_hardening.py +0 -0
  84. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_device_login_guard.py +0 -0
  85. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_device_user_info_cache.py +0 -0
  86. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_dtls_not_ready_burst.py +0 -0
  87. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_dtls_pinning.py +0 -0
  88. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_dtls_skip_signaling_wait.py +0 -0
  89. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_egress_guard.py +0 -0
  90. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_go2rtc.py +0 -0
  91. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_go2rtc_cli.py +0 -0
  92. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_highport_nomination.py +0 -0
  93. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_ice_config_cache.py +0 -0
  94. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_keepalive_renew.py +0 -0
  95. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_keyframe_prompter.py +0 -0
  96. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_lan_control.py +0 -0
  97. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_live_stream_param.py +0 -0
  98. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_motion_poll.py +0 -0
  99. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_narrow_pc_ice.py +0 -0
  100. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_no_undefined_names.py +0 -0
  101. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_offline_keepalive_pause.py +0 -0
  102. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_open_fail_logger.py +0 -0
  103. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_open_gate_delay.py +0 -0
  104. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_persistent_mqtt.py +0 -0
  105. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_persistent_mqtt_robustness.py +0 -0
  106. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_playback_tls.py +0 -0
  107. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_post_merge_hardening.py +0 -0
  108. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_retry_policy.py +0 -0
  109. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_sdes_adaptive.py +0 -0
  110. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_sdes_echo_wait_timeout.py +0 -0
  111. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_sdes_fast_liveplay.py +0 -0
  112. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  113. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_sdes_idle_release.py +0 -0
  114. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_sdes_open_cleanup.py +0 -0
  115. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_sdes_serve_audio.py +0 -0
  116. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_sdes_serve_cmd.py +0 -0
  117. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_sdes_sprop.py +0 -0
  118. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_sdes_talk.py +0 -0
  119. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_sdes_watchdog.py +0 -0
  120. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_security_hardening.py +0 -0
  121. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_serializable_login_info.py +0 -0
  122. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_serve_relay.py +0 -0
  123. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_session_stats.py +0 -0
  124. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_speak.py +0 -0
  125. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_stream_cap.py +0 -0
  126. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_stream_drain_ownership.py +0 -0
  127. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_stream_idle.py +0 -0
  128. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_stream_teardown.py +0 -0
  129. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_talk.py +0 -0
  130. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_terminal_ack.py +0 -0
  131. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_token_file_robustness.py +0 -0
  132. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_token_refresh.py +0 -0
  133. {python_aidot_cameras-0.11.9 → python_aidot_cameras-0.11.11}/tests/test_wait_or_event.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 0.11.9
3
+ Version: 0.11.11
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
@@ -0,0 +1,54 @@
1
+ """AiDot camera and device library."""
2
+
3
+ import logging as _logging
4
+
5
+ from .client import AidotClient
6
+ from .device_client import DeviceClient
7
+ from .discover import Discover
8
+ from .exceptions import (
9
+ AidotAuthFailed,
10
+ AidotAuthTokenExpired,
11
+ AidotCameraBusy,
12
+ AidotError,
13
+ AidotNotLogin,
14
+ AidotOSError,
15
+ AidotUserOrPassIncorrect,
16
+ HTTPError,
17
+ InvalidHost,
18
+ InvalidURL,
19
+ )
20
+
21
+ # Cap the per-packet loggers that flood the log at DEBUG. When a user enables
22
+ # DEBUG on the parent ``aidot`` logger to diagnose the integration, these emit
23
+ # thousands of lines per second (in one capture, 99% of all lines) - and on a
24
+ # microSD Home Assistant host that log I/O can starve the recorder. Cap them at
25
+ # INFO so enabling ``aidot`` DEBUG stays useful; the diagnostically valuable
26
+ # DEBUG (DTLS, SCTP/DCEP, ICE connection state) still flows. This covers both the
27
+ # vendored aiortc RTP receiver/sender AND the external ``aioice`` package's
28
+ # per-STUN/TURN-packet loggers (aioice is a real dependency, not vendored, so it
29
+ # is not under ``aidot._vendor``). Respect an explicit level if the user set one.
30
+ for _chatty in (
31
+ "aidot._vendor.aiortc.rtcrtpreceiver",
32
+ "aidot._vendor.aiortc.rtcrtpsender",
33
+ "aioice.ice",
34
+ "aioice.turn",
35
+ ):
36
+ _lg = _logging.getLogger(_chatty)
37
+ if _lg.level == _logging.NOTSET:
38
+ _lg.setLevel(_logging.INFO)
39
+
40
+ __all__ = [
41
+ "AidotAuthFailed",
42
+ "AidotAuthTokenExpired",
43
+ "AidotCameraBusy",
44
+ "AidotClient",
45
+ "AidotError",
46
+ "AidotNotLogin",
47
+ "AidotOSError",
48
+ "AidotUserOrPassIncorrect",
49
+ "DeviceClient",
50
+ "Discover",
51
+ "HTTPError",
52
+ "InvalidHost",
53
+ "InvalidURL",
54
+ ]
@@ -79,6 +79,13 @@ def _build_stun_binding_success_response(
79
79
 
80
80
  _WEBRTC_TERMINAL_ACK_CODES = (-50002, -50015)
81
81
 
82
+ # If a post-connect MQTT drop stays disconnected longer than this (seconds) with
83
+ # no successful paho reconnect, the receive loop ends the session rather than
84
+ # polling a dead socket until the full duration deadline. Long enough to ride
85
+ # out an ordinary network blip + paho's auto-reconnect, short enough that a
86
+ # terminal drop (session taken over, revoked creds, broker gone) fails fast.
87
+ _MQTT_RECONNECT_GRACE = 20.0
88
+
82
89
 
83
90
  def _terminal_webrtc_ack(msg: dict):
84
91
  """Return ``(code, desc)`` if ``msg`` is a webrtcResp carrying a TERMINAL ack
@@ -1188,7 +1195,8 @@ def _mqtt_session_sync(
1188
1195
 
1189
1196
  msg_q = _queue.Queue()
1190
1197
  conn_ev = threading.Event()
1191
- status = {"connected": False, "rc": None, "rc_str": "", "error": None, "log": []}
1198
+ status = {"connected": False, "rc": None, "rc_str": "", "error": None,
1199
+ "log": [], "disconnected_since": None}
1192
1200
 
1193
1201
  # Build client - handle paho >=2.0 (VERSION2) and <2.0
1194
1202
  try:
@@ -1217,6 +1225,9 @@ def _mqtt_session_sync(
1217
1225
  status["rc"] = rc
1218
1226
  status["rc_str"] = str(reason_code)
1219
1227
  if rc == 0:
1228
+ # Successful (re)connect: a prior drop (if any) has recovered, so the
1229
+ # receive loop must not fail it fast - clear the disconnect marker.
1230
+ status["disconnected_since"] = None
1220
1231
  # (Re)subscribe on EVERY successful connect. paho's loop_start
1221
1232
  # auto-reconnects after a transient drop, but with clean_session the
1222
1233
  # broker retains no subscriptions, so a reconnected client would be
@@ -1250,14 +1261,21 @@ def _mqtt_session_sync(
1250
1261
  conn_ev.set()
1251
1262
  msg_q.put(None) # sentinel: end the receive loop
1252
1263
  return
1253
- # Already connected once: this is a transient drop. paho's loop_start
1254
- # auto-reconnects and _on_connect re-subscribes, so DON'T end the receive
1255
- # loop here - a broker blip must not tear down a long-lived stream's
1256
- # signaling (the non-persistent transport runs one session for the whole
1257
- # stream). A real teardown ends the loop via the outgoing-queue None
1258
- # sentinel or the duration deadline instead.
1264
+ # Already connected once: this is (so far) a transient drop. paho's
1265
+ # loop_start auto-reconnects and _on_connect re-subscribes AND clears
1266
+ # disconnected_since, so a brief blip must NOT tear down a long-lived
1267
+ # stream's signaling (the non-persistent transport runs one session for
1268
+ # the whole stream). But a drop that never recovers - session taken over
1269
+ # (rc=142) when the account's persistent client reclaims the clientId,
1270
+ # revoked creds, broker gone - would otherwise leave the receive loop
1271
+ # polling a dead socket until the full duration deadline. Record when the
1272
+ # drop began; the loop ends the session once it has persisted past
1273
+ # _MQTT_RECONNECT_GRACE. (dict single-key set/get is atomic under the GIL,
1274
+ # so no lock is needed across the paho thread and the receive loop.)
1275
+ if status.get("disconnected_since") is None:
1276
+ status["disconnected_since"] = time.monotonic()
1259
1277
  _LOGGER.debug(
1260
- "_mqtt_session: transient disconnect rc=%s (paho will reconnect)",
1278
+ "_mqtt_session: disconnect rc=%s (awaiting paho reconnect)",
1261
1279
  reason_code,
1262
1280
  )
1263
1281
 
@@ -1327,6 +1345,18 @@ def _mqtt_session_sync(
1327
1345
  remaining = deadline - _time.monotonic()
1328
1346
  if remaining <= 0:
1329
1347
  break
1348
+ # End the session if a post-connect drop never recovered: a terminal
1349
+ # disconnect delivers no further messages, so without this the loop would
1350
+ # poll a dead socket (and the caller's camera signaling would silently
1351
+ # stall) until the full duration deadline.
1352
+ _ds = status.get("disconnected_since")
1353
+ if _ds is not None and (_time.monotonic() - _ds) > _MQTT_RECONNECT_GRACE:
1354
+ _LOGGER.warning(
1355
+ "_mqtt_session: MQTT disconnected >%.0fs with no reconnect "
1356
+ "(rc=%s) - ending session; camera signaling can no longer arrive",
1357
+ _MQTT_RECONNECT_GRACE, status.get("rc_str"),
1358
+ )
1359
+ break
1330
1360
  try:
1331
1361
  item = msg_q.get(timeout=min(remaining, 0.1))
1332
1362
  except _queue.Empty:
@@ -1284,8 +1284,11 @@ class _WebRTCOpenMixin:
1284
1284
  # ------------------------------------------------------------------ #
1285
1285
  # aiortc peer connection (DTLS-SRTP path)
1286
1286
  # ------------------------------------------------------------------ #
1287
- import logging as _logging_dtls
1288
- _logging_dtls.getLogger("aioice").setLevel(_logging_dtls.DEBUG)
1287
+ # NOTE: do NOT force aioice to DEBUG here. It floods the log on every DTLS
1288
+ # open, and the package-init cap (aidot/__init__.py) holds aioice.ice /
1289
+ # aioice.turn at INFO anyway, so forcing DEBUG was both self-defeating and
1290
+ # noisy. To debug ICE, set the aioice logger level explicitly (that
1291
+ # overrides the NOTSET-guarded cap).
1289
1292
  from aidot._vendor.aiortc import RTCConfiguration, RTCIceServer
1290
1293
 
1291
1294
  def _sanitize_ice_uris(uris):
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-aidot-cameras"
7
- version = "0.11.9"
7
+ version = "0.11.11"
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 0.11.9
3
+ Version: 0.11.11
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
@@ -0,0 +1,31 @@
1
+ """Importing the library caps the chatty per-packet loggers.
2
+
3
+ aiortc's RTP receiver/sender log every media packet at DEBUG, and the external
4
+ aioice package logs every STUN/TURN packet. Enabling DEBUG on the parent ``aidot``
5
+ logger (the normal way to diagnose the integration) would otherwise unleash
6
+ thousands of lines per second - which on a microSD host can starve the recorder.
7
+ The package init caps those loggers at INFO so DEBUG on ``aidot`` stays useful.
8
+ """
9
+ import logging
10
+
11
+ import aidot # noqa: F401 - import triggers the logger caps
12
+
13
+ CAPPED = (
14
+ "aidot._vendor.aiortc.rtcrtpreceiver",
15
+ "aidot._vendor.aiortc.rtcrtpsender",
16
+ "aioice.ice",
17
+ "aioice.turn",
18
+ )
19
+
20
+
21
+ def test_packet_loggers_capped_to_info():
22
+ for name in CAPPED:
23
+ lg = logging.getLogger(name)
24
+ assert lg.level == logging.INFO, f"{name} not capped (level={lg.level})"
25
+
26
+
27
+ def test_aidot_debug_does_not_unleash_the_packet_firehose():
28
+ # Even with the parent 'aidot' logger at DEBUG, the per-packet DEBUG is off.
29
+ logging.getLogger("aidot").setLevel(logging.DEBUG)
30
+ for name in CAPPED:
31
+ assert not logging.getLogger(name).isEnabledFor(logging.DEBUG), f"{name} still emits DEBUG"
@@ -82,3 +82,53 @@ def test_disconnect_before_connect_ends_session(monkeypatch):
82
82
  )
83
83
  assert messages == []
84
84
  assert status["connected"] is False
85
+
86
+
87
+ def test_terminal_disconnect_ends_session_before_deadline(monkeypatch):
88
+ """A post-connect drop that never reconnects must end the session via the
89
+ reconnect grace, not poll a dead socket until the full duration deadline."""
90
+ monkeypatch.setattr(proto, "_MQTT_RECONNECT_GRACE", 0.15)
91
+
92
+ def driver(c):
93
+ c.on_connect(c, None, {}, 0) # connected
94
+ c.on_message(c, None, _FakeMsg("t/before", b"x")) # delivered pre-drop
95
+ time.sleep(0.02)
96
+ c.on_disconnect(c, None, 142) # terminal drop; NO reconnect
97
+
98
+ _install_fake_client(monkeypatch, driver)
99
+
100
+ t0 = time.monotonic()
101
+ messages, status = proto._mqtt_session_sync(
102
+ "wss://broker.example/mqtt", "u", "p", "cid",
103
+ ["t/a"], [], 30.0, # 30s duration - must NOT wait this long
104
+ )
105
+ elapsed = time.monotonic() - t0
106
+
107
+ assert elapsed < 5.0, f"terminal disconnect should end fast, took {elapsed:.1f}s"
108
+ assert ("t/before", "x") in messages # pre-drop message still collected
109
+ assert status["disconnected_since"] is not None # the drop was recorded
110
+
111
+
112
+ def test_reconnect_within_grace_keeps_session(monkeypatch):
113
+ """A drop that reconnects within the grace clears disconnected_since, so the
114
+ receive loop must NOT fail the session fast (guards the transient path)."""
115
+ monkeypatch.setattr(proto, "_MQTT_RECONNECT_GRACE", 0.5)
116
+ outgoing = queue.Queue()
117
+
118
+ def driver(c):
119
+ c.on_connect(c, None, {}, 0)
120
+ c.on_disconnect(c, None, 7) # blip
121
+ time.sleep(0.05) # < grace
122
+ c.on_connect(c, None, {}, 0) # recovered
123
+ c.on_message(c, None, _FakeMsg("t/after", b"live"))
124
+ time.sleep(0.05)
125
+ outgoing.put_nowait(None) # clean stop
126
+
127
+ _install_fake_client(monkeypatch, driver)
128
+
129
+ messages, status = proto._mqtt_session_sync(
130
+ "wss://broker.example/mqtt", "u", "p", "cid",
131
+ ["t/a"], [], 30.0, outgoing_queue=outgoing,
132
+ )
133
+ assert ("t/after", "live") in messages # survived the blip
134
+ assert status["disconnected_since"] is None # cleared on reconnect
@@ -1,46 +0,0 @@
1
- """AiDot camera and device library."""
2
-
3
- import logging as _logging
4
-
5
- from .client import AidotClient
6
- from .device_client import DeviceClient
7
- from .discover import Discover
8
- from .exceptions import (
9
- AidotAuthFailed,
10
- AidotAuthTokenExpired,
11
- AidotCameraBusy,
12
- AidotError,
13
- AidotNotLogin,
14
- AidotOSError,
15
- AidotUserOrPassIncorrect,
16
- HTTPError,
17
- InvalidHost,
18
- InvalidURL,
19
- )
20
-
21
- # The vendored aiortc RTP receiver/sender log EVERY media packet at DEBUG. When a
22
- # user enables DEBUG on the parent ``aidot`` logger to diagnose the integration,
23
- # that becomes thousands of lines per second and floods the logs (in one capture,
24
- # 99% of all lines). Cap just those two per-packet loggers at INFO so enabling
25
- # ``aidot`` DEBUG stays useful - the diagnostically valuable aiortc DEBUG (DTLS,
26
- # ICE, SCTP/DCEP) still flows. Respect an explicit level if the user set one.
27
- for _chatty in ("aidot._vendor.aiortc.rtcrtpreceiver", "aidot._vendor.aiortc.rtcrtpsender"):
28
- _lg = _logging.getLogger(_chatty)
29
- if _lg.level == _logging.NOTSET:
30
- _lg.setLevel(_logging.INFO)
31
-
32
- __all__ = [
33
- "AidotAuthFailed",
34
- "AidotAuthTokenExpired",
35
- "AidotCameraBusy",
36
- "AidotClient",
37
- "AidotError",
38
- "AidotNotLogin",
39
- "AidotOSError",
40
- "AidotUserOrPassIncorrect",
41
- "DeviceClient",
42
- "Discover",
43
- "HTTPError",
44
- "InvalidHost",
45
- "InvalidURL",
46
- ]
@@ -1,22 +0,0 @@
1
- """Importing the library caps the vendored aiortc per-packet loggers.
2
-
3
- aiortc's RTP receiver/sender log every media packet at DEBUG. Enabling DEBUG on
4
- the parent ``aidot`` logger (the normal way to diagnose the integration) would
5
- otherwise unleash thousands of lines per second. The package init caps just those
6
- two loggers at INFO so DEBUG on ``aidot`` stays useful without the firehose.
7
- """
8
- import logging
9
-
10
- import aidot # noqa: F401 - import triggers the logger caps
11
-
12
-
13
- def test_vendored_aiortc_packet_loggers_capped_to_info():
14
- for name in (
15
- "aidot._vendor.aiortc.rtcrtpreceiver",
16
- "aidot._vendor.aiortc.rtcrtpsender",
17
- ):
18
- lg = logging.getLogger(name)
19
- assert lg.level == logging.INFO, f"{name} not capped (level={lg.level})"
20
- # Even with the parent 'aidot' logger at DEBUG, the per-packet DEBUG is off.
21
- logging.getLogger("aidot").setLevel(logging.DEBUG)
22
- assert not lg.isEnabledFor(logging.DEBUG), f"{name} still emits DEBUG"