python-aidot-cameras 0.13.1__tar.gz → 0.13.2__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 (159) hide show
  1. {python_aidot_cameras-0.13.1/python_aidot_cameras.egg-info → python_aidot_cameras-0.13.2}/PKG-INFO +1 -1
  2. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/client.py +21 -9
  3. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/pyproject.toml +1 -1
  4. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  5. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
  6. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_battery_detection.py +30 -3
  7. python_aidot_cameras-0.13.2/tests/test_battery_evidence_corroboration.py +139 -0
  8. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_live_validate_policy.py +55 -0
  9. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/LICENSE +0 -0
  10. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/MANIFEST.in +0 -0
  11. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/README.md +0 -0
  12. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/__init__.py +0 -0
  13. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/__main__.py +0 -0
  14. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_upstream.py +0 -0
  15. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/__init__.py +0 -0
  16. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  17. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  18. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  19. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  20. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  21. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  22. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  23. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  24. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  25. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  26. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  27. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  28. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  29. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  30. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  31. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  32. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  33. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  34. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  35. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  36. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  37. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  38. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  39. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  40. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  41. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  42. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  43. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  44. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  45. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  46. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  47. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  48. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/__init__.py +0 -0
  49. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/constants.py +0 -0
  50. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/controls.py +0 -0
  51. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/go2rtc.py +0 -0
  52. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/lan_control.py +0 -0
  53. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/models.py +0 -0
  54. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/playback.py +0 -0
  55. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/protocol.py +0 -0
  56. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/sdes.py +0 -0
  57. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/sdes_open.py +0 -0
  58. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/tutk.py +0 -0
  59. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/webrtc.py +0 -0
  60. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/webrtc_open.py +0 -0
  61. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/client.py +0 -0
  62. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/const.py +0 -0
  63. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/credentials.py +0 -0
  64. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/crypto.py +0 -0
  65. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/device_client.py +0 -0
  66. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/discover.py +0 -0
  67. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/exceptions.py +0 -0
  68. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/g711.py +0 -0
  69. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/aidot_cameras/py.typed +0 -0
  70. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  71. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  72. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/python_aidot_cameras.egg-info/requires.txt +0 -0
  73. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  74. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/setup.cfg +0 -0
  75. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_aioice_compat.py +0 -0
  76. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_alarm_event.py +0 -0
  77. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_backoff.py +0 -0
  78. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_battery_skip_turn.py +0 -0
  79. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_constants_imports_resolve.py +0 -0
  80. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_deferred_hardening.py +0 -0
  81. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_device_login_guard.py +0 -0
  82. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_device_user_info_cache.py +0 -0
  83. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_dormant_camera_cleanup.py +0 -0
  84. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_dtls_mux_terminal_write.py +0 -0
  85. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_dtls_not_ready_burst.py +0 -0
  86. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_dtls_pinning.py +0 -0
  87. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_dtls_serve_decode_skip.py +0 -0
  88. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_dtls_serve_open_timeout.py +0 -0
  89. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_dtls_skip_signaling_wait.py +0 -0
  90. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_dtls_slow_probe.py +0 -0
  91. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_egress_guard.py +0 -0
  92. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_go2rtc.py +0 -0
  93. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_go2rtc_cli.py +0 -0
  94. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_h264_decode_rate_limit.py +0 -0
  95. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_highport_nomination.py +0 -0
  96. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_ice_config_cache.py +0 -0
  97. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_ice_peer_reflexive.py +0 -0
  98. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_idle_release_both_loops.py +0 -0
  99. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_keepalive_renew.py +0 -0
  100. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_key_restart_sdp.py +0 -0
  101. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_keyframe_prompter.py +0 -0
  102. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_lan_control.py +0 -0
  103. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_light_active_color_mode.py +0 -0
  104. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_live_stream_param.py +0 -0
  105. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_logging_caps.py +0 -0
  106. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_motion_poll.py +0 -0
  107. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_mqtt_credential_refetch.py +0 -0
  108. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_mqtt_credential_self_heal.py +0 -0
  109. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_mqtt_password_not_persisted.py +0 -0
  110. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_mqtt_session_reconnect.py +0 -0
  111. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_narrow_pc_ice.py +0 -0
  112. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_no_self_referential_go2rtc.py +0 -0
  113. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_no_undefined_names.py +0 -0
  114. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_offline_keepalive_pause.py +0 -0
  115. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_open_fail_logger.py +0 -0
  116. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_open_gate_delay.py +0 -0
  117. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_peerid_reuse.py +0 -0
  118. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_persistent_mqtt.py +0 -0
  119. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_persistent_mqtt_robustness.py +0 -0
  120. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_playback_tls.py +0 -0
  121. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_post_merge_hardening.py +0 -0
  122. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_retry_policy.py +0 -0
  123. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_sdes_adaptive.py +0 -0
  124. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_sdes_bridge_break_gate.py +0 -0
  125. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_sdes_echo_wait_timeout.py +0 -0
  126. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_sdes_fast_liveplay.py +0 -0
  127. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  128. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_sdes_idle_release.py +0 -0
  129. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_sdes_open_cleanup.py +0 -0
  130. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_sdes_serve_audio.py +0 -0
  131. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_sdes_serve_cmd.py +0 -0
  132. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_sdes_sprop.py +0 -0
  133. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_sdes_talk.py +0 -0
  134. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_sdes_teardown_exit_log.py +0 -0
  135. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_sdes_terminal_ack.py +0 -0
  136. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_sdes_watchdog.py +0 -0
  137. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_sdp_payload_type_narrowing.py +0 -0
  138. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_security_hardening.py +0 -0
  139. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_serializable_login_info.py +0 -0
  140. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_serve_relay.py +0 -0
  141. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_session_stats.py +0 -0
  142. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_shared_house_seam.py +0 -0
  143. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_speak.py +0 -0
  144. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_stream_cap.py +0 -0
  145. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_stream_drain_ownership.py +0 -0
  146. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_stream_idle.py +0 -0
  147. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_stream_slot_cap.py +0 -0
  148. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_stream_teardown.py +0 -0
  149. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_talk.py +0 -0
  150. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_terminal_ack.py +0 -0
  151. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_token_file_robustness.py +0 -0
  152. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_token_refresh.py +0 -0
  153. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_turn_create_permission.py +0 -0
  154. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_upstream_compat.py +0 -0
  155. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_viewer_check_throttle.py +0 -0
  156. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_viewer_presence.py +0 -0
  157. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_viewer_present_push_mode.py +0 -0
  158. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_wait_or_event.py +0 -0
  159. {python_aidot_cameras-0.13.1 → python_aidot_cameras-0.13.2}/tests/test_wake_readiness_retry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 0.13.1
