python-aidot-cameras 0.12.16__tar.gz → 0.13.0__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.12.16/python_aidot_cameras.egg-info → python_aidot_cameras-0.13.0}/PKG-INFO +4 -4
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/README.md +2 -2
- python_aidot_cameras-0.13.0/aidot_cameras/_upstream.py +396 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/client.py +253 -20
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/constants.py +8 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/sdes_open.py +588 -24
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/webrtc_open.py +65 -30
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/client.py +243 -65
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/const.py +16 -3
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/crypto.py +12 -6
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/device_client.py +90 -24
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/discover.py +38 -14
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/pyproject.toml +17 -5
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0/python_aidot_cameras.egg-info}/PKG-INFO +4 -4
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/python_aidot_cameras.egg-info/SOURCES.txt +9 -1
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/python_aidot_cameras.egg-info/requires.txt +1 -1
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_aioice_compat.py +2 -2
- python_aidot_cameras-0.13.0/tests/test_battery_detection.py +206 -0
- python_aidot_cameras-0.13.0/tests/test_constants_imports_resolve.py +62 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_device_login_guard.py +3 -3
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_dtls_mux_terminal_write.py +2 -1
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_egress_guard.py +3 -3
- python_aidot_cameras-0.13.0/tests/test_ice_peer_reflexive.py +92 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_light_active_color_mode.py +4 -6
- python_aidot_cameras-0.13.0/tests/test_live_stream_param.py +145 -0
- python_aidot_cameras-0.13.0/tests/test_live_validate_policy.py +281 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_no_self_referential_go2rtc.py +2 -2
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_post_merge_hardening.py +37 -20
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_session_stats.py +3 -3
- python_aidot_cameras-0.13.0/tests/test_shared_house_seam.py +98 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_token_refresh.py +34 -20
- python_aidot_cameras-0.13.0/tests/test_turn_create_permission.py +291 -0
- python_aidot_cameras-0.13.0/tests/test_upstream_compat.py +568 -0
- python_aidot_cameras-0.13.0/tests/test_wake_readiness_retry.py +167 -0
- python_aidot_cameras-0.12.16/tests/test_live_stream_param.py +0 -123
- python_aidot_cameras-0.12.16/tests/test_upstream_compat.py +0 -268
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/LICENSE +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/protocol.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/setup.cfg +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-0.12.16 → python_aidot_cameras-0.13.0}/tests/test_wait_or_event.py +0 -0
{python_aidot_cameras-0.12.16/python_aidot_cameras.egg-info → python_aidot_cameras-0.13.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
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
|
|
@@ -11,7 +11,7 @@ Classifier: Operating System :: OS Independent
|
|
|
11
11
|
Requires-Python: >=3.11
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist: python-aidot
|
|
14
|
+
Requires-Dist: python-aidot<0.4,>=0.3.55
|
|
15
15
|
Requires-Dist: aiohttp>=3.9
|
|
16
16
|
Requires-Dist: paho-mqtt>=1.6.1
|
|
17
17
|
Requires-Dist: cryptography>=42.0
|
|
@@ -171,12 +171,12 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
171
171
|
| `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
|
|
172
172
|
| `AIDOT_FAST_CONNECT` | Enable LAN-direct "fast connect" (STUN-only, skips several cloud signaling waits) when truthy. On-LAN only - off-subnet/strict-NAT viewers must leave it off. | unset (off) |
|
|
173
173
|
| `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2-3 s of cold-start latency) so signaling goes straight out with the host candidate. Faster on a LAN, at the cost of no relay fallback for a camera on a different segment / behind strict NAT. Experimental, opt-in (truthy = `1`/`true`/`yes`/`on`). | unset (off) |
|
|
174
|
-
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. | unset (off) |
|
|
174
|
+
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
|
|
175
175
|
| `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
176
176
|
| `AIDOT_DTLS_FAST_LIVEPLAY` | The DTLS (A000088) analogue: skip the `livePlayReq`-echo and `livePlayResp` waits (the dominant LAN cold-start cost) while keeping the full ICE/TURN/DTLS handshake, so remote/relay viewing is unaffected. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
177
177
|
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for commands, attribute fetches, and stream-open signaling (matching the official app) instead of connecting per operation. **On by default** (live soaks cut SDES NO_MEDIA from ~57% to ~11-19%); set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
178
178
|
| `AIDOT_SERVE_RELAY` | Hold the public stream port via an internal relay that proxies to ffmpeg, so the first (cold) view connects instead of failing while ffmpeg can't pre-bind the port. Set to `0` to serve ffmpeg directly. | `1` (enabled) |
|
|
179
|
-
| `AIDOT_LIVESTREAM_PARAM` |
|
|
179
|
+
| `AIDOT_LIVESTREAM_PARAM` | **No-op (ignored).** Formerly requested the cloud `liveStreamParam` pre-connect for battery cameras. That call provisions the camera toward AWS KVS, so it sends its media there instead of to this library and the live view negotiates and then shows nothing - and battery cameras were the only ones it applied to. Setting it (or `start_keepalive(live_stream_param=True)`) logs one warning and changes nothing. | ignored |
|
|
180
180
|
|
|
181
181
|
### Security hardening
|
|
182
182
|
|
|
@@ -140,12 +140,12 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
140
140
|
| `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
|
|
141
141
|
| `AIDOT_FAST_CONNECT` | Enable LAN-direct "fast connect" (STUN-only, skips several cloud signaling waits) when truthy. On-LAN only - off-subnet/strict-NAT viewers must leave it off. | unset (off) |
|
|
142
142
|
| `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2-3 s of cold-start latency) so signaling goes straight out with the host candidate. Faster on a LAN, at the cost of no relay fallback for a camera on a different segment / behind strict NAT. Experimental, opt-in (truthy = `1`/`true`/`yes`/`on`). | unset (off) |
|
|
143
|
-
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. | unset (off) |
|
|
143
|
+
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
|
|
144
144
|
| `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
145
145
|
| `AIDOT_DTLS_FAST_LIVEPLAY` | The DTLS (A000088) analogue: skip the `livePlayReq`-echo and `livePlayResp` waits (the dominant LAN cold-start cost) while keeping the full ICE/TURN/DTLS handshake, so remote/relay viewing is unaffected. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
146
146
|
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for commands, attribute fetches, and stream-open signaling (matching the official app) instead of connecting per operation. **On by default** (live soaks cut SDES NO_MEDIA from ~57% to ~11-19%); set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
147
147
|
| `AIDOT_SERVE_RELAY` | Hold the public stream port via an internal relay that proxies to ffmpeg, so the first (cold) view connects instead of failing while ffmpeg can't pre-bind the port. Set to `0` to serve ffmpeg directly. | `1` (enabled) |
|
|
148
|
-
| `AIDOT_LIVESTREAM_PARAM` |
|
|
148
|
+
| `AIDOT_LIVESTREAM_PARAM` | **No-op (ignored).** Formerly requested the cloud `liveStreamParam` pre-connect for battery cameras. That call provisions the camera toward AWS KVS, so it sends its media there instead of to this library and the live view negotiates and then shows nothing - and battery cameras were the only ones it applied to. Setting it (or `start_keepalive(live_stream_param=True)`) logs one warning and changes nothing. | ignored |
|
|
149
149
|
|
|
150
150
|
### Security hardening
|
|
151
151
|
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""One place that knows which upstream ``python-aidot`` is installed.
|
|
2
|
+
|
|
3
|
+
Upstream ships two incompatible shapes of the same private API, and both are
|
|
4
|
+
live: Home Assistant core pins ``0.3.56`` while this library was written against
|
|
5
|
+
``0.3.55``. They are not a forward/backward pair - ``0.3.56`` is a **revert** of
|
|
6
|
+
the refactor that ``0.3.54`` introduced and ``0.3.55`` carried, so the "old"
|
|
7
|
+
shape is also the current one:
|
|
8
|
+
|
|
9
|
+
0.3.53 dict-based client, ``aes_utils``, ``login_const`` <- shape A
|
|
10
|
+
0.3.54 typed dataclasses, ``api/``, ``utils/crypto`` <- shape B
|
|
11
|
+
0.3.55 shape B
|
|
12
|
+
0.3.56 shape A again (plus the ``models/`` package)
|
|
13
|
+
|
|
14
|
+
Because upstream has flip-flopped once, neither shape can be assumed permanent,
|
|
15
|
+
so both are supported rather than tracked. Every difference is resolved here,
|
|
16
|
+
once, at import time; no other module in this package may branch on the upstream
|
|
17
|
+
version. If you find yourself adding a second ``try: import aidot...`` anywhere
|
|
18
|
+
else, put it here instead.
|
|
19
|
+
|
|
20
|
+
The detection is by capability (does the name import?), never by parsing a
|
|
21
|
+
version string: upstream shipped shape A under both ``0.3.53`` and ``0.3.56``
|
|
22
|
+
with a five-day excursion in between, so a version comparison would encode the
|
|
23
|
+
excursion rather than the shape.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from enum import IntEnum
|
|
27
|
+
from typing import Any, Optional
|
|
28
|
+
|
|
29
|
+
# --------------------------------------------------------------------------- #
|
|
30
|
+
# Shape detection
|
|
31
|
+
# --------------------------------------------------------------------------- #
|
|
32
|
+
|
|
33
|
+
try: # shape B (0.3.54 - 0.3.55): typed account/device dataclasses + CloudApi
|
|
34
|
+
from aidot.models.auth_model import UserInformation as _UserInformation
|
|
35
|
+
HAS_TYPED_ACCOUNT = True
|
|
36
|
+
except ImportError: # shape A (<=0.3.53, >=0.3.56): plain dicts
|
|
37
|
+
_UserInformation = None
|
|
38
|
+
HAS_TYPED_ACCOUNT = False
|
|
39
|
+
|
|
40
|
+
#: True when upstream exposes the typed account layer (``user_info``,
|
|
41
|
+
#: ``_cloud_api``, ``UserInformation``). Prefer the named helpers below to
|
|
42
|
+
#: branching on this directly.
|
|
43
|
+
UPSTREAM_SHAPE = "typed" if HAS_TYPED_ACCOUNT else "dict"
|
|
44
|
+
|
|
45
|
+
#: Upstream's account record class, or ``None`` on shape A where the account is
|
|
46
|
+
#: a plain dict. Only used as a type annotation and an isinstance guard.
|
|
47
|
+
UserInformation = _UserInformation
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# --------------------------------------------------------------------------- #
|
|
51
|
+
# Constants: four moved from ``aidot.const`` to ``aidot.login_const``
|
|
52
|
+
# --------------------------------------------------------------------------- #
|
|
53
|
+
|
|
54
|
+
try: # shape B
|
|
55
|
+
from aidot.const import (
|
|
56
|
+
API_URL_TEMPLATE,
|
|
57
|
+
APP_ID,
|
|
58
|
+
DEFAULT_REGION,
|
|
59
|
+
PUBLIC_KEY_PEM,
|
|
60
|
+
)
|
|
61
|
+
except ImportError: # shape A
|
|
62
|
+
from aidot.login_const import (
|
|
63
|
+
API_URL_TEMPLATE,
|
|
64
|
+
APP_ID,
|
|
65
|
+
DEFAULT_REGION,
|
|
66
|
+
PUBLIC_KEY_PEM,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# --------------------------------------------------------------------------- #
|
|
71
|
+
# Crypto: ``aidot.utils.crypto`` -> ``aidot.aes_utils``
|
|
72
|
+
# --------------------------------------------------------------------------- #
|
|
73
|
+
|
|
74
|
+
try: # shape B
|
|
75
|
+
from aidot.utils.crypto import (
|
|
76
|
+
aes_decrypt,
|
|
77
|
+
aes_decrypt_to_json,
|
|
78
|
+
aes_encrypt,
|
|
79
|
+
)
|
|
80
|
+
except ImportError: # shape A
|
|
81
|
+
from aidot.aes_utils import (
|
|
82
|
+
aes_decrypt,
|
|
83
|
+
aes_decrypt_to_json,
|
|
84
|
+
aes_encrypt,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def rsa_encrypt(message: str, public_key: Any = None) -> str:
|
|
89
|
+
"""RSA/PKCS1v15 encrypt ``message``, returned base64.
|
|
90
|
+
|
|
91
|
+
Implemented here rather than re-exported. Shape B had
|
|
92
|
+
``aidot.utils.crypto.rsa_encrypt(message, public_key)``; shape A replaced it
|
|
93
|
+
with ``aidot.client.rsa_password_encrypt(message)``, which bakes the key in
|
|
94
|
+
and takes one argument. Neither signature is available on both shapes, and
|
|
95
|
+
``aidot_cameras.crypto.rsa_encrypt`` is public surface the integration repo
|
|
96
|
+
may import (see the back-compat rule in CLAUDE.md), so the two-argument form
|
|
97
|
+
is kept and satisfied locally. ``public_key`` defaults to upstream's
|
|
98
|
+
``PUBLIC_KEY_PEM``, which is the only key any caller ever passed.
|
|
99
|
+
"""
|
|
100
|
+
from cryptography.hazmat.backends import default_backend
|
|
101
|
+
from cryptography.hazmat.primitives import serialization
|
|
102
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
103
|
+
import base64
|
|
104
|
+
|
|
105
|
+
if public_key is None:
|
|
106
|
+
public_key = PUBLIC_KEY_PEM
|
|
107
|
+
loaded = serialization.load_pem_public_key(
|
|
108
|
+
public_key.encode() if isinstance(public_key, str) else public_key,
|
|
109
|
+
backend=default_backend(),
|
|
110
|
+
)
|
|
111
|
+
encrypted = loaded.encrypt(message.encode("utf-8"), padding.PKCS1v15())
|
|
112
|
+
return base64.b64encode(encrypted).decode("utf-8")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# --------------------------------------------------------------------------- #
|
|
116
|
+
# Device record: ``DeviceModel`` (shape B) / ``DeviceInformation`` (shape A)
|
|
117
|
+
# --------------------------------------------------------------------------- #
|
|
118
|
+
|
|
119
|
+
try: # shape B
|
|
120
|
+
from aidot.models.device_model import DeviceModel as DeviceRecord
|
|
121
|
+
except ImportError: # shape A
|
|
122
|
+
from aidot.models.device_model import DeviceInformation as DeviceRecord
|
|
123
|
+
|
|
124
|
+
#: The typed cloud device record. Both shapes expose ``from_json`` /
|
|
125
|
+
#: ``to_dict`` and the core field set this package reads (``id``, ``modelId``,
|
|
126
|
+
#: ``aesKey``, ``password``, ``product``); shape A drops ~20 fields shape B
|
|
127
|
+
#: carried and defaults everything to ``None`` rather than ``""``/empty, so
|
|
128
|
+
#: never rely on a nested default being present - check for ``None`` first.
|
|
129
|
+
DeviceModel = DeviceRecord
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# --------------------------------------------------------------------------- #
|
|
133
|
+
# DeviceState: upstream's LAN session enum, deleted in shape A
|
|
134
|
+
# --------------------------------------------------------------------------- #
|
|
135
|
+
|
|
136
|
+
try: # shape B
|
|
137
|
+
from aidot.device_client import DeviceState as _DeviceState
|
|
138
|
+
DEVICE_STATE_IS_UPSTREAMS = True
|
|
139
|
+
except ImportError: # shape A: no state machine, just two booleans
|
|
140
|
+
DEVICE_STATE_IS_UPSTREAMS = False
|
|
141
|
+
|
|
142
|
+
class _DeviceState(IntEnum):
|
|
143
|
+
"""Stand-in for upstream's deleted ``DeviceState``.
|
|
144
|
+
|
|
145
|
+
Shape A tracks the LAN session with ``_connecting`` /
|
|
146
|
+
``_connect_and_login`` booleans instead of an enum. The enum is still
|
|
147
|
+
re-exported from ``aidot_cameras.device_client`` because the Home
|
|
148
|
+
Assistant integration annotates and compares against it, and dropping a
|
|
149
|
+
public name outright would break it at import.
|
|
150
|
+
|
|
151
|
+
Members and values are copied from 0.3.55 so a stored or hard-coded
|
|
152
|
+
comparison keeps its meaning. On shape A nothing upstream ever
|
|
153
|
+
*produces* one of these - use ``device_session_authenticated()`` rather
|
|
154
|
+
than comparing a client's private state against it.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
IDLE = 0
|
|
158
|
+
INITIALIZING = 1
|
|
159
|
+
CONNECTING = 2
|
|
160
|
+
CONNECTED = 3
|
|
161
|
+
CONNECTION_FAILED = 4
|
|
162
|
+
AUTHENTICATING = 5
|
|
163
|
+
AUTHENTICATED = 6
|
|
164
|
+
AUTHENTICATION_FAILED = 7
|
|
165
|
+
DISCONNECTED = 8
|
|
166
|
+
|
|
167
|
+
DeviceState = _DeviceState
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def device_session_authenticated(device_client: Any) -> bool:
|
|
171
|
+
"""Whether a device client's LAN session is logged in, on either shape.
|
|
172
|
+
|
|
173
|
+
Shape B exposes ``_state == DeviceState.AUTHENTICATED``; shape A replaced
|
|
174
|
+
that with the ``connect_and_login`` property. Consumers that only want the
|
|
175
|
+
answer should call this instead of touching either private attribute.
|
|
176
|
+
"""
|
|
177
|
+
if DEVICE_STATE_IS_UPSTREAMS:
|
|
178
|
+
return getattr(device_client, "_state", None) == DeviceState.AUTHENTICATED
|
|
179
|
+
return bool(getattr(device_client, "connect_and_login", False))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# --------------------------------------------------------------------------- #
|
|
183
|
+
# Reconnect timer handle: ``_reconnect_timer`` -> ``_reconnect_handle``
|
|
184
|
+
# --------------------------------------------------------------------------- #
|
|
185
|
+
|
|
186
|
+
#: Attribute names a pending reconnect may be parked under, newest first.
|
|
187
|
+
#: ``CameraClient.async_close`` cancels whichever exists so a closed account
|
|
188
|
+
#: cannot re-open a TCP connection ~45 s later (upstream cancels neither).
|
|
189
|
+
RECONNECT_HANDLE_ATTRS = ("_reconnect_handle", "_reconnect_timer")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def cancel_pending_reconnect(device_client: Any) -> bool:
|
|
193
|
+
"""Cancel a device client's pending reconnect, whichever shape it uses.
|
|
194
|
+
|
|
195
|
+
Returns True if something was cancelled. Deliberately not a silent
|
|
196
|
+
``getattr(x, "_reconnect_timer", None)``: that spelling exists only on shape
|
|
197
|
+
B, so on shape A it would find nothing, cancel nothing, and report success -
|
|
198
|
+
the leak fix would go quiet instead of failing. Callers that care should
|
|
199
|
+
log the False case.
|
|
200
|
+
"""
|
|
201
|
+
for attr in RECONNECT_HANDLE_ATTRS:
|
|
202
|
+
handle = getattr(device_client, attr, None)
|
|
203
|
+
if handle is not None:
|
|
204
|
+
handle.cancel()
|
|
205
|
+
return True
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# --------------------------------------------------------------------------- #
|
|
210
|
+
# Account HTTP surface: ``CloudApi`` (shape B) -> client methods (shape A)
|
|
211
|
+
# --------------------------------------------------------------------------- #
|
|
212
|
+
|
|
213
|
+
async def api_get_houses(client: Any) -> Optional[list]:
|
|
214
|
+
"""List the account's houses."""
|
|
215
|
+
if HAS_TYPED_ACCOUNT:
|
|
216
|
+
return await client._cloud_api.get_houses()
|
|
217
|
+
return await client.async_get_houses()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
async def api_get_devices(client: Any, house_id: str) -> Optional[list]:
|
|
221
|
+
"""List one house's devices."""
|
|
222
|
+
if HAS_TYPED_ACCOUNT:
|
|
223
|
+
return await client._cloud_api.get_devices(house_id)
|
|
224
|
+
return await client.async_get_devices(house_id)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
async def api_get_products(client: Any, product_ids: str) -> Optional[list]:
|
|
228
|
+
"""Fetch product metadata for a comma-joined id list."""
|
|
229
|
+
if HAS_TYPED_ACCOUNT:
|
|
230
|
+
return await client._cloud_api.get_products(product_ids)
|
|
231
|
+
return await client.async_get_products(product_ids)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
async def api_refresh_token(client: Any) -> Any:
|
|
235
|
+
"""Exchange the refresh token for a new access token.
|
|
236
|
+
|
|
237
|
+
Both shapes return the response body and raise ``AidotAuthFailed`` when the
|
|
238
|
+
refresh token itself is rejected; shape B additionally returns ``None`` on
|
|
239
|
+
some non-auth failures, so callers must treat a falsy return as "no token".
|
|
240
|
+
"""
|
|
241
|
+
if HAS_TYPED_ACCOUNT:
|
|
242
|
+
return await client._cloud_api.refresh_token()
|
|
243
|
+
return await client.async_refresh_token()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# --------------------------------------------------------------------------- #
|
|
247
|
+
# Account fields: ``user_info`` dataclass (shape B) -> plain attrs (shape A)
|
|
248
|
+
# --------------------------------------------------------------------------- #
|
|
249
|
+
|
|
250
|
+
def account_region(client: Any) -> str:
|
|
251
|
+
"""The account's API region ("us", "eu", ...)."""
|
|
252
|
+
if HAS_TYPED_ACCOUNT:
|
|
253
|
+
return getattr(client.user_info, "region", "") or ""
|
|
254
|
+
return getattr(client, "_region", "") or ""
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def account_refresh_token(client: Any) -> str:
|
|
258
|
+
"""The stored refresh token, or "" when there is none."""
|
|
259
|
+
if HAS_TYPED_ACCOUNT:
|
|
260
|
+
return getattr(client.user_info, "refreshToken", "") or ""
|
|
261
|
+
from .const import CONF_REFRESH_TOKEN
|
|
262
|
+
return (client.login_info or {}).get(CONF_REFRESH_TOKEN) or ""
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def account_token_ttl(client: Any, default: int = 7200) -> int:
|
|
266
|
+
"""Access-token lifetime in seconds, defaulting to the server's 7200."""
|
|
267
|
+
if HAS_TYPED_ACCOUNT:
|
|
268
|
+
raw = getattr(client.user_info, "expiresIn", 0)
|
|
269
|
+
else:
|
|
270
|
+
raw = (client.login_info or {}).get("expiresIn", 0)
|
|
271
|
+
try:
|
|
272
|
+
return int(raw or default)
|
|
273
|
+
except (TypeError, ValueError):
|
|
274
|
+
return default
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def account_record(client: Any) -> Any:
|
|
278
|
+
"""What to pass as a device client's second constructor argument.
|
|
279
|
+
|
|
280
|
+
Shape B wants the ``UserInformation`` dataclass; shape A wants the raw
|
|
281
|
+
``login_info`` dict. See ``device_client_args``.
|
|
282
|
+
"""
|
|
283
|
+
if HAS_TYPED_ACCOUNT:
|
|
284
|
+
return client.user_info
|
|
285
|
+
return client.login_info
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def device_client_args(
|
|
289
|
+
device_record: Any,
|
|
290
|
+
raw_device: dict,
|
|
291
|
+
typed_account: Any,
|
|
292
|
+
login_info: Optional[dict],
|
|
293
|
+
) -> tuple:
|
|
294
|
+
"""The ``(device, user_info)`` pair upstream's ``DeviceClient`` expects.
|
|
295
|
+
|
|
296
|
+
Shape B: ``(DeviceModel, UserInformation)`` - typed dataclasses.
|
|
297
|
+
Shape A: ``(dict, dict)`` - the raw cloud records, natively.
|
|
298
|
+
|
|
299
|
+
Shape A taking raw dicts is why this package carried ``raw_device`` /
|
|
300
|
+
``login_info`` alongside the typed models in the first place: the typed
|
|
301
|
+
round trip drops every camera-only field. On shape A the originals go
|
|
302
|
+
straight through and nothing is lost.
|
|
303
|
+
|
|
304
|
+
Takes the four values rather than the account client, because the only
|
|
305
|
+
caller (``CameraDeviceClient.__init__``) is handed them directly and has no
|
|
306
|
+
client reference. ``typed_account`` may be a ``UserInformation`` or, from
|
|
307
|
+
an older caller, already a dict - on shape A a dict ``login_info`` wins and
|
|
308
|
+
a dict ``typed_account`` is the fallback, so both call styles work.
|
|
309
|
+
"""
|
|
310
|
+
if HAS_TYPED_ACCOUNT:
|
|
311
|
+
return (device_record, typed_account)
|
|
312
|
+
account = login_info if isinstance(login_info, dict) else None
|
|
313
|
+
if account is None:
|
|
314
|
+
account = typed_account if isinstance(typed_account, dict) else {}
|
|
315
|
+
if not isinstance(typed_account, dict) and hasattr(typed_account, "to_dict"):
|
|
316
|
+
account = typed_account.to_dict()
|
|
317
|
+
return (raw_device, account)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
#: True when upstream's ``DeviceClient`` exposes a ``read_data`` seam - one
|
|
321
|
+
#: decrypted frame at a time, which this package hooks to recover camera-only
|
|
322
|
+
#: attribute keys that upstream's typed ``DeviceAttr`` model drops. Shape A
|
|
323
|
+
#: inlines the read into ``receive_data``, so there is no seam and the raw JSON
|
|
324
|
+
#: never escapes. See ``CameraDeviceClient._notify_status_update``.
|
|
325
|
+
try:
|
|
326
|
+
from aidot.device_client import DeviceClient as _DeviceClient
|
|
327
|
+
HAS_READ_DATA_SEAM = callable(getattr(_DeviceClient, "read_data", None))
|
|
328
|
+
except ImportError: # pragma: no cover - upstream is a hard dependency
|
|
329
|
+
HAS_READ_DATA_SEAM = False
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# --------------------------------------------------------------------------- #
|
|
333
|
+
# Discovery: static ``Discover`` (shape B) -> instance ``Discover`` (shape A)
|
|
334
|
+
# --------------------------------------------------------------------------- #
|
|
335
|
+
|
|
336
|
+
#: True when upstream's ``Discover`` keeps discovered addresses in the
|
|
337
|
+
#: process-wide ``DISCOVERED_DEVICE`` class dict. Shape A made discovery
|
|
338
|
+
#: instance-scoped (``self._discover.discovered_device``), which is the shape
|
|
339
|
+
#: this package's ``CameraDiscover`` already presents - so on shape A the map
|
|
340
|
+
#: no longer has to be shared through the base class to reach upstream's
|
|
341
|
+
#: ``get_device_client``.
|
|
342
|
+
try:
|
|
343
|
+
from aidot.discover import Discover as _Discover
|
|
344
|
+
HAS_SHARED_DISCOVERY_MAP = isinstance(
|
|
345
|
+
getattr(_Discover, "DISCOVERED_DEVICE", None), dict
|
|
346
|
+
)
|
|
347
|
+
except ImportError: # pragma: no cover - upstream is a hard dependency
|
|
348
|
+
HAS_SHARED_DISCOVERY_MAP = False
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def broadcast_protocol_args(callback: Any, user_id: str) -> tuple:
|
|
352
|
+
"""Positional args for upstream's ``BroadcastProtocol.__init__``.
|
|
353
|
+
|
|
354
|
+
Shape B takes ``(callback)`` and has no notion of the account id; shape A
|
|
355
|
+
takes ``(callback, user_id)`` and uses it to build the request payload.
|
|
356
|
+
"""
|
|
357
|
+
import inspect
|
|
358
|
+
from aidot.discover import BroadcastProtocol
|
|
359
|
+
|
|
360
|
+
params = list(inspect.signature(BroadcastProtocol.__init__).parameters)
|
|
361
|
+
return (callback, user_id) if "user_id" in params else (callback,)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
#: Names this module intends as its public surface within the package.
|
|
365
|
+
__all__ = [
|
|
366
|
+
"API_URL_TEMPLATE",
|
|
367
|
+
"APP_ID",
|
|
368
|
+
"DEFAULT_REGION",
|
|
369
|
+
"DEVICE_STATE_IS_UPSTREAMS",
|
|
370
|
+
"HAS_READ_DATA_SEAM",
|
|
371
|
+
"HAS_SHARED_DISCOVERY_MAP",
|
|
372
|
+
"HAS_TYPED_ACCOUNT",
|
|
373
|
+
"PUBLIC_KEY_PEM",
|
|
374
|
+
"RECONNECT_HANDLE_ATTRS",
|
|
375
|
+
"UPSTREAM_SHAPE",
|
|
376
|
+
"DeviceModel",
|
|
377
|
+
"DeviceRecord",
|
|
378
|
+
"DeviceState",
|
|
379
|
+
"UserInformation",
|
|
380
|
+
"account_record",
|
|
381
|
+
"account_refresh_token",
|
|
382
|
+
"account_region",
|
|
383
|
+
"account_token_ttl",
|
|
384
|
+
"aes_decrypt",
|
|
385
|
+
"aes_decrypt_to_json",
|
|
386
|
+
"aes_encrypt",
|
|
387
|
+
"api_get_devices",
|
|
388
|
+
"api_get_houses",
|
|
389
|
+
"api_get_products",
|
|
390
|
+
"api_refresh_token",
|
|
391
|
+
"broadcast_protocol_args",
|
|
392
|
+
"cancel_pending_reconnect",
|
|
393
|
+
"device_client_args",
|
|
394
|
+
"device_session_authenticated",
|
|
395
|
+
"rsa_encrypt",
|
|
396
|
+
]
|