python-aidot-cameras 0.11.2__tar.gz → 0.11.5__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.11.2/python_aidot_cameras.egg-info → python_aidot_cameras-0.11.5}/PKG-INFO +3 -3
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/README.md +1 -1
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/__main__.py +31 -8
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/camera/client.py +103 -10
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/camera/lan_control.py +4 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/camera/playback.py +19 -1
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/camera/protocol.py +6 -1
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/camera/sdes_open.py +15 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/camera/webrtc_open.py +9 -25
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/client.py +8 -1
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/pyproject.toml +2 -2
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5/python_aidot_cameras.egg-info}/PKG-INFO +3 -3
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/python_aidot_cameras.egg-info/SOURCES.txt +2 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/python_aidot_cameras.egg-info/requires.txt +1 -1
- python_aidot_cameras-0.11.5/tests/test_keepalive_renew.py +119 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_lan_control.py +20 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_motion_poll.py +1 -1
- python_aidot_cameras-0.11.5/tests/test_token_file_robustness.py +43 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/LICENSE +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/__init__.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/aes_utils.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/camera/__init__.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/camera/constants.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/camera/controls.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/camera/models.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/camera/sdes.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/camera/tutk.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/const.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/credentials.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/device_client.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/discover.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/exceptions.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/g711.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/login_const.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/models/__init__.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/models/device_client_model.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/models/device_model.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/models/discover_model.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/py.typed +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/setup.cfg +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_wait_or_event.py +0 -0
{python_aidot_cameras-0.11.2/python_aidot_cameras.egg-info → python_aidot_cameras-0.11.5}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.5
|
|
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
|
|
@@ -12,7 +12,7 @@ Requires-Python: >=3.11
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: aiohttp>=3.9
|
|
15
|
-
Requires-Dist: paho-mqtt>=
|
|
15
|
+
Requires-Dist: paho-mqtt>=1.6.1
|
|
16
16
|
Requires-Dist: cryptography>=42.0
|
|
17
17
|
Requires-Dist: pycryptodome>=3.20
|
|
18
18
|
Requires-Dist: dacite>=1.8
|
|
@@ -167,7 +167,7 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
167
167
|
| `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) |
|
|
168
168
|
| `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) |
|
|
169
169
|
| `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) |
|
|
170
|
-
| `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
|
|
170
|
+
| `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) |
|
|
171
171
|
| `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) |
|
|
172
172
|
| `AIDOT_LIVESTREAM_PARAM` | Set to `0` to skip the cloud `liveStreamParam` pre-connect that provisions battery cameras' live-stream sessions before signaling (without it, battery cameras like the L2 models reject streaming with `-50019`). | `1` (enabled) |
|
|
173
173
|
|
|
@@ -137,7 +137,7 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
137
137
|
| `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) |
|
|
138
138
|
| `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) |
|
|
139
139
|
| `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) |
|
|
140
|
-
| `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
|
|
140
|
+
| `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) |
|
|
141
141
|
| `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) |
|
|
142
142
|
| `AIDOT_LIVESTREAM_PARAM` | Set to `0` to skip the cloud `liveStreamParam` pre-connect that provisions battery cameras' live-stream sessions before signaling (without it, battery cameras like the L2 models reject streaming with `-50019`). | `1` (enabled) |
|
|
143
143
|
|
|
@@ -59,6 +59,7 @@ import logging
|
|
|
59
59
|
import os
|
|
60
60
|
import signal
|
|
61
61
|
import sys
|
|
62
|
+
import tempfile
|
|
62
63
|
|
|
63
64
|
import aiohttp
|
|
64
65
|
|
|
@@ -85,10 +86,25 @@ def _read_token_file(path: str) -> dict:
|
|
|
85
86
|
|
|
86
87
|
|
|
87
88
|
def _write_token_file(path: str, data: dict) -> None:
|
|
88
|
-
"""
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
os.chmod
|
|
89
|
+
"""Atomic, 0600-secure write of login_info to ``path``.
|
|
90
|
+
|
|
91
|
+
Serialize into a temp file in the same directory (mkstemp creates it 0600,
|
|
92
|
+
closing the world-readable window os.chmod-after-write leaves) and only then
|
|
93
|
+
os.replace it over ``path`` - so a serialization failure never truncates the
|
|
94
|
+
previously-valid token. On any error the temp file is removed and re-raised.
|
|
95
|
+
"""
|
|
96
|
+
directory = os.path.dirname(path) or "."
|
|
97
|
+
fd, tmp = tempfile.mkstemp(dir=directory, prefix=".token-", suffix=".tmp")
|
|
98
|
+
try:
|
|
99
|
+
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
100
|
+
json.dump(data, fh)
|
|
101
|
+
os.replace(tmp, path)
|
|
102
|
+
except BaseException:
|
|
103
|
+
try:
|
|
104
|
+
os.unlink(tmp)
|
|
105
|
+
except OSError:
|
|
106
|
+
pass
|
|
107
|
+
raise
|
|
92
108
|
|
|
93
109
|
|
|
94
110
|
def _install_token_cache(client: AidotClient, path: str) -> None:
|
|
@@ -133,10 +149,17 @@ async def _make_client(session: aiohttp.ClientSession) -> AidotClient:
|
|
|
133
149
|
password = os.environ.get("AIDOT_PASSWORD")
|
|
134
150
|
|
|
135
151
|
if token_file and os.path.exists(token_file):
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
152
|
+
try:
|
|
153
|
+
token = await loop.run_in_executor(None, _read_token_file, token_file)
|
|
154
|
+
except (OSError, ValueError) as exc:
|
|
155
|
+
# ValueError covers json.JSONDecodeError (empty/partial/corrupt
|
|
156
|
+
# cache). Fall through to the username/password path rather than
|
|
157
|
+
# crash CLI startup.
|
|
158
|
+
_LOGGER.warning("ignoring unreadable token cache %s: %s", token_file, exc)
|
|
159
|
+
else:
|
|
160
|
+
client = AidotClient(session=session, token=token)
|
|
161
|
+
_install_token_cache(client, token_file)
|
|
162
|
+
return client
|
|
140
163
|
|
|
141
164
|
if not username or not password:
|
|
142
165
|
sys.exit(
|
|
@@ -565,6 +565,14 @@ def _get_stream_slots() -> "asyncio.Semaphore":
|
|
|
565
565
|
|
|
566
566
|
|
|
567
567
|
|
|
568
|
+
# Battery cameras re-sleep on their own low-power timer ~25 s after the last
|
|
569
|
+
# keep-alive, even mid-view. Renew setKeepAliveTime INSIDE that window so there
|
|
570
|
+
# is no sleep gap (the official app renews every 30 s, which leaves a ~5 s gap
|
|
571
|
+
# against the 25 s window). See CameraMixin._keepalive_renew_loop.
|
|
572
|
+
_KEEPALIVE_WINDOW_S = 25
|
|
573
|
+
_KEEPALIVE_RENEW_S = 20
|
|
574
|
+
|
|
575
|
+
|
|
568
576
|
class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
569
577
|
"""All camera/streaming methods, mixed into DeviceClient via inheritance."""
|
|
570
578
|
|
|
@@ -695,7 +703,9 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
695
703
|
self._motion_active: bool = False
|
|
696
704
|
self._motion_cb: Optional[Callable] = None
|
|
697
705
|
self._motion_task: Optional["asyncio.Task[None]"] = None
|
|
698
|
-
|
|
706
|
+
# Insertion-ordered set (dict keys) so the memory-bound trim can evict
|
|
707
|
+
# the OLDEST uids deterministically rather than arbitrary ones.
|
|
708
|
+
self._motion_seen: dict = {}
|
|
699
709
|
self._motion_interval: float = 30.0
|
|
700
710
|
|
|
701
711
|
# -- Pre-0.6 core API, kept for existing consumers ----------------------- #
|
|
@@ -1020,9 +1030,12 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
1020
1030
|
if not mqtt_host:
|
|
1021
1031
|
# Fall back to the regional MQTT broker URL known to work from
|
|
1022
1032
|
# the AiDot web client: wss://{region}-mqtt.arnoo.com:8443/mqtt
|
|
1033
|
+
# Redacted: `data`/`body` carries mqttPassword; log only the
|
|
1034
|
+
# non-sensitive key names present, mirroring the hasPwd-style
|
|
1035
|
+
# redaction of the debug log below.
|
|
1023
1036
|
_LOGGER.warning(
|
|
1024
1037
|
"getServerUrlConfig returned no mqttServerUrl; "
|
|
1025
|
-
"using regional fallback.
|
|
1038
|
+
"using regional fallback. keys=%s", sorted(data)
|
|
1026
1039
|
)
|
|
1027
1040
|
mqtt_host = f"wss://{self._region}-mqtt.arnoo.com:8443/mqtt"
|
|
1028
1041
|
|
|
@@ -2116,11 +2129,16 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2116
2129
|
return
|
|
2117
2130
|
frame_event.set()
|
|
2118
2131
|
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2132
|
+
try:
|
|
2133
|
+
session = await self.async_open_webrtc_stream(
|
|
2134
|
+
on_frame=_on_frame,
|
|
2135
|
+
timeout=timeout,
|
|
2136
|
+
status_callback=status_callback,
|
|
2137
|
+
)
|
|
2138
|
+
except Exception as _snap_exc:
|
|
2139
|
+
_LOGGER.error(
|
|
2140
|
+
"async_snapshot DTLS failed for %s: %s", self.device_id, _snap_exc)
|
|
2141
|
+
return False
|
|
2124
2142
|
try:
|
|
2125
2143
|
try:
|
|
2126
2144
|
await _asyncio.wait_for(frame_event.wait(), timeout=timeout)
|
|
@@ -2159,6 +2177,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2159
2177
|
if self._stream_task is not None and not self._stream_task.done():
|
|
2160
2178
|
return
|
|
2161
2179
|
self._streaming_active = True
|
|
2180
|
+
self._start_keepalive_renew() # battery: renew keep-alive on this path too
|
|
2162
2181
|
self._stream_task = asyncio.ensure_future(self._streaming_loop())
|
|
2163
2182
|
|
|
2164
2183
|
async def async_stop_streaming(self) -> None:
|
|
@@ -2186,6 +2205,13 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2186
2205
|
await task
|
|
2187
2206
|
except (asyncio.CancelledError, Exception):
|
|
2188
2207
|
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), 'async_stop_streaming', exc_info=True)
|
|
2208
|
+
ka_task, self._keepalive_task = getattr(self, "_keepalive_task", None), None
|
|
2209
|
+
if ka_task is not None and not ka_task.done():
|
|
2210
|
+
ka_task.cancel()
|
|
2211
|
+
try:
|
|
2212
|
+
await ka_task
|
|
2213
|
+
except (asyncio.CancelledError, Exception):
|
|
2214
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), 'async_stop_streaming', exc_info=True)
|
|
2189
2215
|
# Reap a persistent-MQTT stream drain that no session stopped (e.g. an
|
|
2190
2216
|
# open cancelled mid-handshake) so its handler is removed from the shared
|
|
2191
2217
|
# connection and its blocked executor thread is released.
|
|
@@ -2243,7 +2269,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2243
2269
|
if self._motion_task is not None and not self._motion_task.done():
|
|
2244
2270
|
return
|
|
2245
2271
|
self._motion_active = True
|
|
2246
|
-
self._motion_seen =
|
|
2272
|
+
self._motion_seen = {}
|
|
2247
2273
|
self._motion_task = asyncio.ensure_future(self._motion_poll_loop(lookback_s))
|
|
2248
2274
|
|
|
2249
2275
|
async def async_stop_motion_polling(self) -> None:
|
|
@@ -2273,7 +2299,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2273
2299
|
uid = it.get("eventUuid") or it.get("eventId") or it.get("recordId")
|
|
2274
2300
|
if not uid or uid in self._motion_seen:
|
|
2275
2301
|
continue
|
|
2276
|
-
self._motion_seen
|
|
2302
|
+
self._motion_seen[uid] = None
|
|
2277
2303
|
fresh.append(it)
|
|
2278
2304
|
if primed and fresh and self._motion_cb is not None:
|
|
2279
2305
|
for it in sorted(fresh, key=lambda x: x.get("eventTime") or 0):
|
|
@@ -2285,7 +2311,9 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2285
2311
|
_LOGGER.debug("motion callback raised", exc_info=True)
|
|
2286
2312
|
primed = True
|
|
2287
2313
|
if len(self._motion_seen) > 1000: # bound memory
|
|
2288
|
-
|
|
2314
|
+
# Keep the 400 most-recent uids (insertion order), evicting
|
|
2315
|
+
# the oldest so still-in-window events stay deduplicated.
|
|
2316
|
+
self._motion_seen = dict.fromkeys(list(self._motion_seen)[-400:])
|
|
2289
2317
|
except asyncio.CancelledError:
|
|
2290
2318
|
return
|
|
2291
2319
|
except Exception as exc:
|
|
@@ -2295,6 +2323,65 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2295
2323
|
except asyncio.CancelledError:
|
|
2296
2324
|
return
|
|
2297
2325
|
|
|
2326
|
+
async def _async_set_keep_alive(self, keep_alive_s: int = _KEEPALIVE_WINDOW_S) -> None:
|
|
2327
|
+
"""POST setKeepAliveTime so a battery camera stays awake for keep_alive_s
|
|
2328
|
+
seconds. Best-effort (errors swallowed). Wire format matches the app
|
|
2329
|
+
(n.java: keepAliveTime=25)."""
|
|
2330
|
+
try:
|
|
2331
|
+
import aiohttp as _aiohttp
|
|
2332
|
+
async with _aiohttp.ClientSession() as _s:
|
|
2333
|
+
async with _s.post(
|
|
2334
|
+
f"{self._aidot_v32_base}/devices/{self.device_id}/setKeepAliveTime",
|
|
2335
|
+
json={"keepAliveTime": keep_alive_s},
|
|
2336
|
+
headers=self._aidot_headers(),
|
|
2337
|
+
timeout=_aiohttp.ClientTimeout(total=8),
|
|
2338
|
+
) as _r:
|
|
2339
|
+
_LOGGER.debug(
|
|
2340
|
+
"setKeepAliveTime HTTP %d for %s", _r.status, self.device_id
|
|
2341
|
+
)
|
|
2342
|
+
except Exception as _exc:
|
|
2343
|
+
_LOGGER.debug(
|
|
2344
|
+
"setKeepAliveTime HTTP failed for %s: %s", self.device_id, _exc
|
|
2345
|
+
)
|
|
2346
|
+
|
|
2347
|
+
def _start_keepalive_renew(self) -> None:
|
|
2348
|
+
"""Start the battery keep-alive renew loop - battery-only and
|
|
2349
|
+
single-instance: cancel any prior loop first so a re-view within the
|
|
2350
|
+
renew sleep window can't orphan one (an orphan keeps POSTing
|
|
2351
|
+
setKeepAliveTime until _streaming_active next clears). No-op for mains
|
|
2352
|
+
cameras, which never sleep."""
|
|
2353
|
+
if not self.is_battery_camera:
|
|
2354
|
+
return
|
|
2355
|
+
prev = getattr(self, "_keepalive_task", None)
|
|
2356
|
+
if prev is not None and not prev.done():
|
|
2357
|
+
prev.cancel()
|
|
2358
|
+
self._keepalive_task = asyncio.ensure_future(self._keepalive_renew_loop())
|
|
2359
|
+
|
|
2360
|
+
def _cancel_keepalive_renew(self) -> None:
|
|
2361
|
+
"""Cancel the battery keep-alive renew loop if running (fire-and-forget)."""
|
|
2362
|
+
task, self._keepalive_task = getattr(self, "_keepalive_task", None), None
|
|
2363
|
+
if task is not None and not task.done():
|
|
2364
|
+
task.cancel()
|
|
2365
|
+
|
|
2366
|
+
async def _keepalive_renew_loop(self) -> None:
|
|
2367
|
+
"""Renew setKeepAliveTime while a BATTERY camera is streaming (app parity).
|
|
2368
|
+
|
|
2369
|
+
The camera's own low-power timer returns it to sleep ~25 s after the last
|
|
2370
|
+
keep-alive even mid-view, so a one-shot keep-alive at open lets the stream
|
|
2371
|
+
drop partway through. The official app renews it throughout a live view;
|
|
2372
|
+
we renew every _KEEPALIVE_RENEW_S seconds (inside the 25 s window, so no
|
|
2373
|
+
sleep gap). Mains cameras never sleep, so this is battery-only."""
|
|
2374
|
+
if not self.is_battery_camera:
|
|
2375
|
+
return
|
|
2376
|
+
try:
|
|
2377
|
+
while self._streaming_active:
|
|
2378
|
+
await asyncio.sleep(_KEEPALIVE_RENEW_S)
|
|
2379
|
+
if not self._streaming_active:
|
|
2380
|
+
break
|
|
2381
|
+
await self._async_set_keep_alive()
|
|
2382
|
+
except asyncio.CancelledError:
|
|
2383
|
+
pass
|
|
2384
|
+
|
|
2298
2385
|
async def start_keepalive(
|
|
2299
2386
|
self,
|
|
2300
2387
|
rtsp_push_url: Optional[str] = None,
|
|
@@ -2369,6 +2456,10 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2369
2456
|
self._keepalive_rtsp_url = rtsp_push_url
|
|
2370
2457
|
self._go2rtc_url = go2rtc_url
|
|
2371
2458
|
self._streaming_active = True
|
|
2459
|
+
# App parity: keep a battery camera awake for the whole view (mains cams
|
|
2460
|
+
# never sleep, so this is a no-op there). Single-instance; cancelled in
|
|
2461
|
+
# stop / idle-release.
|
|
2462
|
+
self._start_keepalive_renew()
|
|
2372
2463
|
if self.is_sdes_camera:
|
|
2373
2464
|
self._stream_task = asyncio.ensure_future(self._sdes_keepalive_loop())
|
|
2374
2465
|
elif rtsp_push_url and rtsp_push_url.startswith("http"):
|
|
@@ -2691,6 +2782,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2691
2782
|
except Exception:
|
|
2692
2783
|
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_sdes_keepalive_loop', exc_info=True)
|
|
2693
2784
|
self._streaming_active = False
|
|
2785
|
+
self._cancel_keepalive_renew()
|
|
2694
2786
|
self._keepalive_rtsp_url = None
|
|
2695
2787
|
self._serve_ready.clear()
|
|
2696
2788
|
_LOGGER.debug(
|
|
@@ -3478,6 +3570,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3478
3570
|
# No viewer - go dormant; camera.stream_source() restarts us when
|
|
3479
3571
|
# someone opens the live view again.
|
|
3480
3572
|
self._streaming_active = False
|
|
3573
|
+
self._cancel_keepalive_renew()
|
|
3481
3574
|
self._keepalive_rtsp_url = None
|
|
3482
3575
|
self._serve_ready.clear()
|
|
3483
3576
|
_LOGGER.debug(
|
|
@@ -309,6 +309,10 @@ class CameraLanClient:
|
|
|
309
309
|
"attr": [], "channel": "tcp", "ascNumber": asc},
|
|
310
310
|
"tst": int(time.time() * 1000), "deviceId": self.device_id})]
|
|
311
311
|
replies = await self._session(build)
|
|
312
|
+
if not replies:
|
|
313
|
+
raise CameraLanError(
|
|
314
|
+
f"{self.device_id}: no reply to attribute query"
|
|
315
|
+
)
|
|
312
316
|
return (replies[0].get("payload") or {}).get("attr") or {}
|
|
313
317
|
|
|
314
318
|
async def async_set_attributes(self, attr: dict) -> bool:
|
|
@@ -193,15 +193,18 @@ class CloudPlaybackSession:
|
|
|
193
193
|
)
|
|
194
194
|
except TimeoutError:
|
|
195
195
|
_LOGGER.error("Cloud playback: login response timed out")
|
|
196
|
+
await self._cleanup()
|
|
196
197
|
return False
|
|
197
198
|
except Exception as exc:
|
|
198
199
|
_LOGGER.error("Cloud playback: login read error: %s", exc)
|
|
200
|
+
await self._cleanup()
|
|
199
201
|
return False
|
|
200
202
|
|
|
201
203
|
if hdr["cmd"] != _CMD_LOGIN_RES:
|
|
202
204
|
_LOGGER.error(
|
|
203
205
|
"Cloud playback: unexpected login response cmd=0x%04x", hdr["cmd"]
|
|
204
206
|
)
|
|
207
|
+
await self._cleanup()
|
|
205
208
|
return False
|
|
206
209
|
|
|
207
210
|
try:
|
|
@@ -211,6 +214,7 @@ class CloudPlaybackSession:
|
|
|
211
214
|
"Cloud playback: login rejected code=%s body=%s",
|
|
212
215
|
body_obj.get("code"), body_obj,
|
|
213
216
|
)
|
|
217
|
+
await self._cleanup()
|
|
214
218
|
return False
|
|
215
219
|
except (json.JSONDecodeError, ValueError):
|
|
216
220
|
pass # some firmware sends no JSON body - treat as success
|
|
@@ -292,6 +296,13 @@ class CloudPlaybackSession:
|
|
|
292
296
|
else:
|
|
293
297
|
_LOGGER.warning("Cloud playback: unexpected stream result=%d", result)
|
|
294
298
|
|
|
299
|
+
# Loop exited on end-of-stream or a receive error/timeout (not via an
|
|
300
|
+
# external stop(), which cancels this task and owns its own teardown).
|
|
301
|
+
# Tear down here so the heartbeat loop wakes to a closed writer and
|
|
302
|
+
# exits, and the socket is not left open until stop() is called.
|
|
303
|
+
self._running = False
|
|
304
|
+
await self._cleanup()
|
|
305
|
+
|
|
295
306
|
async def start(self) -> bool:
|
|
296
307
|
self._running = True
|
|
297
308
|
if not await self._connect_and_login():
|
|
@@ -326,12 +337,19 @@ class CloudPlaybackSession:
|
|
|
326
337
|
pass
|
|
327
338
|
self._hb_task = None
|
|
328
339
|
self._rx_task = None
|
|
340
|
+
await self._cleanup()
|
|
341
|
+
|
|
342
|
+
async def _cleanup(self) -> None:
|
|
343
|
+
# Close the TCP writer and drop the reader/writer refs. Nulling the
|
|
344
|
+
# writer makes a subsequent stop() (or a second call) a safe no-op, so
|
|
345
|
+
# this is the single teardown path shared by the login-failure returns,
|
|
346
|
+
# the receive-loop exit, and stop().
|
|
329
347
|
if self._writer is not None:
|
|
330
348
|
try:
|
|
331
349
|
self._writer.close()
|
|
332
350
|
await self._writer.wait_closed()
|
|
333
351
|
except Exception:
|
|
334
|
-
_LOGGER.debug("swallowed exception in %s", '
|
|
352
|
+
_LOGGER.debug("swallowed exception in %s", '_cleanup', exc_info=True)
|
|
335
353
|
self._writer = None
|
|
336
354
|
self._reader = None
|
|
337
355
|
|
|
@@ -1219,7 +1219,12 @@ def _mqtt_session_sync(
|
|
|
1219
1219
|
else str(msg.payload))
|
|
1220
1220
|
msg_q.put((msg.topic, payload))
|
|
1221
1221
|
|
|
1222
|
-
def _on_disconnect(c, ud,
|
|
1222
|
+
def _on_disconnect(c, ud, *args):
|
|
1223
|
+
# paho >=2 calls (disconnect_flags, reason_code, properties); paho <2
|
|
1224
|
+
# calls (rc,). Pull the reason code from whichever slot it lands in so
|
|
1225
|
+
# the log is truthful on both (a fixed 5-arg signature put paho 1.x's rc
|
|
1226
|
+
# in disconnect_flags and logged reason_code as None).
|
|
1227
|
+
reason_code = args[1] if len(args) >= 2 else (args[0] if args else None)
|
|
1223
1228
|
# If _on_connect was never fired (WebSocket upgrade failed, auth refused
|
|
1224
1229
|
# at TCP level, etc.) signal conn_ev now so the caller doesn't time out.
|
|
1225
1230
|
if not conn_ev.is_set():
|
|
@@ -672,6 +672,15 @@ class _SdesOpenMixin:
|
|
|
672
672
|
# proceed rather than abort on a code we can't classify as terminal
|
|
673
673
|
# (genuine terminal rejects are still caught on the webrtcResp ack).
|
|
674
674
|
if _lp_on_sdes == 0:
|
|
675
|
+
# Match the sibling raises: close the reserved sockets and
|
|
676
|
+
# signal the MQTT thread before propagating. (No sdp_path
|
|
677
|
+
# unlink here - it is not created until later, below.)
|
|
678
|
+
for _rsock in (_audio_sock, _video_sock):
|
|
679
|
+
try:
|
|
680
|
+
_rsock.close()
|
|
681
|
+
except Exception:
|
|
682
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_bridge_fn', exc_info=True)
|
|
683
|
+
outgoing_q.put_nowait(None) # stop MQTT thread
|
|
675
684
|
raise RuntimeError(
|
|
676
685
|
f"livePlay refused by camera (livePlay=0, code={_lp_code_sdes})")
|
|
677
686
|
elif _lp_code_sdes not in (0, 200):
|
|
@@ -3604,6 +3613,12 @@ class _SdesOpenMixin:
|
|
|
3604
3613
|
stdout=subprocess.DEVNULL,
|
|
3605
3614
|
stderr=subprocess.PIPE,
|
|
3606
3615
|
)
|
|
3616
|
+
# Point the shared holder at the live proc immediately.
|
|
3617
|
+
# The bridge thread polls _proc_holder[0]; if it still
|
|
3618
|
+
# sees the terminated old proc it logs "stream ended",
|
|
3619
|
+
# breaks, and closes the loopback sockets - starving the
|
|
3620
|
+
# restarted ffmpeg (0-frame stream).
|
|
3621
|
+
_proc_holder[0] = proc
|
|
3607
3622
|
_status("ffmpeg restarted with camera's SRTP keys")
|
|
3608
3623
|
except TimeoutError:
|
|
3609
3624
|
if _cam_echo_received:
|
|
@@ -1000,32 +1000,13 @@ class _WebRTCOpenMixin:
|
|
|
1000
1000
|
"lowPowerActiveState HTTP failed for %s: %s", device_id, _we
|
|
1001
1001
|
)
|
|
1002
1002
|
|
|
1003
|
-
async def _http_keepalive() -> None:
|
|
1004
|
-
# setKeepAliveTime keeps the camera in active state for 25 s
|
|
1005
|
-
# after wake (n.java:224 - keepAliveTime=25, not 20).
|
|
1006
|
-
# Without this call the camera's built-in timer may return it
|
|
1007
|
-
# to sleep before SCTP + LIVING completes (~10-15 s).
|
|
1008
|
-
try:
|
|
1009
|
-
import aiohttp as _aiohttp_k
|
|
1010
|
-
async with _aiohttp_k.ClientSession() as _ks:
|
|
1011
|
-
async with _ks.post(
|
|
1012
|
-
f"{self._aidot_v32_base}/devices/{device_id}"
|
|
1013
|
-
"/setKeepAliveTime",
|
|
1014
|
-
json={"keepAliveTime": 25},
|
|
1015
|
-
headers=self._aidot_headers(),
|
|
1016
|
-
timeout=_aiohttp_k.ClientTimeout(total=8),
|
|
1017
|
-
) as _kr:
|
|
1018
|
-
_LOGGER.debug(
|
|
1019
|
-
"setKeepAliveTime HTTP %d for %s",
|
|
1020
|
-
_kr.status, device_id,
|
|
1021
|
-
)
|
|
1022
|
-
except Exception as _ke:
|
|
1023
|
-
_LOGGER.debug(
|
|
1024
|
-
"setKeepAliveTime HTTP failed for %s: %s", device_id, _ke
|
|
1025
|
-
)
|
|
1026
|
-
|
|
1027
1003
|
_spawn_bg(_http_wake())
|
|
1028
|
-
|
|
1004
|
+
# setKeepAliveTime keeps the camera active for ~25 s after wake so the
|
|
1005
|
+
# SCTP + LIVING handshake can complete. For BATTERY cameras a renewal
|
|
1006
|
+
# loop (CameraMixin._keepalive_renew_loop, started by start_keepalive)
|
|
1007
|
+
# then re-issues it every 20 s for the whole view - app parity so the
|
|
1008
|
+
# camera doesn't return to sleep mid-stream.
|
|
1009
|
+
_spawn_bg(self._async_set_keep_alive())
|
|
1029
1010
|
|
|
1030
1011
|
outgoing_q.put_nowait(
|
|
1031
1012
|
(f"iot/v1/s/{user_id}/IPC/getIceConfigReq", _ice_req_payload)
|
|
@@ -1156,6 +1137,7 @@ class _WebRTCOpenMixin:
|
|
|
1156
1137
|
# transient on battery cameras and recover via ICE; abort
|
|
1157
1138
|
# there would spuriously kill otherwise-good streams.
|
|
1158
1139
|
if _lp_on == 0:
|
|
1140
|
+
outgoing_q.put_nowait(None) # stop MQTT thread (avoid orphan)
|
|
1159
1141
|
raise RuntimeError(
|
|
1160
1142
|
f"livePlay refused by camera (livePlay=0, code={_lp_code})"
|
|
1161
1143
|
)
|
|
@@ -1207,6 +1189,7 @@ class _WebRTCOpenMixin:
|
|
|
1207
1189
|
_lp_code2 = int(_lp_resp2.get("code", 200))
|
|
1208
1190
|
_lp_on2 = int(_lp_resp2.get("livePlay", 1))
|
|
1209
1191
|
if _lp_on2 == 0:
|
|
1192
|
+
outgoing_q.put_nowait(None) # stop MQTT thread (avoid orphan)
|
|
1210
1193
|
raise RuntimeError(
|
|
1211
1194
|
f"livePlay refused by camera (livePlay=0, code={_lp_code2})"
|
|
1212
1195
|
)
|
|
@@ -3210,6 +3193,7 @@ class _WebRTCOpenMixin:
|
|
|
3210
3193
|
# encoder is still cold. Tear down and raise so the serve loop
|
|
3211
3194
|
# fast-retries in a bounded burst instead of letting DTLS
|
|
3212
3195
|
# co-fail into the full 15s-gated generic retry.
|
|
3196
|
+
outgoing_q.put_nowait(None) # stop MQTT thread (avoid orphan)
|
|
3213
3197
|
try:
|
|
3214
3198
|
await pc.close()
|
|
3215
3199
|
except Exception:
|
|
@@ -38,6 +38,8 @@ from .const import (
|
|
|
38
38
|
SUPPORTED_COUNTRYS,
|
|
39
39
|
DEFAULT_COUNTRY_CODE,
|
|
40
40
|
CONF_IS_OWNER,
|
|
41
|
+
LOGIN_INFO_PERSISTENT_MQTT_KEY,
|
|
42
|
+
LOGIN_INFO_PERSISTENT_MQTT_LOCK_KEY,
|
|
41
43
|
RUNTIME_ONLY_LOGIN_INFO_KEYS,
|
|
42
44
|
ServerErrorCode,
|
|
43
45
|
)
|
|
@@ -567,7 +569,12 @@ class AidotClient:
|
|
|
567
569
|
# Close the account-shared persistent MQTT connection, if one was opened
|
|
568
570
|
# (AIDOT_PERSISTENT_MQTT). Stored on the shared login_info by the camera
|
|
569
571
|
# command/attr paths; closing here stops its background paho loop.
|
|
570
|
-
pm =
|
|
572
|
+
pm = None
|
|
573
|
+
if isinstance(self.login_info, dict):
|
|
574
|
+
pm = self.login_info.pop(LOGIN_INFO_PERSISTENT_MQTT_KEY, None)
|
|
575
|
+
# Also drop the lock so the asyncio.Lock does not linger on the
|
|
576
|
+
# account-shared login_info after close.
|
|
577
|
+
self.login_info.pop(LOGIN_INFO_PERSISTENT_MQTT_LOCK_KEY, None)
|
|
571
578
|
if pm is not None:
|
|
572
579
|
try:
|
|
573
580
|
pm.close()
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-aidot-cameras"
|
|
7
|
-
version = "0.11.
|
|
7
|
+
version = "0.11.5"
|
|
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"
|
|
@@ -22,7 +22,7 @@ classifiers = [
|
|
|
22
22
|
# after re-validating against it.
|
|
23
23
|
dependencies = [
|
|
24
24
|
"aiohttp>=3.9",
|
|
25
|
-
"paho-mqtt>=
|
|
25
|
+
"paho-mqtt>=1.6.1",
|
|
26
26
|
"cryptography>=42.0",
|
|
27
27
|
"pycryptodome>=3.20",
|
|
28
28
|
"dacite>=1.8",
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5/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.11.
|
|
3
|
+
Version: 0.11.5
|
|
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
|
|
@@ -12,7 +12,7 @@ Requires-Python: >=3.11
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: aiohttp>=3.9
|
|
15
|
-
Requires-Dist: paho-mqtt>=
|
|
15
|
+
Requires-Dist: paho-mqtt>=1.6.1
|
|
16
16
|
Requires-Dist: cryptography>=42.0
|
|
17
17
|
Requires-Dist: pycryptodome>=3.20
|
|
18
18
|
Requires-Dist: dacite>=1.8
|
|
@@ -167,7 +167,7 @@ audio, idle release, the sprop cache path) are documented in
|
|
|
167
167
|
| `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) |
|
|
168
168
|
| `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) |
|
|
169
169
|
| `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) |
|
|
170
|
-
| `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
|
|
170
|
+
| `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) |
|
|
171
171
|
| `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) |
|
|
172
172
|
| `AIDOT_LIVESTREAM_PARAM` | Set to `0` to skip the cloud `liveStreamParam` pre-connect that provisions battery cameras' live-stream sessions before signaling (without it, battery cameras like the L2 models reject streaming with `-50019`). | `1` (enabled) |
|
|
173
173
|
|
|
@@ -85,6 +85,7 @@ tests/test_go2rtc.py
|
|
|
85
85
|
tests/test_go2rtc_cli.py
|
|
86
86
|
tests/test_highport_nomination.py
|
|
87
87
|
tests/test_ice_config_cache.py
|
|
88
|
+
tests/test_keepalive_renew.py
|
|
88
89
|
tests/test_keyframe_prompter.py
|
|
89
90
|
tests/test_lan_control.py
|
|
90
91
|
tests/test_live_stream_param.py
|
|
@@ -117,5 +118,6 @@ tests/test_stream_idle.py
|
|
|
117
118
|
tests/test_stream_teardown.py
|
|
118
119
|
tests/test_talk.py
|
|
119
120
|
tests/test_terminal_ack.py
|
|
121
|
+
tests/test_token_file_robustness.py
|
|
120
122
|
tests/test_token_refresh.py
|
|
121
123
|
tests/test_wait_or_event.py
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Battery-camera setKeepAliveTime renewal during streaming (app parity).
|
|
2
|
+
|
|
3
|
+
The official app re-issues setKeepAliveTime throughout a live view so a battery
|
|
4
|
+
camera's low-power timer doesn't return it to sleep mid-stream. We renew inside
|
|
5
|
+
the 25 s window; mains cameras never sleep and are skipped.
|
|
6
|
+
"""
|
|
7
|
+
import asyncio
|
|
8
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
9
|
+
|
|
10
|
+
from aidot.camera.client import CameraMixin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _obj(battery=True, streaming=True):
|
|
14
|
+
o = MagicMock()
|
|
15
|
+
o.is_battery_camera = battery
|
|
16
|
+
o._streaming_active = streaming
|
|
17
|
+
o.device_id = "cam1"
|
|
18
|
+
o._async_set_keep_alive = AsyncMock()
|
|
19
|
+
return o
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_renew_loop_skips_mains_cameras():
|
|
23
|
+
o = _obj(battery=False)
|
|
24
|
+
asyncio.run(CameraMixin._keepalive_renew_loop(o))
|
|
25
|
+
o._async_set_keep_alive.assert_not_awaited()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_renew_loop_renews_battery_until_stream_stops():
|
|
29
|
+
o = _obj(battery=True)
|
|
30
|
+
n = {"i": 0}
|
|
31
|
+
|
|
32
|
+
async def fake_sleep(_s):
|
|
33
|
+
n["i"] += 1
|
|
34
|
+
if n["i"] >= 3:
|
|
35
|
+
o._streaming_active = False # stop after the 3rd sleep
|
|
36
|
+
|
|
37
|
+
with patch("aidot.camera.client.asyncio.sleep", fake_sleep):
|
|
38
|
+
asyncio.run(CameraMixin._keepalive_renew_loop(o))
|
|
39
|
+
# sleep(renew), sleep(renew), sleep(stop -> break): 2 renewals
|
|
40
|
+
assert o._async_set_keep_alive.await_count == 2
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_renew_loop_swallows_cancel():
|
|
44
|
+
o = _obj(battery=True)
|
|
45
|
+
|
|
46
|
+
async def _run():
|
|
47
|
+
t = asyncio.ensure_future(CameraMixin._keepalive_renew_loop(o))
|
|
48
|
+
await asyncio.sleep(0) # let it reach the first (real) sleep
|
|
49
|
+
t.cancel() # cancel interrupts the sleep immediately
|
|
50
|
+
await t # CancelledError is swallowed inside the loop
|
|
51
|
+
|
|
52
|
+
asyncio.run(_run())
|
|
53
|
+
o._async_set_keep_alive.assert_not_awaited()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_start_renew_is_noop_for_mains_cameras():
|
|
57
|
+
# Mains cameras never sleep - no throwaway task should be scheduled.
|
|
58
|
+
o = _obj(battery=False)
|
|
59
|
+
with patch("aidot.camera.client.asyncio.ensure_future") as ef:
|
|
60
|
+
CameraMixin._start_keepalive_renew(o)
|
|
61
|
+
ef.assert_not_called()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_start_renew_cancels_a_still_running_prior_loop():
|
|
65
|
+
# Re-view within the renew sleep window must not orphan the prior loop.
|
|
66
|
+
o = _obj(battery=True)
|
|
67
|
+
prior = MagicMock()
|
|
68
|
+
prior.done.return_value = False
|
|
69
|
+
o._keepalive_task = prior
|
|
70
|
+
with patch("aidot.camera.client.asyncio.ensure_future", return_value="new") as ef:
|
|
71
|
+
CameraMixin._start_keepalive_renew(o)
|
|
72
|
+
prior.cancel.assert_called_once() # old loop stopped
|
|
73
|
+
ef.assert_called_once() # exactly one new loop
|
|
74
|
+
assert o._keepalive_task == "new"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_cancel_renew_stops_the_loop_and_clears_the_ref():
|
|
78
|
+
o = _obj(battery=True)
|
|
79
|
+
task = MagicMock()
|
|
80
|
+
task.done.return_value = False
|
|
81
|
+
o._keepalive_task = task
|
|
82
|
+
CameraMixin._cancel_keepalive_renew(o)
|
|
83
|
+
task.cancel.assert_called_once()
|
|
84
|
+
assert o._keepalive_task is None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_set_keep_alive_posts_expected_request():
|
|
88
|
+
o = MagicMock()
|
|
89
|
+
o.device_id = "cam1"
|
|
90
|
+
o._aidot_v32_base = "https://api.example/v3.2"
|
|
91
|
+
o._aidot_headers = MagicMock(return_value={"Authorization": "x"})
|
|
92
|
+
cap = {}
|
|
93
|
+
|
|
94
|
+
class _Resp:
|
|
95
|
+
status = 200
|
|
96
|
+
|
|
97
|
+
async def __aenter__(self):
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
async def __aexit__(self, *a):
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
class _Sess:
|
|
104
|
+
async def __aenter__(self):
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
async def __aexit__(self, *a):
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
def post(self, url, json=None, headers=None, timeout=None):
|
|
111
|
+
cap.update(url=url, json=json, headers=headers)
|
|
112
|
+
return _Resp()
|
|
113
|
+
|
|
114
|
+
with patch("aiohttp.ClientSession", lambda: _Sess()):
|
|
115
|
+
asyncio.run(CameraMixin._async_set_keep_alive(o))
|
|
116
|
+
|
|
117
|
+
assert cap["url"] == "https://api.example/v3.2/devices/cam1/setKeepAliveTime"
|
|
118
|
+
assert cap["json"] == {"keepAliveTime": 25}
|
|
119
|
+
assert cap["headers"] == {"Authorization": "x"}
|
|
@@ -80,3 +80,23 @@ def test_network_helpers_are_callable():
|
|
|
80
80
|
assert inspect.iscoroutinefunction(discover_subnet)
|
|
81
81
|
sig = inspect.signature(discover_subnet)
|
|
82
82
|
assert list(sig.parameters) == ["cidr24", "timeout", "concurrency"]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_get_attributes_empty_replies_raises_lanerror():
|
|
86
|
+
# If the camera accepts login but never answers getDevAttrReq (read timeout),
|
|
87
|
+
# _session returns [] - async_get_attributes must raise CameraLanError (the
|
|
88
|
+
# contract callers rely on for cloud fallback), not an unguarded IndexError.
|
|
89
|
+
import asyncio
|
|
90
|
+
|
|
91
|
+
c = CameraLanClient(DEVICE, USER, ip="192.0.2.10")
|
|
92
|
+
|
|
93
|
+
async def _empty(_build):
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
c._session = _empty
|
|
97
|
+
|
|
98
|
+
async def _go():
|
|
99
|
+
with pytest.raises(CameraLanError):
|
|
100
|
+
await c.async_get_attributes()
|
|
101
|
+
|
|
102
|
+
asyncio.run(_go())
|
|
@@ -24,7 +24,7 @@ class _FakeDC:
|
|
|
24
24
|
self._call = 0
|
|
25
25
|
self._motion_active = True
|
|
26
26
|
self._motion_cb = None
|
|
27
|
-
self._motion_seen = set()
|
|
27
|
+
self._motion_seen: dict = {} # insertion-ordered set (dict keys); see CameraMixin
|
|
28
28
|
self._motion_interval = 0.01 # fast for the test
|
|
29
29
|
|
|
30
30
|
async def async_get_cloud_recordings(self, *a, **k):
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Tests for the standalone CLI's token-cache read/write robustness.
|
|
2
|
+
|
|
3
|
+
Covers the 0.11.3 fixes: the token file is written atomically at 0600 (a
|
|
4
|
+
serialization failure must not truncate a previously-valid token), and a
|
|
5
|
+
corrupt/partial cache is a caught ValueError (so _make_client can fall back
|
|
6
|
+
to username/password login instead of crashing).
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import stat
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from aidot.__main__ import _read_token_file, _write_token_file
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_write_token_file_round_trips_and_is_0600(tmp_path):
|
|
17
|
+
p = tmp_path / "token.json"
|
|
18
|
+
_write_token_file(str(p), {"accessToken": "abc", "id": "u1"})
|
|
19
|
+
assert _read_token_file(str(p)) == {"accessToken": "abc", "id": "u1"}
|
|
20
|
+
assert stat.S_IMODE(os.stat(p).st_mode) == 0o600
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_write_token_file_preserves_previous_on_serialization_failure(tmp_path):
|
|
24
|
+
p = tmp_path / "token.json"
|
|
25
|
+
_write_token_file(str(p), {"accessToken": "good"}) # seed a valid token
|
|
26
|
+
|
|
27
|
+
# A set is not JSON-serializable, so json.dump raises mid-write. The atomic
|
|
28
|
+
# temp-then-replace write must leave the previously-valid token untouched.
|
|
29
|
+
with pytest.raises(TypeError):
|
|
30
|
+
_write_token_file(str(p), {"broken": {1, 2, 3}})
|
|
31
|
+
|
|
32
|
+
assert _read_token_file(str(p)) == {"accessToken": "good"}
|
|
33
|
+
# And it must not leave a stray temp file behind.
|
|
34
|
+
assert [f for f in os.listdir(tmp_path) if f.startswith(".token-")] == []
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_read_token_file_raises_valueerror_on_corrupt(tmp_path):
|
|
38
|
+
p = tmp_path / "token.json"
|
|
39
|
+
p.write_text("{ not valid json", encoding="utf-8")
|
|
40
|
+
# json.JSONDecodeError is a subclass of ValueError - the exact type
|
|
41
|
+
# _make_client catches to fall back to password login rather than crash.
|
|
42
|
+
with pytest.raises(ValueError):
|
|
43
|
+
_read_token_file(str(p))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/base.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/g711.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/g722.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/h264.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/opus.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/vpx.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/contrib/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/contrib/media.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/exceptions.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/jitterbuffer.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/mediastreams.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcconfiguration.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcdatachannel.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcdtlstransport.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcicetransport.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcrtpparameters.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcrtpreceiver.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcrtpsender.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcsctptransport.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/aidot/models/device_client_model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_deferred_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_device_login_guard.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_device_user_info_cache.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_dtls_not_ready_burst.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_dtls_skip_signaling_wait.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_highport_nomination.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_no_undefined_names.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_offline_keepalive_pause.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_persistent_mqtt_robustness.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_post_merge_hardening.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_sdes_echo_wait_timeout.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_sdes_fast_liveplay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_security_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.2 → python_aidot_cameras-0.11.5}/tests/test_serializable_login_info.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|