3
+ Version: 0.13.2
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
@@ -960,15 +960,27 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
960
960
  for _key in ("Battery_remaining", "batteryRemaining", "batteryLevel"):
961
961
  if _as_int(_props.get(_key)) is not None:
962
962
  return True
963
- # The low-power flag: batteryMode=2 is what a camera that sleeps between
964
- # sessions reports (see _async_send_camera_command). Deliberately NOT
965
- # keyed on ``powerType``: that name is also the value we SEND in
966
- # livePlayReq, we have never observed the cloud returning it as a device
967
- # property, and p2pCache - the field right next to it - reads 2 on every
968
- # camera including mains ones. Reading it here would risk classifying a
969
- # mains camera as battery, which would put powerType=2 on ITS wire payload
970
- # and can leave an A000088 un-armed. Evidence has to be unambiguous.
971
- if _as_int(_props.get("batteryMode")) == 2:
963
+ # The low-power flag, but ONLY when corroborated. ``batteryMode=2`` on
964
+ # its own is not evidence: measured against the live cloud payload, every
965
+ # MAINS A000088 on this fleet reports ``batteryMode: '2'`` and nothing
966
+ # else battery-related, while a real battery model reports it alongside
967
+ # actual telemetry (``Battery_remaining``, ``lowPowerStatus``,
968
+ # ``charging``). Trusting it alone classified four mains cameras as
969
+ # battery, which put powerType=2 on their wire payload and gave them the
970
+ # battery idle window instead of the mains warm-hold.
971
+ #
972
+ # This is the same trap the paragraph below describes for ``powerType`` /
973
+ # ``p2pCache`` - a field that reads 2 on every camera - so it gets the
974
+ # same treatment rather than being taken at face value. Corroboration is
975
+ # kept (instead of dropping the flag) so a future battery model that
976
+ # names its level differently is still caught by the flag plus any one
977
+ # battery-only field.
978
+ _BATTERY_TELEMETRY = (
979
+ "Battery_remaining", "batteryRemaining", "batteryLevel",
980
+ "lowPowerStatus", "charging",
981
+ )
982
+ if (_as_int(_props.get("batteryMode")) == 2
983
+ and any(_props.get(k) is not None for k in _BATTERY_TELEMETRY)):
972
984
  return True
