python-aidot-cameras 0.11.10__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 (131) hide show
  1. {python_aidot_cameras-0.11.10/python_aidot_cameras.egg-info → python_aidot_cameras-0.11.11}/PKG-INFO +1 -1
  2. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/camera/protocol.py +38 -8
  3. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/camera/webrtc_open.py +5 -2
  4. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/pyproject.toml +1 -1
  5. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  6. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_mqtt_session_reconnect.py +50 -0
  7. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/LICENSE +0 -0
  8. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/MANIFEST.in +0 -0
  9. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/README.md +0 -0
  10. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/__init__.py +0 -0
  11. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/__main__.py +0 -0
  12. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/__init__.py +0 -0
  13. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/__init__.py +0 -0
  14. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/clock.py +0 -0
  15. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/codecs/__init__.py +0 -0
  16. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/codecs/base.py +0 -0
  17. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/codecs/g711.py +0 -0
  18. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/codecs/g722.py +0 -0
  19. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/codecs/h264.py +0 -0
  20. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/codecs/opus.py +0 -0
  21. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/codecs/vpx.py +0 -0
  22. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/contrib/__init__.py +0 -0
  23. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/contrib/media.py +0 -0
  24. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/contrib/signaling.py +0 -0
  25. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/events.py +0 -0
  26. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/exceptions.py +0 -0
  27. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/jitterbuffer.py +0 -0
  28. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/mediastreams.py +0 -0
  29. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rate.py +0 -0
  30. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcconfiguration.py +0 -0
  31. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcdatachannel.py +0 -0
  32. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcdtlstransport.py +0 -0
  33. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcicetransport.py +0 -0
  34. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcpeerconnection.py +0 -0
  35. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcrtpparameters.py +0 -0
  36. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  37. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcrtpsender.py +0 -0
  38. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  39. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcsctptransport.py +0 -0
  40. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtcsessiondescription.py +0 -0
  41. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/rtp.py +0 -0
  42. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/sdp.py +0 -0
  43. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/stats.py +0 -0
  44. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/_vendor/aiortc/utils.py +0 -0
  45. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/aes_utils.py +0 -0
  46. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/camera/__init__.py +0 -0
  47. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/camera/client.py +0 -0
  48. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/camera/constants.py +0 -0
  49. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/camera/controls.py +0 -0
  50. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/camera/go2rtc.py +0 -0
  51. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/camera/lan_control.py +0 -0
  52. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/camera/models.py +0 -0
  53. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/camera/playback.py +0 -0
  54. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/camera/sdes.py +0 -0
  55. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/camera/sdes_open.py +0 -0
  56. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/camera/tutk.py +0 -0
  57. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/camera/webrtc.py +0 -0
  58. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/client.py +0 -0
  59. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/const.py +0 -0
  60. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/credentials.py +0 -0
  61. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/device_client.py +0 -0
  62. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/discover.py +0 -0
  63. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/exceptions.py +0 -0
  64. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/g711.py +0 -0
  65. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/login_const.py +0 -0
  66. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/models/__init__.py +0 -0
  67. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/models/device_client_model.py +0 -0
  68. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/models/device_model.py +0 -0
  69. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/models/discover_model.py +0 -0
  70. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/aidot/py.typed +0 -0
  71. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
  72. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  73. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  74. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/python_aidot_cameras.egg-info/requires.txt +0 -0
  75. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  76. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/setup.cfg +0 -0
  77. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_aioice_compat.py +0 -0
  78. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_alarm_event.py +0 -0
  79. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_backoff.py +0 -0
  80. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_deferred_hardening.py +0 -0
  81. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_device_login_guard.py +0 -0
  82. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_device_user_info_cache.py +0 -0
  83. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_dtls_not_ready_burst.py +0 -0
  84. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_dtls_pinning.py +0 -0
  85. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_dtls_skip_signaling_wait.py +0 -0
  86. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_egress_guard.py +0 -0
  87. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_go2rtc.py +0 -0
  88. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_go2rtc_cli.py +0 -0
  89. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_highport_nomination.py +0 -0
  90. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_ice_config_cache.py +0 -0
  91. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_keepalive_renew.py +0 -0
  92. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_keyframe_prompter.py +0 -0
  93. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_lan_control.py +0 -0
  94. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_live_stream_param.py +0 -0
  95. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_logging_caps.py +0 -0
  96. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_motion_poll.py +0 -0
  97. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_narrow_pc_ice.py +0 -0
  98. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_no_undefined_names.py +0 -0
  99. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_offline_keepalive_pause.py +0 -0
  100. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_open_fail_logger.py +0 -0
  101. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_open_gate_delay.py +0 -0
  102. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_persistent_mqtt.py +0 -0
  103. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_persistent_mqtt_robustness.py +0 -0
  104. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_playback_tls.py +0 -0
  105. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_post_merge_hardening.py +0 -0
  106. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_retry_policy.py +0 -0
  107. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_sdes_adaptive.py +0 -0
  108. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_sdes_echo_wait_timeout.py +0 -0
  109. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_sdes_fast_liveplay.py +0 -0
  110. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  111. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_sdes_idle_release.py +0 -0
  112. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_sdes_open_cleanup.py +0 -0
  113. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_sdes_serve_audio.py +0 -0
  114. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_sdes_serve_cmd.py +0 -0
  115. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_sdes_sprop.py +0 -0
  116. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_sdes_talk.py +0 -0
  117. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_sdes_watchdog.py +0 -0
  118. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_security_hardening.py +0 -0
  119. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_serializable_login_info.py +0 -0
  120. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_serve_relay.py +0 -0
  121. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_session_stats.py +0 -0
  122. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_speak.py +0 -0
  123. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_stream_cap.py +0 -0
  124. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_stream_drain_ownership.py +0 -0
  125. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_stream_idle.py +0 -0
  126. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_stream_teardown.py +0 -0
  127. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_talk.py +0 -0
  128. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_terminal_ack.py +0 -0
  129. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_token_file_robustness.py +0 -0
  130. {python_aidot_cameras-0.11.10 → python_aidot_cameras-0.11.11}/tests/test_token_refresh.py +0 -0
  131. {python_aidot_cameras-0.11.10 → 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.10
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
@@ -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.10"
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.10
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
@@ -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