python-aidot-cameras 0.13.0__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.
- {python_aidot_cameras-0.13.0/python_aidot_cameras.egg-info → python_aidot_cameras-0.13.2}/PKG-INFO +1 -1
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/client.py +91 -27
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/pyproject.toml +1 -1
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/python_aidot_cameras.egg-info/SOURCES.txt +2 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_battery_detection.py +30 -3
- python_aidot_cameras-0.13.2/tests/test_battery_evidence_corroboration.py +139 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_live_validate_policy.py +55 -0
- python_aidot_cameras-0.13.2/tests/test_viewer_present_push_mode.py +236 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/LICENSE +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/README.md +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/protocol.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/sdes_open.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/setup.cfg +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_ice_peer_reflexive.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_wake_readiness_retry.py +0 -0
{python_aidot_cameras-0.13.0/python_aidot_cameras.egg-info → python_aidot_cameras-0.13.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.13.
|
|
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
|
|
@@ -854,6 +854,10 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
854
854
|
self._last_frame_time: float = 0.0
|
|
855
855
|
self._keepalive_rtsp_url: Optional[str] = None # local serve URL (go2rtc pulls)
|
|
856
856
|
self._go2rtc_url: Optional[str] = None # go2rtc API base (prefer-go2rtc)
|
|
857
|
+
# Whether WE own this camera's go2rtc stream registration. False when the
|
|
858
|
+
# consumer registers the stream itself and passes the URL for viewer
|
|
859
|
+
# queries only - in which case we must neither add nor REMOVE it.
|
|
860
|
+
self._go2rtc_manages_stream: bool = True
|
|
857
861
|
self._go2rtc_pull_url: Optional[str] = None # go2rtc pull URL once registered
|
|
858
862
|
self._go2rtc_task: "Optional[asyncio.Task[None]]" = None
|
|
859
863
|
# Set by the DTLS serve loop once ffmpeg is bound + serving, so
|
|
@@ -956,15 +960,27 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
956
960
|
for _key in ("Battery_remaining", "batteryRemaining", "batteryLevel"):
|
|
957
961
|
if _as_int(_props.get(_key)) is not None:
|
|
958
962
|
return True
|
|
959
|
-
# The low-power flag
|
|
960
|
-
#
|
|
961
|
-
#
|
|
962
|
-
#
|
|
963
|
-
#
|
|
964
|
-
#
|
|
965
|
-
#
|
|
966
|
-
#
|
|
967
|
-
|
|
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)):
|
|
968
984
|
return True
|
|
969
985
|
# Already-parsed status, for a camera refreshed from an attribute push
|
|
970
986
|
# rather than from a device-list dict.
|
|
@@ -2789,6 +2805,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2789
2805
|
fast_connect: Optional[bool] = None,
|
|
2790
2806
|
sdes_audio: Optional[bool] = None,
|
|
2791
2807
|
go2rtc_url: Optional[str] = None,
|
|
2808
|
+
go2rtc_register: bool = True,
|
|
2792
2809
|
live_stream_param: Optional[bool] = None,
|
|
2793
2810
|
serve_relay: Optional[bool] = None,
|
|
2794
2811
|
stream_idle_s: Optional[float] = None,
|
|
@@ -2828,6 +2845,22 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2828
2845
|
``AIDOT_SERVE_RELAY`` env var (default on); set False to serve ffmpeg
|
|
2829
2846
|
directly on the public port (the pre-0.7.18 behaviour).
|
|
2830
2847
|
|
|
2848
|
+
``go2rtc_url`` points at a go2rtc API base. It does two things: register
|
|
2849
|
+
the local serve with go2rtc (exposing its low-latency pull URL), and let
|
|
2850
|
+
the idle-release logic ask go2rtc **who is watching**.
|
|
2851
|
+
|
|
2852
|
+
``go2rtc_register=False`` keeps the second and drops the first, for a
|
|
2853
|
+
consumer that registers the stream itself - registering it twice
|
|
2854
|
+
re-points the source mid-stream and was observed leaving DTLS cameras
|
|
2855
|
+
producing nothing at all. Such a consumer must still pass ``go2rtc_url``:
|
|
2856
|
+
withholding it does not merely lose a nicety, it silently disables viewer
|
|
2857
|
+
detection, and in SDES **push** mode that is the only viewer signal there
|
|
2858
|
+
is (the serve port is go2rtc's shared RTSP port, where every camera's own
|
|
2859
|
+
publisher is connected, so a socket check would report a viewer for every
|
|
2860
|
+
camera forever). With no signal ``_viewer_present`` answers "unknown",
|
|
2861
|
+
``_idle_release_due`` correctly never releases on unknown, and a battery
|
|
2862
|
+
camera's keepalive renews forever against a camera nobody is watching.
|
|
2863
|
+
|
|
2831
2864
|
``stream_idle_s`` sets the no-viewer idle-release window in seconds,
|
|
2832
2865
|
overriding the ``AIDOT_STREAM_IDLE_S`` env default (120). ``0`` (or
|
|
2833
2866
|
negative) keeps the warm WebRTC session forever so re-views are instant
|
|
@@ -2860,6 +2893,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2860
2893
|
return
|
|
2861
2894
|
self._keepalive_rtsp_url = rtsp_push_url
|
|
2862
2895
|
self._go2rtc_url = go2rtc_url
|
|
2896
|
+
self._go2rtc_manages_stream = bool(go2rtc_register)
|
|
2863
2897
|
self._streaming_active = True
|
|
2864
2898
|
# App parity: keep a battery camera awake for the whole view (mains cams
|
|
2865
2899
|
# never sleep, so this is a no-op there). Single-instance; cancelled in
|
|
@@ -2874,7 +2908,18 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2874
2908
|
# Prefer-go2rtc: if a go2rtc server is configured, register the local
|
|
2875
2909
|
# serve with it once ready and expose its pull URL (ffmpeg fallback
|
|
2876
2910
|
# otherwise). Opt-in: with no go2rtc_url, behaviour is unchanged.
|
|
2877
|
-
|
|
2911
|
+
#
|
|
2912
|
+
# ``go2rtc_register=False`` stores the URL for QUERIES ONLY and skips
|
|
2913
|
+
# registration. That exists for a consumer that registers the stream
|
|
2914
|
+
# itself: registering it twice re-points the source while a stream is
|
|
2915
|
+
# mid-flight, which was observed leaving DTLS cameras producing nothing
|
|
2916
|
+
# at all. Such a consumer previously had to withhold go2rtc_url
|
|
2917
|
+
# entirely, which silently disabled viewer detection - and in SDES push
|
|
2918
|
+
# mode that means a battery camera can never idle-release, because
|
|
2919
|
+
# ``_viewer_present`` answers "unknown" and ``_idle_release_due``
|
|
2920
|
+
# (correctly) never releases on unknown. The keepalive then renews
|
|
2921
|
+
# forever against a camera nobody is watching.
|
|
2922
|
+
if go2rtc_url and go2rtc_register:
|
|
2878
2923
|
self._go2rtc_task = asyncio.ensure_future(self._register_with_go2rtc())
|
|
2879
2924
|
|
|
2880
2925
|
@property
|
|
@@ -2934,9 +2979,17 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2934
2979
|
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_register_with_go2rtc', exc_info=True)
|
|
2935
2980
|
|
|
2936
2981
|
async def _deregister_go2rtc(self) -> None:
|
|
2937
|
-
"""Remove this camera's stream from go2rtc (best-effort).
|
|
2982
|
+
"""Remove this camera's stream from go2rtc (best-effort).
|
|
2983
|
+
|
|
2984
|
+
Only ever removes a registration this client created. With
|
|
2985
|
+
``go2rtc_register=False`` the consumer owns the stream and merely lent us
|
|
2986
|
+
the URL to ask who is watching; deleting its stream on stop or
|
|
2987
|
+
idle-release would tear down the very thing it is serving from.
|
|
2988
|
+
"""
|
|
2938
2989
|
base = self._go2rtc_url
|
|
2939
2990
|
self._go2rtc_pull_url = None
|
|
2991
|
+
if not self._go2rtc_manages_stream:
|
|
2992
|
+
return
|
|
2940
2993
|
# Deliberately NOT gated on `pull`: go2rtc answers the register call with
|
|
2941
2994
|
# 400 when it cannot immediately validate the source (a camera that has
|
|
2942
2995
|
# not started producing yet), yet it still keeps the stream registered.
|
|
@@ -3099,22 +3152,21 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3099
3152
|
|
|
3100
3153
|
name = self._go2rtc_stream_name()
|
|
3101
3154
|
base = self._go2rtc_url
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
return None
|
|
3155
|
+
|
|
3156
|
+
# In PUSH mode the "serve port" is go2rtc's SHARED RTSP port (8554),
|
|
3157
|
+
# where every camera's own publisher is connected - so the socket
|
|
3158
|
+
# fallback below is not merely unavailable, it is WRONG: it would report
|
|
3159
|
+
# a viewer for every camera forever and nothing would ever release.
|
|
3160
|
+
# Tracked separately from `base` because it must gate the fallback even
|
|
3161
|
+
# when go2rtc IS configured but the query fails.
|
|
3162
|
+
push_mode = bool(self._keepalive_rtsp_url) and not str(
|
|
3163
|
+
self._keepalive_rtsp_url).startswith("http")
|
|
3164
|
+
|
|
3165
|
+
# Ask go2rtc when we can. This is the only viewer signal that works in
|
|
3166
|
+
# push mode, which is why a consumer that registers its own stream
|
|
3167
|
+
# should still pass go2rtc_url with go2rtc_register=False rather than
|
|
3168
|
+
# withholding it (withholding it silently pins push-mode cameras to
|
|
3169
|
+
# "unknown", and unknown never releases).
|
|
3118
3170
|
if base and name:
|
|
3119
3171
|
try:
|
|
3120
3172
|
import aiohttp
|
|
@@ -3128,6 +3180,18 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3128
3180
|
return _answer
|
|
3129
3181
|
except Exception as exc:
|
|
3130
3182
|
_LOGGER.debug("go2rtc viewer check failed: %s", exc)
|
|
3183
|
+
|
|
3184
|
+
if push_mode:
|
|
3185
|
+
# Answer "unknown" instead of a confident lie - the caller then holds
|
|
3186
|
+
# the stream rather than tearing down a live view.
|
|
3187
|
+
_LOGGER.debug(
|
|
3188
|
+
"cannot tell whether anyone is watching: the serve is an RTSP "
|
|
3189
|
+
"push to a shared port and go2rtc %s",
|
|
3190
|
+
"did not answer" if base else "was not configured",
|
|
3191
|
+
)
|
|
3192
|
+
self._viewer_cache = (_now_mono, None)
|
|
3193
|
+
return None
|
|
3194
|
+
|
|
3131
3195
|
_answer = self._sdes_serve_consumer_present(port)
|
|
3132
3196
|
self._viewer_cache = (_now_mono, _answer)
|
|
3133
3197
|
return _answer
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-aidot-cameras"
|
|
7
|
-
version = "0.13.
|
|
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"
|
{python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2/python_aidot_cameras.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.13.
|
|
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
|
|
@@ -151,5 +152,6 @@ tests/test_turn_create_permission.py
|
|
|
151
152
|
tests/test_upstream_compat.py
|
|
152
153
|
tests/test_viewer_check_throttle.py
|
|
153
154
|
tests/test_viewer_presence.py
|
|
155
|
+
tests/test_viewer_present_push_mode.py
|
|
154
156
|
tests/test_wait_or_event.py
|
|
155
157
|
tests/test_wake_readiness_retry.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
|
|
95
|
-
|
|
96
|
-
|
|
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
|
{python_aidot_cameras-0.13.0 → python_aidot_cameras-0.13.2}/tests/test_live_validate_policy.py
RENAMED
|
@@ -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"]
|