973
985
  # Already-parsed status, for a camera refreshed from an attribute push
974
986
  # rather than from a device-list dict.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-aidot-cameras"
7
- version = "0.13.1"
7
+ version = "0.13.2"
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.13.1
3
+ Version: 0.13.2
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
@@ -71,6 +71,7 @@ tests/test_aioice_compat.py
71
71
  tests/test_alarm_event.py
72
72
  tests/test_backoff.py
73
73
  tests/test_battery_detection.py
74
+ tests/test_battery_evidence_corroboration.py
74
75
  tests/test_battery_skip_turn.py
75
76
  tests/test_constants_imports_resolve.py
76
77
  tests/test_deferred_hardening.py
@@ -91,9 +91,36 @@ def test_numeric_string_battery_level_counts():
91
91
  assert _cam("LK.IPC.A009999", props={"Battery_remaining": "38"}).is_battery_camera
92
92
 
93
93
 
94
- def test_battery_mode_two_counts():
95
- assert _cam("LK.IPC.A009999", props={"batteryMode": 2}).is_battery_camera is True
96
- assert _cam("LK.IPC.A009999", props={"batteryMode": "2"}).is_battery_camera is True
94
+ def test_battery_mode_two_alone_is_not_evidence():
95
+ """REVISED 2026-08-02 against the live cloud payload.
96
+
97
+ This test used to assert that ``batteryMode=2`` alone proves a battery, on
98
+ the reasonable reading of the field's name. The account's actual device list
99
+ disproves it: every MAINS A000088 reports ``batteryMode: '2'`` and no other
100
+ battery field, while real battery models report it alongside real telemetry
101
+ (``Battery_remaining``, ``lowPowerStatus``, ``charging``).
102
+
103
+ Trusting it alone classified four mains cameras as battery, which put
104
+ powerType=2 on their wire payload and gave them the battery idle window
105
+ instead of the mains warm-hold - the same trap
106
+ ``test_power_type_property_is_deliberately_not_evidence`` guards for
107
+ ``powerType``/``p2pCache``, which also read 2 on every camera.
108
+ """
109
+ for value in (2, "2"):
110
+ assert _cam("LK.IPC.A009999",
111
+ props={"batteryMode": value}).is_battery_camera is False
112
+
113
+
114
+ def test_battery_mode_two_counts_when_corroborated():
115
+ """The flag is still a signal - it just needs one real battery field.
116
+
117
+ Kept (rather than dropping the flag) so a battery model whose level field we
118
+ have not seen is still caught, provided it reports any battery-only field.
119
+ """
120
+ for extra in ({"charging": "0"}, {"lowPowerStatus": "0"},
121
+ {"Battery_remaining": "100"}):
122
+ assert _cam("LK.IPC.A009999",
123
+ props={"batteryMode": "2", **extra}).is_battery_camera is True
97
124
 
98
125
 
99
126
  def test_battery_mode_one_is_not_evidence():
