python-aidot-cameras 1.0.0b2__tar.gz → 1.0.0b4__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-1.0.0b2/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b4}/PKG-INFO +28 -1
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/README.md +27 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/camera/client.py +214 -0
- python_aidot_cameras-1.0.0b4/aidot_cameras/camera/sd_events.py +134 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/camera/sdes_open.py +265 -59
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/pyproject.toml +1 -1
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4/python_aidot_cameras.egg-info}/PKG-INFO +28 -1
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/python_aidot_cameras.egg-info/SOURCES.txt +15 -0
- python_aidot_cameras-1.0.0b4/tests/test_bitrate_campaign_arms.py +113 -0
- python_aidot_cameras-1.0.0b4/tests/test_cloud_count.py +152 -0
- python_aidot_cameras-1.0.0b4/tests/test_cloud_plan.py +164 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_feature_probe_needs_a_live_session.py +28 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_ice_peer_reflexive.py +7 -3
- python_aidot_cameras-1.0.0b4/tests/test_live_session_carries_talk.py +113 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_live_validate_policy.py +4 -1
- python_aidot_cameras-1.0.0b4/tests/test_quality_campaign_in_session.py +237 -0
- python_aidot_cameras-1.0.0b4/tests/test_recent_recordings.py +158 -0
- python_aidot_cameras-1.0.0b4/tests/test_sd_event_decode.py +153 -0
- python_aidot_cameras-1.0.0b4/tests/test_sd_event_probe_payloads.py +108 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_first_media_stall_report.py +17 -9
- python_aidot_cameras-1.0.0b4/tests/test_self_peer_is_an_address_not_an_ip.py +101 -0
- python_aidot_cameras-1.0.0b4/tests/test_stall_report_says_what_the_answer_carried.py +161 -0
- python_aidot_cameras-1.0.0b4/tests/test_stall_report_says_whether_the_trigger_was_answered.py +79 -0
- python_aidot_cameras-1.0.0b4/tests/test_stall_report_separates_silence_from_undecryptable.py +93 -0
- python_aidot_cameras-1.0.0b4/tests/test_stall_report_survives_a_caller_giving_up.py +115 -0
- python_aidot_cameras-1.0.0b4/tests/test_stall_reports_reach_the_artifact.py +95 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/LICENSE +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/MANIFEST.in +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_upstream.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/camera/hwaccel.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/camera/protocol.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/lan_retry.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/setup.cfg +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_answer_sdp_video_pt.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_audio_mux_offline.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_avio_request.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_avio_response_parse.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_avio_response_router.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_battery_evidence_corroboration.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_ble_gateway.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_bridge_state_sentinels.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_busy_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_constants_imports_resolve.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_control_verdicts.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_device_cmd_seq_match.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_dtls_resolution_ack.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_dtls_stdout_producer.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_env_seams.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_futile_keepalive.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_gate_requires_a_decoded_frame.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_hwaccel_probe.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_login_connect_deadline.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_login_reconnect_backoff.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_media_counted_only_when_plaintext.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_media_socket_rcvbuf.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_media_wait_floor.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_no_key_material_in_logs.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_remb.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_resolution_persists.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_restart_cycle.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_scripts_importable.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sctp_init_ack_tsn.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_avio_dispatch.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_offer_video_order.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_offer_video_pin.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_sctp_abort.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_serve_stderr_drain.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_terminal_ack_latency.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdp_narrowing_properties.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_serve_loss_tolerance.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_shared_house_seam.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_snapshot_cancellation.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_speak.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sprop_capture_gate.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sprop_staleness.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_srtp_rx_rekey.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_srtp_tx_key_note.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_talk.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_tutk_path_is_out_of_reach.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_video_profile_log.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_viewer_present_push_mode.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_wake_gate_camera_evidence.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_wake_readiness_retry.py +0 -0
- {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/tests/test_webrtc_answer_race.py +0 -0
{python_aidot_cameras-1.0.0b2/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b4}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0b4
|
|
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
|
|
@@ -66,6 +66,33 @@ Other battery models (A001108, A001360) are recognized in code with the same
|
|
|
66
66
|
battery handling. See [`docs/CAMERAS.md`](docs/CAMERAS.md#supported-cameras) for
|
|
67
67
|
the authoritative table and per-model notes.
|
|
68
68
|
|
|
69
|
+
## Known characteristics
|
|
70
|
+
|
|
71
|
+
Two behaviours that are measured, understood as far as the evidence allows, and
|
|
72
|
+
not going to change in this release. Neither is a fault to report.
|
|
73
|
+
|
|
74
|
+
**Recorded video is retrievable only from cloud storage.** Whether a camera
|
|
75
|
+
records to the cloud or to its own SD card is per-camera, not per-model, and the
|
|
76
|
+
device reports it as `IsSupportPlayback`. This library retrieves cloud
|
|
77
|
+
recordings (`async_get_cloud_recordings`, `async_open_cloud_playback`) and has
|
|
78
|
+
no SD-card equivalent, so a camera storing to a card records normally and
|
|
79
|
+
nothing here can play it back. On the reference fleet that is four of seven
|
|
80
|
+
cameras. The vendor's own commands for it are identified but their responses
|
|
81
|
+
have never been decoded; see `docs/ROAD-TO-1.0.md` item 6.
|
|
82
|
+
|
|
83
|
+
**The A001064 (PTZ) streams at roughly 2.2 Mbps, about 24% above the vendor
|
|
84
|
+
app.** Measured from a capture of the app on the same camera: it takes about
|
|
85
|
+
1796 Kbps on HD and about 866 Kbps on SD. An earlier version of this note
|
|
86
|
+
repeated a figure of 225-500 Kbps for the app - that was never measured here,
|
|
87
|
+
and the capture contradicts it at both quality settings.
|
|
88
|
+
|
|
89
|
+
So the practical difference is a quarter more bandwidth at equivalent quality,
|
|
90
|
+
plus a roughly 2:1 saving that the app's SD control achieves and this library's
|
|
91
|
+
does not. The command sent here is byte-for-byte the app's, in-band and
|
|
92
|
+
mid-session on the same transport, and it does not change the rate; the app's
|
|
93
|
+
does. Streaming is correct either way and the cost is bandwidth, which matters
|
|
94
|
+
on a metered or congested link.
|
|
95
|
+
|
|
69
96
|
## Local (LAN) control and account ownership
|
|
70
97
|
|
|
71
98
|
Local control over the devices' TCP:10000 channel - both the light/plug protocol
|
|
@@ -35,6 +35,33 @@ Other battery models (A001108, A001360) are recognized in code with the same
|
|
|
35
35
|
battery handling. See [`docs/CAMERAS.md`](docs/CAMERAS.md#supported-cameras) for
|
|
36
36
|
the authoritative table and per-model notes.
|
|
37
37
|
|
|
38
|
+
## Known characteristics
|
|
39
|
+
|
|
40
|
+
Two behaviours that are measured, understood as far as the evidence allows, and
|
|
41
|
+
not going to change in this release. Neither is a fault to report.
|
|
42
|
+
|
|
43
|
+
**Recorded video is retrievable only from cloud storage.** Whether a camera
|
|
44
|
+
records to the cloud or to its own SD card is per-camera, not per-model, and the
|
|
45
|
+
device reports it as `IsSupportPlayback`. This library retrieves cloud
|
|
46
|
+
recordings (`async_get_cloud_recordings`, `async_open_cloud_playback`) and has
|
|
47
|
+
no SD-card equivalent, so a camera storing to a card records normally and
|
|
48
|
+
nothing here can play it back. On the reference fleet that is four of seven
|
|
49
|
+
cameras. The vendor's own commands for it are identified but their responses
|
|
50
|
+
have never been decoded; see `docs/ROAD-TO-1.0.md` item 6.
|
|
51
|
+
|
|
52
|
+
**The A001064 (PTZ) streams at roughly 2.2 Mbps, about 24% above the vendor
|
|
53
|
+
app.** Measured from a capture of the app on the same camera: it takes about
|
|
54
|
+
1796 Kbps on HD and about 866 Kbps on SD. An earlier version of this note
|
|
55
|
+
repeated a figure of 225-500 Kbps for the app - that was never measured here,
|
|
56
|
+
and the capture contradicts it at both quality settings.
|
|
57
|
+
|
|
58
|
+
So the practical difference is a quarter more bandwidth at equivalent quality,
|
|
59
|
+
plus a roughly 2:1 saving that the app's SD control achieves and this library's
|
|
60
|
+
does not. The command sent here is byte-for-byte the app's, in-band and
|
|
61
|
+
mid-session on the same transport, and it does not change the rate; the app's
|
|
62
|
+
does. Streaming is correct either way and the cost is bandwidth, which matters
|
|
63
|
+
on a metered or congested link.
|
|
64
|
+
|
|
38
65
|
## Local (LAN) control and account ownership
|
|
39
66
|
|
|
40
67
|
Local control over the devices' TCP:10000 channel - both the light/plug protocol
|
{python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b4}/aidot_cameras/camera/client.py
RENAMED
|
@@ -2391,6 +2391,88 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2391
2391
|
)
|
|
2392
2392
|
return None
|
|
2393
2393
|
|
|
2394
|
+
async def async_get_recent_recordings(self, total: int = 10) -> "List[dict]":
|
|
2395
|
+
"""The camera's most recent cloud event recordings.
|
|
2396
|
+
|
|
2397
|
+
A second listing API, and the one the vendor app actually uses on its
|
|
2398
|
+
event page. Captured 2026-04-05:
|
|
2399
|
+
|
|
2400
|
+
POST /api/ipc/playback/getRecentEventRecordingList
|
|
2401
|
+
{"deviceIds": [...], "total": 3}
|
|
2402
|
+
|
|
2403
|
+
**Both listings work; the zeros were the account** (measured
|
|
2404
|
+
2026-08-11). Every fleet run had reported zero events from every camera
|
|
2405
|
+
through this call AND through `async_get_cloud_recordings`, which read
|
|
2406
|
+
as two broken listings. On the OWNER account, on the same fleet, ninety
|
|
2407
|
+
minutes after a run that reported zero: this call returns the full count
|
|
2408
|
+
it asks for on six of seven cameras, and the range query returns a full
|
|
2409
|
+
first page against server totals of 121-1517 over thirty days. The
|
|
2410
|
+
seventh camera has no events by any method.
|
|
2411
|
+
|
|
2412
|
+
The fleet runs use a shared-home member. The same runs report no cloud
|
|
2413
|
+
thumbnail either, and the owner/member thumbnail split was measured
|
|
2414
|
+
separately on 2026-08-09 - so an account seeing neither is behaving as
|
|
2415
|
+
the member, not meeting a defect. Read a zero from this method as a
|
|
2416
|
+
question about the account before reading it as a fault.
|
|
2417
|
+
|
|
2418
|
+
Two shape differences from every other call in this class, and both
|
|
2419
|
+
would break the usual handling:
|
|
2420
|
+
|
|
2421
|
+
* the response is a BARE JSON ARRAY, not the ``{"code": ..., "data":
|
|
2422
|
+
...}`` envelope, so there is no code to check and no ``data`` to
|
|
2423
|
+
unwrap;
|
|
2424
|
+
* there is no pagination - ``total`` is "how many do you want", not a
|
|
2425
|
+
page size.
|
|
2426
|
+
|
|
2427
|
+
Each item carries ``eventUuid``, ``eventTime`` (epoch ms), ``deviceId``,
|
|
2428
|
+
``deviceName``, ``eventCodeList``, ``eventDescList`` and ``picUrl``.
|
|
2429
|
+
Filtered to THIS camera, because the app asks for a whole house at once
|
|
2430
|
+
and the reply is not per-device.
|
|
2431
|
+
"""
|
|
2432
|
+
import aiohttp
|
|
2433
|
+
|
|
2434
|
+
body = {"deviceIds": [self.device_id], "total": max(1, int(total))}
|
|
2435
|
+
|
|
2436
|
+
async def _fetch():
|
|
2437
|
+
async with aiohttp.ClientSession() as session:
|
|
2438
|
+
async with session.post(
|
|
2439
|
+
f"{self._aidot_v32_base}/playback/getRecentEventRecordingList",
|
|
2440
|
+
json=body,
|
|
2441
|
+
headers=self._aidot_headers(),
|
|
2442
|
+
timeout=aiohttp.ClientTimeout(total=30),
|
|
2443
|
+
) as resp:
|
|
2444
|
+
return await resp.json(content_type=None)
|
|
2445
|
+
|
|
2446
|
+
try:
|
|
2447
|
+
data = await _fetch()
|
|
2448
|
+
# The auth check reads a dict; a list is already a success here.
|
|
2449
|
+
if isinstance(data, dict):
|
|
2450
|
+
if self._is_auth_error(data) and await self._async_refresh_auth_token():
|
|
2451
|
+
data = await _fetch()
|
|
2452
|
+
if isinstance(data, dict):
|
|
2453
|
+
# An envelope means an error, since success is a bare array.
|
|
2454
|
+
_LOGGER.warning(
|
|
2455
|
+
"getRecentEventRecordingList code=%s msg=%s for %s",
|
|
2456
|
+
data.get("code"), data.get("desc") or data.get("msg"),
|
|
2457
|
+
self.device_id,
|
|
2458
|
+
)
|
|
2459
|
+
return []
|
|
2460
|
+
if not isinstance(data, list):
|
|
2461
|
+
return []
|
|
2462
|
+
mine = [e for e in data
|
|
2463
|
+
if isinstance(e, dict)
|
|
2464
|
+
and e.get("deviceId") in (None, self.device_id)]
|
|
2465
|
+
_LOGGER.info(
|
|
2466
|
+
"getRecentEventRecordingList for %s: %d of %d event(s)",
|
|
2467
|
+
self.device_id, len(mine), len(data),
|
|
2468
|
+
)
|
|
2469
|
+
return mine
|
|
2470
|
+
except Exception as exc:
|
|
2471
|
+
_LOGGER.warning(
|
|
2472
|
+
"getRecentEventRecordingList failed for %s: %s",
|
|
2473
|
+
self.device_id, exc)
|
|
2474
|
+
return []
|
|
2475
|
+
|
|
2394
2476
|
async def async_get_cloud_recordings(
|
|
2395
2477
|
self,
|
|
2396
2478
|
start_ts: int,
|
|
@@ -2403,6 +2485,14 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2403
2485
|
|
|
2404
2486
|
start_ts / end_ts: Unix timestamps in milliseconds.
|
|
2405
2487
|
page: 1-indexed page number (the server uses 1-based pagination).
|
|
2488
|
+
The server caps a page at 10 items whatever ``page_size`` says -
|
|
2489
|
+
measured 2026-08-11 across seven cameras, and the vendor's own client
|
|
2490
|
+
hard-overwrites the field to 10 (5 for a multi-device request), so it
|
|
2491
|
+
is the server's rule rather than a quirk of one account. A caller that
|
|
2492
|
+
asks for 30 gets 10 and no indication that it was truncated, which is
|
|
2493
|
+
how the Home Assistant browser came to show 10 events out of 1517.
|
|
2494
|
+
Page; do not raise ``page_size``. ``data.total`` in the reply is the
|
|
2495
|
+
true count for the window, and is correct even on a one-item page.
|
|
2406
2496
|
Returns list of dicts from the server (each has eventUuid, eventDesc, picUrl, etc.).
|
|
2407
2497
|
|
|
2408
2498
|
Uses /api/ipc/playback/eventRecordingList (confirmed from EventListRepos.java).
|
|
@@ -2456,6 +2546,106 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2456
2546
|
)
|
|
2457
2547
|
return []
|
|
2458
2548
|
|
|
2549
|
+
async def async_count_cloud_recordings(
|
|
2550
|
+
self, start_ts: int, end_ts: int
|
|
2551
|
+
) -> Optional[int]:
|
|
2552
|
+
"""How many cloud events a window holds, in one request.
|
|
2553
|
+
|
|
2554
|
+
The listing endpoint reports the window's true ``total`` alongside
|
|
2555
|
+
whatever page it serves, and that total is correct even on a one-item
|
|
2556
|
+
page - measured 2026-08-11 across seven cameras. So a count costs one
|
|
2557
|
+
request, where counting by paging costs one per ten events: a day
|
|
2558
|
+
holding 1517 events is 152 requests, and a seven-day view that counts
|
|
2559
|
+
by paging fires hundreds every time it is opened.
|
|
2560
|
+
|
|
2561
|
+
Returns None when the call fails. A caller must not read that as zero -
|
|
2562
|
+
"the server did not answer" and "the window is empty" lead to opposite
|
|
2563
|
+
conclusions in a UI, and collapsing them is a defect this project has
|
|
2564
|
+
had to fix before.
|
|
2565
|
+
"""
|
|
2566
|
+
import aiohttp
|
|
2567
|
+
|
|
2568
|
+
_body = {
|
|
2569
|
+
"deviceIds": [self.device_id],
|
|
2570
|
+
"pageNum": 1,
|
|
2571
|
+
"pageSize": 1,
|
|
2572
|
+
"recordSta": start_ts,
|
|
2573
|
+
"recordEnd": end_ts,
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
async def _fetch():
|
|
2577
|
+
async with aiohttp.ClientSession() as session:
|
|
2578
|
+
async with session.post(
|
|
2579
|
+
f"{self._aidot_v32_base}/playback/eventRecordingList",
|
|
2580
|
+
json=_body,
|
|
2581
|
+
headers=self._aidot_headers(),
|
|
2582
|
+
timeout=aiohttp.ClientTimeout(total=30),
|
|
2583
|
+
) as resp:
|
|
2584
|
+
return await resp.json(content_type=None)
|
|
2585
|
+
|
|
2586
|
+
try:
|
|
2587
|
+
body = await _fetch()
|
|
2588
|
+
if self._is_auth_error(body) and await self._async_refresh_auth_token():
|
|
2589
|
+
body = await _fetch()
|
|
2590
|
+
if not isinstance(body, dict) or body.get("code") != 200:
|
|
2591
|
+
_LOGGER.debug(
|
|
2592
|
+
"eventRecordingList count for %s: %s", self.device_id, body)
|
|
2593
|
+
return None
|
|
2594
|
+
total = (body.get("data") or {}).get("total")
|
|
2595
|
+
return int(total) if isinstance(total, int) else None
|
|
2596
|
+
except Exception as exc:
|
|
2597
|
+
_LOGGER.debug(
|
|
2598
|
+
"eventRecordingList count failed for %s: %s", self.device_id, exc)
|
|
2599
|
+
return None
|
|
2600
|
+
|
|
2601
|
+
async def async_get_cloud_plan(self) -> Optional[dict]:
|
|
2602
|
+
"""The camera's cloud recording subscription, or None.
|
|
2603
|
+
|
|
2604
|
+
Answers the question an empty event list cannot: whether there are no
|
|
2605
|
+
events, or no longer a plan under which events would be kept. Measured
|
|
2606
|
+
2026-08-11 on seven cameras - an active plan reads
|
|
2607
|
+
|
|
2608
|
+
{"packageName": "AI Protection ", "packageType": 1,
|
|
2609
|
+
"subscribeStatus": 1, "startTime": <ms>, "endTime": <ms>,
|
|
2610
|
+
"expiredDays": 15, "videoLength": 60}
|
|
2611
|
+
|
|
2612
|
+
Times are epoch milliseconds. ``videoLength`` is the clip length in
|
|
2613
|
+
seconds.
|
|
2614
|
+
|
|
2615
|
+
Returns None on any failure rather than an empty dict: the caller's
|
|
2616
|
+
question is "is there a plan", and {} answers it wrong.
|
|
2617
|
+
|
|
2618
|
+
Source: the vendor app's own JS bundle -
|
|
2619
|
+
GET /api/ipc/recordPlanController/getPackageInfoByDevId?deviceId=
|
|
2620
|
+
"""
|
|
2621
|
+
import aiohttp
|
|
2622
|
+
|
|
2623
|
+
async def _fetch():
|
|
2624
|
+
async with aiohttp.ClientSession() as session:
|
|
2625
|
+
async with session.get(
|
|
2626
|
+
f"{self._aidot_v32_base}/recordPlanController"
|
|
2627
|
+
"/getPackageInfoByDevId",
|
|
2628
|
+
params={"deviceId": self.device_id},
|
|
2629
|
+
headers=self._aidot_headers(),
|
|
2630
|
+
timeout=aiohttp.ClientTimeout(total=15),
|
|
2631
|
+
) as resp:
|
|
2632
|
+
return await resp.json(content_type=None)
|
|
2633
|
+
|
|
2634
|
+
try:
|
|
2635
|
+
body = await _fetch()
|
|
2636
|
+
if self._is_auth_error(body) and await self._async_refresh_auth_token():
|
|
2637
|
+
body = await _fetch()
|
|
2638
|
+
if not isinstance(body, dict) or body.get("code") != 200:
|
|
2639
|
+
_LOGGER.debug(
|
|
2640
|
+
"getPackageInfoByDevId for %s: %s", self.device_id, body)
|
|
2641
|
+
return None
|
|
2642
|
+
data = body.get("data")
|
|
2643
|
+
return data if isinstance(data, dict) and data else None
|
|
2644
|
+
except Exception as exc:
|
|
2645
|
+
_LOGGER.debug(
|
|
2646
|
+
"getPackageInfoByDevId failed for %s: %s", self.device_id, exc)
|
|
2647
|
+
return None
|
|
2648
|
+
|
|
2459
2649
|
async def async_get_event_video_media(
|
|
2460
2650
|
self, event_uuid: str
|
|
2461
2651
|
) -> Optional[tuple[str, str]]:
|
|
@@ -2465,6 +2655,14 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2465
2655
|
most browsers handle direct MP4 more reliably than HLS with signed CDN
|
|
2466
2656
|
segments. Falls back to whatever is available.
|
|
2467
2657
|
|
|
2658
|
+
**That preference has never once been exercised.** Measured 2026-08-11
|
|
2659
|
+
on the owner account, three cameras across three model families
|
|
2660
|
+
(A001064, A001513, A000088): every reply carried exactly ONE entry, and
|
|
2661
|
+
it was type 2. So playback here is HLS in practice, and anything built
|
|
2662
|
+
on it should be designed for HLS rather than for the MP4 this docstring
|
|
2663
|
+
would lead a reader to expect. The branch is kept because it costs
|
|
2664
|
+
nothing and the field exists in the app's own decoder.
|
|
2665
|
+
|
|
2468
2666
|
From CloudPlaybackFragment.java / EventListRepos.java:
|
|
2469
2667
|
POST /api/ipc/playback/getEventVideoUrl
|
|
2470
2668
|
body: {deviceId, eventList: [{eventUuid}]}
|
|
@@ -3477,6 +3675,22 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3477
3675
|
rtsp_push_url=self._keepalive_rtsp_url,
|
|
3478
3676
|
timeout=(_FAST_OPEN_TIMEOUT if _use_fast else 120.0),
|
|
3479
3677
|
reuse_peer_id=_loop_peer_id,
|
|
3678
|
+
# Ask for talk so THIS session can carry it. async_speak
|
|
3679
|
+
# reuses _stream_session only when it is talk-capable, and
|
|
3680
|
+
# an SDES session that did not ask never gets a return audio
|
|
3681
|
+
# track - so without this every press-to-talk opened a
|
|
3682
|
+
# second camera session alongside the live view, and the
|
|
3683
|
+
# camera holds a viewer slot for about 120 s after one ends.
|
|
3684
|
+
#
|
|
3685
|
+
# This changes the SDP offer on the path every SDES live
|
|
3686
|
+
# view takes, which is the kind of change that has caused
|
|
3687
|
+
# fleet-wide blackouts here, so it rests on evidence rather
|
|
3688
|
+
# than reasoning: the release harness has opened all seven
|
|
3689
|
+
# cameras talk-capable across three fleet runs with no
|
|
3690
|
+
# streaming regression. The flag reaches only the offer
|
|
3691
|
+
# (audio sendrecv + a=ssrc) and the shared talk state; it
|
|
3692
|
+
# does not touch the rtsp_push branch this loop uses.
|
|
3693
|
+
talk=True,
|
|
3480
3694
|
)
|
|
3481
3695
|
except asyncio.CancelledError:
|
|
3482
3696
|
self._fast_attempt_override = None
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Decode the camera's recording-list reply (``LISTEVENT_RESP``, 0x319).
|
|
2
|
+
|
|
3
|
+
Written from the vendor client's own decoder rather than from a capture, so it
|
|
4
|
+
exists before any camera here has answered. What is CONFIRMED from
|
|
5
|
+
``KVSWebRTCChannel``'s 0x319 handler:
|
|
6
|
+
|
|
7
|
+
* the reply is matched by the SEQUENCE NUMBER of the request, not by the
|
|
8
|
+
command alone - the handler's first act is ``if getListSeq != seq: return``;
|
|
9
|
+
* a four-byte little-endian value is assembled and named ``listEventTotalTime``
|
|
10
|
+
(its name says time, not count, and this module does not pretend otherwise);
|
|
11
|
+
* a single byte is read as ``listEventEndFlag`` - the reply is PAGED, and a
|
|
12
|
+
caller that stops at the first packet gets a truncated list;
|
|
13
|
+
* a single byte is read as ``listEventCount`` - how many records this packet
|
|
14
|
+
carries;
|
|
15
|
+
* records are **twelve bytes each**, copied out with an explicit
|
|
16
|
+
``arraycopy(..., 12)``, and each begins with a two-byte year read via
|
|
17
|
+
``byteToShort`` - an ``STimeDay``.
|
|
18
|
+
|
|
19
|
+
**The header is now measured, not inferred (2026-08-11, run 31497241870).** The
|
|
20
|
+
first version of this module used the published TUTK layout - two STimeDays then
|
|
21
|
+
the counters, 24 bytes - and refused to decode the real replies because they
|
|
22
|
+
were shorter than that. Refusing was correct, and it is how the true layout came
|
|
23
|
+
back readable instead of being forced into the wrong one.
|
|
24
|
+
|
|
25
|
+
Two live replies, and a 12-byte header fits both exactly:
|
|
26
|
+
|
|
27
|
+
HASLISTEVENT_RESP 00000000 01000000 0001a800 + 168 bytes
|
|
28
|
+
LISTEVENT_RESP 00000000 01000000 00010000 + 0 bytes
|
|
29
|
+
|
|
30
|
+
channel uint32 LE total uint32 LE index end_flag count reserved
|
|
31
|
+
|
|
32
|
+
`count` equals the body length in both - 168 and 0. And 168 is exactly the
|
|
33
|
+
7-day range that was requested, one byte per hour, which is what HASLISTEVENT
|
|
34
|
+
means: a per-hour occupancy map rather than a list. LISTEVENT carries 12-byte
|
|
35
|
+
records in the same body.
|
|
36
|
+
"""
|
|
37
|
+
import struct
|
|
38
|
+
from typing import List, NamedTuple, Optional
|
|
39
|
+
|
|
40
|
+
#: One record. The size is confirmed from the vendor's own arraycopy.
|
|
41
|
+
EVENT_RECORD_LEN = 12
|
|
42
|
+
|
|
43
|
+
#: Measured from two live replies, not taken from the published struct - the
|
|
44
|
+
#: published one is 24 bytes and neither reply is that long.
|
|
45
|
+
_HEADER_LEN = 12
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SdEvent(NamedTuple):
|
|
49
|
+
"""One recording the camera says it holds."""
|
|
50
|
+
|
|
51
|
+
#: UTC, from the record's STimeDay.
|
|
52
|
+
year: int
|
|
53
|
+
month: int
|
|
54
|
+
day: int
|
|
55
|
+
hour: int
|
|
56
|
+
minute: int
|
|
57
|
+
second: int
|
|
58
|
+
channel: int
|
|
59
|
+
event: int
|
|
60
|
+
status: int
|
|
61
|
+
|
|
62
|
+
def isoformat(self) -> str:
|
|
63
|
+
return (f"{self.year:04d}-{self.month:02d}-{self.day:02d}"
|
|
64
|
+
f"T{self.hour:02d}:{self.minute:02d}:{self.second:02d}Z")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SdEventPage(NamedTuple):
|
|
68
|
+
"""One packet of the paged reply.
|
|
69
|
+
|
|
70
|
+
``end_flag`` is why this is a page and not a list: the camera answers a
|
|
71
|
+
range in several packets and a caller that reads only the first silently
|
|
72
|
+
truncates the recording list. Nothing here hides that.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
events: List[SdEvent]
|
|
76
|
+
channel: int
|
|
77
|
+
total: int
|
|
78
|
+
#: Named page_index / record_count, not index / count: a NamedTuple field
|
|
79
|
+
#: called `index` or `count` shadows tuple.index() and tuple.count(), so
|
|
80
|
+
#: `page.count` would stop being callable and read as a number instead.
|
|
81
|
+
page_index: int
|
|
82
|
+
end_flag: int
|
|
83
|
+
record_count: int
|
|
84
|
+
#: True when the payload length matches the count the header declared.
|
|
85
|
+
consistent: bool
|
|
86
|
+
#: Bytes left over after the declared records - non-zero means the layout
|
|
87
|
+
#: read here is wrong, and says so rather than dropping them.
|
|
88
|
+
trailing: int
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _stimeday(buf: bytes, off: int) -> tuple:
|
|
92
|
+
year, month, day, _wday, hour, minute, second = struct.unpack_from(
|
|
93
|
+
"<HBBBBBB", buf, off)
|
|
94
|
+
return year, month, day, hour, minute, second
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def decode_event_record(buf: bytes, off: int = 0) -> SdEvent:
|
|
98
|
+
"""One 12-byte record: an STimeDay then channel, event, status, reserved."""
|
|
99
|
+
year, month, day, hour, minute, second = _stimeday(buf, off)
|
|
100
|
+
channel, event, status = struct.unpack_from("<BBB", buf, off + 8)
|
|
101
|
+
return SdEvent(year, month, day, hour, minute, second,
|
|
102
|
+
channel, event, status)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def decode_list_event_response(payload: bytes) -> Optional[SdEventPage]:
|
|
106
|
+
"""Decode a 0x319 payload, or None if it cannot be one.
|
|
107
|
+
|
|
108
|
+
None rather than an exception or a half-filled result: this runs against a
|
|
109
|
+
reply nobody has seen yet, and "this does not fit" is the answer that keeps
|
|
110
|
+
the next person honest.
|
|
111
|
+
"""
|
|
112
|
+
if payload is None or len(payload) < _HEADER_LEN:
|
|
113
|
+
return None
|
|
114
|
+
channel, total = struct.unpack_from("<II", payload, 0)
|
|
115
|
+
index, end_flag, count, _reserved = struct.unpack_from("<BBBB", payload, 8)
|
|
116
|
+
body = payload[_HEADER_LEN:]
|
|
117
|
+
usable = len(body) // EVENT_RECORD_LEN
|
|
118
|
+
# `count` is a BYTE count on HASLISTEVENT (one byte per hour) and a RECORD
|
|
119
|
+
# count on LISTEVENT. Only the record reading is meaningful here, so the
|
|
120
|
+
# number of records decoded is bounded by what the body can actually hold
|
|
121
|
+
# rather than by what the header claims.
|
|
122
|
+
take = usable
|
|
123
|
+
events = [decode_event_record(body, i * EVENT_RECORD_LEN)
|
|
124
|
+
for i in range(take)]
|
|
125
|
+
return SdEventPage(
|
|
126
|
+
events=events,
|
|
127
|
+
channel=channel,
|
|
128
|
+
total=total,
|
|
129
|
+
page_index=index,
|
|
130
|
+
end_flag=end_flag,
|
|
131
|
+
record_count=count,
|
|
132
|
+
consistent=(count == len(body)),
|
|
133
|
+
trailing=len(body) - usable * EVENT_RECORD_LEN,
|
|
134
|
+
)
|