@@ -0,0 +1,139 @@
1
+ """``batteryMode=2`` alone must not make a mains camera look like a battery one.
2
+
3
+ Every property payload below is the REAL one from the live cloud account
4
+ (2026-08-02), not a construction. That matters: the bug was believing a field
5
+ meant what its name suggests, and only the actual payload disproves it.
6
+
7
+ A001513 (real battery): batteryMode=2, Battery_remaining, lowPowerStatus, charging
8
+ A000088 (MAINS): batteryMode=2 <- and nothing else
9
+ A001064 (mains PTZ): {}
10
+
11
+ Misclassification is costly in BOTH directions, which is why the corroboration
12
+ rule is "flag AND telemetry" rather than dropping the flag or trusting it:
13
+
14
+ * battery read as mains loses the SDES TURN pre-allocation (a cloud-woken
15
+ battery camera has no LAN host candidate, so the relay is its ONLY return
16
+ path), the keep-alive renew and the HTTP wake;
17
+ * mains read as battery puts powerType=2 on its wire payload and hands it the
18
+ battery idle window instead of the mains warm-hold - which is what shipped.
19
+ """
20
+
21
+ import types
22
+
23
+ import pytest
24
+
25
+ from aidot_cameras.camera.client import CameraMixin
26
+
27
+ # --- real payloads, verbatim from the cloud device list --------------------- #
28
+ L2_BATTERY = {"batteryMode": "2", "lowPowerStatus": "0",
29
+ "Battery_remaining": "100", "charging": "0"}
30
+ M3_PRO_MAINS = {"batteryMode": "2"}
31
+ PTZ_MAINS: dict = {}
32
+
33
+
34
+ class _Cam:
35
+ """The real classification code on a minimal object.
36
+
37
+ The methods and properties are taken straight off CameraMixin rather than
38
+ reimplemented, so these tests exercise shipping logic; only the three
39
+ attributes it reads are stubbed.
40
+ """
41
+
42
+ _BATTERY_MODELS = CameraMixin._BATTERY_MODELS
43
+ _battery_evidence = CameraMixin._battery_evidence
44
+ is_battery_camera = CameraMixin.is_battery_camera
45
+ live_power_type = CameraMixin.live_power_type
46
+
47
+ def __init__(self, props, model="LK.IPC.A000088"):
48
+ self._raw_device = {"properties": props}
49
+ self.info = types.SimpleNamespace(model_id=model)
50
+ self.status = None
51
+
52
+
53
+ def _cam(props, model="LK.IPC.A000088"):
54
+ return _Cam(props, model)
55
+
56
+
57
+ # --------------------------------------------------------------------------- #
58
+ # the regression
59
+ # --------------------------------------------------------------------------- #
60
+
61
+ @pytest.mark.parametrize("name", ["M3 Pro_180", "Family Room Cam",
62
+ "Bedroom M3 Pro", "Deck"])
63
+ def test_mains_a000088_reporting_batterymode_is_not_a_battery_camera(name):
64
+ """The shipped bug: four mains cameras classified as battery.
65
+
66
+ They report `batteryMode: '2'` and no other battery field. Trusting the flag
67
+ alone gave them powerType=2 and the battery idle window.
68
+ """
69
+ cam = _cam(M3_PRO_MAINS, model="LK.IPC.A000088")
70
+ assert cam._battery_evidence() is None, name
71
+ assert cam.is_battery_camera is False, name
72
+
73
+
74
+ def test_mains_ptz_with_no_battery_fields_stays_mains():
75
+ cam = _cam(PTZ_MAINS, model="LK.IPC.A001064")
76
+ assert cam.is_battery_camera is False
77
+
78
+
79
+ # --------------------------------------------------------------------------- #
80
+ # the thing that must NOT regress
81
+ # --------------------------------------------------------------------------- #
82
+
83
+ def test_a_real_battery_camera_is_still_detected():
84
+ """A001513 must stay battery - losing this costs it the TURN pre-alloc."""
85
+ cam = _cam(L2_BATTERY, model="LK.IPC.A001513")
86
+ assert cam._battery_evidence() is True
87
+ assert cam.is_battery_camera is True
88
+
89
+
90
+ def test_battery_detection_does_not_lean_on_the_model_list_alone():
91
+ """Evidence must carry it, so an unlisted battery revision still works."""
92
+ cam = _cam(L2_BATTERY, model="LK.IPC.A999999-2")
93
+ assert cam.is_battery_camera is True
94
+
95
+
96
+ def test_a_listed_model_with_no_properties_is_still_battery():
97
+ """The model list is the backstop when the payload carries nothing."""
98
+ cam = _cam({}, model="LK.IPC.A001513")
99
+ assert cam._battery_evidence() is None
100
+ assert cam.is_battery_camera is True
101
+
102
+
103
+ # --------------------------------------------------------------------------- #
104
+ # the corroboration rule itself
105
+ # --------------------------------------------------------------------------- #
106
+
107
+ @pytest.mark.parametrize("corroborating", [
108
+ {"Battery_remaining": "55"},
109
+ {"batteryRemaining": "55"},
110
+ {"batteryLevel": "55"},
111
+ {"lowPowerStatus": "0"},
112
+ {"charging": "1"},
113
+ ])
114
+ def test_batterymode_counts_when_any_battery_field_corroborates(corroborating):
115
+ """The flag is kept as a signal, not discarded - it just needs backing.
116
+
117
+ This is what catches a future battery model whose level field we have not
118
+ seen, provided it reports any battery-only field at all.
119
+ """
120
+ cam = _cam({"batteryMode": "2", **corroborating}, model="LK.IPC.A000088")
121
+ assert cam._battery_evidence() is True
122
+
123
+
124
+ def test_a_zero_valued_battery_field_still_corroborates():
125
+ """`charging: '0'` is a report, not an absence - presence is the signal."""
126
+ cam = _cam({"batteryMode": "2", "charging": "0"}, model="LK.IPC.A000088")
127
+ assert cam._battery_evidence() is True
128
+
129
+
130
+ def test_evidence_never_returns_false_so_it_cannot_demote_a_known_model():
131
+ """It may only ADD to the battery set, never remove - preserved property."""
132
+ for props in (M3_PRO_MAINS, PTZ_MAINS, {"batteryMode": "1"}):
133
+ assert _cam(props)._battery_evidence() in (True, None)
134
+
135
+
136
+ def test_power_type_follows_the_classification():
137
+ """powerType on the wire must never disagree with the battery guards."""
138
+ assert _cam(M3_PRO_MAINS, model="LK.IPC.A000088").live_power_type == 1
139
+ assert _cam(L2_BATTERY, model="LK.IPC.A001513").live_power_type == 2
@@ -279,3 +279,58 @@ def test_media_seen_accepts_dtls_frames(lv):
279
279
  ok, evidence = lv._media_seen(object(), frames=42, out_path=None)
280
280
  assert ok
281
281
  assert evidence["frames"] == 42
282
+
283
+
284
+ # --------------------------------------------------------------------------- #
285
+ # absent-camera early exit
286
+ # --------------------------------------------------------------------------- #
287
+
288
+ def test_slotless_budget_is_smaller_than_the_dtls_budget(lv):
289
+ """The early exit must actually save attempts, or it is decoration."""
290
+ assert lv.SLOTLESS_MAX_ATTEMPTS < lv.ATTEMPTS_DTLS
291
+
292
+
293
+ def test_one_retry_is_still_allowed_for_a_single_missed_response(lv):
294
+ """A camera silent ONCE may just have missed a webrtcResp on a busy account.
295
+
296
+ Dropping to a single attempt would turn one unlucky timeout into a failed
297
+ release, so the budget must be >1.
298
+ """
299
+ assert lv.SLOTLESS_MAX_ATTEMPTS > 1
300
+
301
+
302
+ def test_only_the_no_session_verdict_is_treated_as_absent(lv):
303
+ """ERROR alone means no session was opened, so nothing was probabilistic.
304
+
305
+ NO_MEDIA and BUSY DID reach the camera - those are exactly the flaky cases
306
+ the retry budget exists for, and they must keep their full allowance.
307
+ """
308
+ assert lv._SLOTLESS_VERDICTS == frozenset({"ERROR"})
309
+ for reached_the_camera in ("PASS", "NO_MEDIA", "BUSY"):
310
+ assert reached_the_camera not in lv._SLOTLESS_VERDICTS
311
+
312
+
313
+ def test_a_camera_that_reached_us_once_keeps_its_full_budget(lv):
314
+ """The early exit requires EVERY attempt to be slotless.
315
+
316
+ A camera that returns NO_MEDIA then ERROR is flaky, not absent, and must not
317
+ be cut short - which is why the loop checks all() rather than the last one.
318
+ """
319
+ mixed = [{"verdict": "NO_MEDIA"}, {"verdict": "ERROR"}]
320
+ assert not all(a["verdict"] in lv._SLOTLESS_VERDICTS for a in mixed)
321
+
322
+ absent = [{"verdict": "ERROR"}, {"verdict": "ERROR"}]
323
+ assert all(a["verdict"] in lv._SLOTLESS_VERDICTS for a in absent)
324
+
325
+
326
+ def test_an_absent_camera_still_reaches_a_failing_verdict(lv, tmp_path):
327
+ """Skipping attempts must not soften the verdict - absent is still ERROR.
328
+
329
+ The saving is wall clock, not leniency: a required model with no working
330
+ camera must still fail the release.
331
+ """
332
+ report = {"cameras": [_cam(lv, "LK.IPC.A000088", "ERROR", name="Deck")]}
333
+ report["cameras"][0]["attempts_used"] = lv.SLOTLESS_MAX_ATTEMPTS
334
+ assert lv._summarize(report, _args(tmp_path)) == 1
335
+ assert report["verdict"] == "FAIL"
336
+ assert report["required_failed"] == ["Deck"]