python-aidot-cameras 0.11.0__tar.gz → 0.11.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_aidot_cameras-0.11.0/python_aidot_cameras.egg-info → python_aidot_cameras-0.11.2}/PKG-INFO +9 -1
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/README.md +8 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/__main__.py +14 -4
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/camera/client.py +71 -7
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/client.py +23 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/const.py +15 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/pyproject.toml +1 -1
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2/python_aidot_cameras.egg-info}/PKG-INFO +9 -1
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/python_aidot_cameras.egg-info/SOURCES.txt +2 -0
- python_aidot_cameras-0.11.2/tests/test_offline_keepalive_pause.py +112 -0
- python_aidot_cameras-0.11.2/tests/test_serializable_login_info.py +82 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/LICENSE +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/__init__.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/aes_utils.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/camera/__init__.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/camera/constants.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/camera/controls.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/camera/models.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/camera/playback.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/camera/protocol.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/camera/sdes.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/camera/sdes_open.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/camera/tutk.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/credentials.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/device_client.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/discover.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/exceptions.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/g711.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/login_const.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/models/__init__.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/models/device_client_model.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/models/device_model.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/models/discover_model.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/py.typed +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/setup.cfg +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_wait_or_event.py +0 -0
{python_aidot_cameras-0.11.0/python_aidot_cameras.egg-info → python_aidot_cameras-0.11.2}/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.2
|
|
4
4
|
Summary: Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)
|
|
5
5
|
Author-email: cbrightly <chris.brightly@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -94,10 +94,18 @@ pipx install "python-aidot-cameras[webrtc]"
|
|
|
94
94
|
# or:
|
|
95
95
|
uv tool install "python-aidot-cameras[webrtc]"
|
|
96
96
|
|
|
97
|
+
export AIDOT_USERNAME=... AIDOT_PASSWORD=... # or AIDOT_TOKEN_FILE, see below
|
|
97
98
|
aidot-go2rtc --list # discover cameras + their transport
|
|
98
99
|
aidot-go2rtc <device_id> '{output}' # stream one camera (as a go2rtc exec: source)
|
|
99
100
|
```
|
|
100
101
|
|
|
102
|
+
Authenticates via `AIDOT_USERNAME`/`AIDOT_PASSWORD` (`AIDOT_COUNTRY`, default
|
|
103
|
+
`US`) - a dedicated login is recommended for long-running standalone use, so
|
|
104
|
+
it doesn't fight Home Assistant over a shared rotating refresh token. Set
|
|
105
|
+
`AIDOT_TOKEN_FILE=/path/to/token.json` to cache the login across restarts;
|
|
106
|
+
token rotations are written back automatically. Run `aidot-go2rtc --help` for
|
|
107
|
+
the full list of authentication and stream env vars.
|
|
108
|
+
|
|
101
109
|
## Usage
|
|
102
110
|
|
|
103
111
|
Open a live WebRTC stream from a camera device client:
|
|
@@ -64,10 +64,18 @@ pipx install "python-aidot-cameras[webrtc]"
|
|
|
64
64
|
# or:
|
|
65
65
|
uv tool install "python-aidot-cameras[webrtc]"
|
|
66
66
|
|
|
67
|
+
export AIDOT_USERNAME=... AIDOT_PASSWORD=... # or AIDOT_TOKEN_FILE, see below
|
|
67
68
|
aidot-go2rtc --list # discover cameras + their transport
|
|
68
69
|
aidot-go2rtc <device_id> '{output}' # stream one camera (as a go2rtc exec: source)
|
|
69
70
|
```
|
|
70
71
|
|
|
72
|
+
Authenticates via `AIDOT_USERNAME`/`AIDOT_PASSWORD` (`AIDOT_COUNTRY`, default
|
|
73
|
+
`US`) - a dedicated login is recommended for long-running standalone use, so
|
|
74
|
+
it doesn't fight Home Assistant over a shared rotating refresh token. Set
|
|
75
|
+
`AIDOT_TOKEN_FILE=/path/to/token.json` to cache the login across restarts;
|
|
76
|
+
token rotations are written back automatically. Run `aidot-go2rtc --help` for
|
|
77
|
+
the full list of authentication and stream env vars.
|
|
78
|
+
|
|
71
79
|
## Usage
|
|
72
80
|
|
|
73
81
|
Open a live WebRTC stream from a camera device client:
|
|
@@ -102,9 +102,17 @@ def _install_token_cache(client: AidotClient, path: str) -> None:
|
|
|
102
102
|
"""
|
|
103
103
|
def _cb() -> None:
|
|
104
104
|
try:
|
|
105
|
-
_write_token_file(path, client.
|
|
105
|
+
_write_token_file(path, client.serializable_login_info())
|
|
106
106
|
_LOGGER.debug("Cached refreshed token to %s", path)
|
|
107
|
-
except OSError as exc:
|
|
107
|
+
except (OSError, TypeError) as exc:
|
|
108
|
+
# TypeError: a defensive belt-and-suspenders catch alongside
|
|
109
|
+
# serializable_login_info() itself - if a future runtime-only key
|
|
110
|
+
# is ever added to login_info without also being added to
|
|
111
|
+
# RUNTIME_ONLY_LOGIN_INFO_KEYS, this keeps a caching bug from
|
|
112
|
+
# ever propagating out of a callback and interrupting a token
|
|
113
|
+
# refresh that had otherwise already succeeded (confirmed live:
|
|
114
|
+
# this exact TypeError - "Object of type Lock is not JSON
|
|
115
|
+
# serializable" - previously escaped this callback uncaught).
|
|
108
116
|
_LOGGER.warning("Could not cache refreshed token to %s: %s", path, exc)
|
|
109
117
|
|
|
110
118
|
client.set_token_fresh_cb(_cb)
|
|
@@ -149,8 +157,10 @@ async def _make_client(session: aiohttp.ClientSession) -> AidotClient:
|
|
|
149
157
|
if token_file:
|
|
150
158
|
_install_token_cache(client, token_file)
|
|
151
159
|
try:
|
|
152
|
-
await loop.run_in_executor(
|
|
153
|
-
|
|
160
|
+
await loop.run_in_executor(
|
|
161
|
+
None, _write_token_file, token_file, client.serializable_login_info()
|
|
162
|
+
)
|
|
163
|
+
except (OSError, TypeError) as exc:
|
|
154
164
|
_LOGGER.warning("Could not seed token cache %s: %s", token_file, exc)
|
|
155
165
|
return client
|
|
156
166
|
|
|
@@ -10,6 +10,10 @@ from typing import Any, Callable, List, Optional
|
|
|
10
10
|
|
|
11
11
|
from ..exceptions import AidotCameraBusy, AidotCameraNotReady
|
|
12
12
|
from ..login_const import APP_ID as _AIDOT_APP_ID
|
|
13
|
+
from ..const import (
|
|
14
|
+
LOGIN_INFO_PERSISTENT_MQTT_KEY,
|
|
15
|
+
LOGIN_INFO_PERSISTENT_MQTT_LOCK_KEY,
|
|
16
|
+
)
|
|
13
17
|
|
|
14
18
|
# Wire/protocol constants live in constants.py; re-imported here so all
|
|
15
19
|
# in-module references (and any external importers) keep resolving.
|
|
@@ -86,6 +90,13 @@ from .protocol import ( # noqa: F401 - re-exported for device_client / back-com
|
|
|
86
90
|
|
|
87
91
|
_LOGGER = logging.getLogger(__name__)
|
|
88
92
|
|
|
93
|
+
# Offline keepalive pause (see CameraMixin._backoff_or_offline_pause): while the
|
|
94
|
+
# cloud explicitly reports a device offline, failed-open retries re-check the
|
|
95
|
+
# flag every RECHECK seconds and only probe a real open every PROBE seconds,
|
|
96
|
+
# instead of consuming an open-gate slot on the normal reconnect cadence.
|
|
97
|
+
_OFFLINE_RECHECK_S = float(os.environ.get("AIDOT_OFFLINE_RECHECK_S", "30"))
|
|
98
|
+
_OFFLINE_PROBE_S = float(os.environ.get("AIDOT_OFFLINE_PROBE_S", "600"))
|
|
99
|
+
|
|
89
100
|
# Strong refs to fire-and-forget tasks: asyncio only keeps weak refs, so a
|
|
90
101
|
# discarded task can be garbage-collected mid-flight. Discarded on completion.
|
|
91
102
|
_BG_TASKS: set = set()
|
|
@@ -761,8 +772,56 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
761
772
|
self.status.update_from_camera_attributes(props)
|
|
762
773
|
if device.get("online") is not None:
|
|
763
774
|
self.status.online = bool(device.get("online"))
|
|
775
|
+
# The cloud explicitly reported reachability - only then may the
|
|
776
|
+
# keepalive loops trust an offline flag (see
|
|
777
|
+
# _backoff_or_offline_pause; DeviceStatusData.online defaults to
|
|
778
|
+
# False, which must not read as "offline").
|
|
779
|
+
self._cloud_online_explicit = True
|
|
764
780
|
return self.status
|
|
765
781
|
|
|
782
|
+
async def _backoff_or_offline_pause(self, delay: float) -> None:
|
|
783
|
+
"""Sleep a reconnect backoff, extended while the device is cloud-offline.
|
|
784
|
+
|
|
785
|
+
Normal case (device online, or the cloud never reported an online
|
|
786
|
+
flag): plain ``asyncio.sleep(delay)`` - behavior unchanged.
|
|
787
|
+
|
|
788
|
+
When an open just failed AND the cloud has explicitly reported the
|
|
789
|
+
device offline (``update_status_from_device`` saw ``online: false``),
|
|
790
|
+
hold here instead of retrying on the pacer cadence: re-check the flag
|
|
791
|
+
every ``AIDOT_OFFLINE_RECHECK_S`` (default 30 s) and resume the moment
|
|
792
|
+
it flips back on, or fall through after ``AIDOT_OFFLINE_PROBE_S``
|
|
793
|
+
(default 600 s) so one probe open still runs in case the cloud flag is
|
|
794
|
+
stale. This keeps a dead/unpowered camera from consuming an open-gate
|
|
795
|
+
slot every backoff cycle and starving live cameras' cold opens
|
|
796
|
+
(observed live: two unpowered A000088s cycling 30 s open attempts
|
|
797
|
+
pushed a healthy camera's cold open past two minutes).
|
|
798
|
+
"""
|
|
799
|
+
if self.status.online or not getattr(self, "_cloud_online_explicit", False):
|
|
800
|
+
await asyncio.sleep(delay)
|
|
801
|
+
return
|
|
802
|
+
_LOGGER.info(
|
|
803
|
+
"keepalive[%s]: device is cloud-offline - pausing open retries "
|
|
804
|
+
"(recheck %.0fs, probe %.0fs)",
|
|
805
|
+
self.device_id, _OFFLINE_RECHECK_S, _OFFLINE_PROBE_S,
|
|
806
|
+
)
|
|
807
|
+
_t0 = time.monotonic()
|
|
808
|
+
while (_elapsed := time.monotonic() - _t0) < _OFFLINE_PROBE_S:
|
|
809
|
+
await asyncio.sleep(
|
|
810
|
+
min(_OFFLINE_RECHECK_S, max(0.1, _OFFLINE_PROBE_S - _elapsed))
|
|
811
|
+
)
|
|
812
|
+
if not getattr(self, "_streaming_active", False):
|
|
813
|
+
return
|
|
814
|
+
if self.status.online:
|
|
815
|
+
_LOGGER.info(
|
|
816
|
+
"keepalive[%s]: device back online - resuming opens",
|
|
817
|
+
self.device_id,
|
|
818
|
+
)
|
|
819
|
+
return
|
|
820
|
+
_LOGGER.debug(
|
|
821
|
+
"keepalive[%s]: offline probe interval elapsed - probing one open",
|
|
822
|
+
self.device_id,
|
|
823
|
+
)
|
|
824
|
+
|
|
766
825
|
# -- Camera helpers ------------------------------------------------------ #
|
|
767
826
|
|
|
768
827
|
@property
|
|
@@ -2560,7 +2619,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2560
2619
|
self.device_id, _delay, exc,
|
|
2561
2620
|
)
|
|
2562
2621
|
try:
|
|
2563
|
-
await
|
|
2622
|
+
await self._backoff_or_offline_pause(_delay)
|
|
2564
2623
|
except asyncio.CancelledError:
|
|
2565
2624
|
return
|
|
2566
2625
|
continue
|
|
@@ -2735,7 +2794,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2735
2794
|
self.device_id, _delay, exc,
|
|
2736
2795
|
)
|
|
2737
2796
|
try:
|
|
2738
|
-
await
|
|
2797
|
+
await self._backoff_or_offline_pause(_delay)
|
|
2739
2798
|
except asyncio.CancelledError:
|
|
2740
2799
|
return
|
|
2741
2800
|
continue
|
|
@@ -3041,7 +3100,12 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3041
3100
|
li = self._user_info if isinstance(getattr(self, "_user_info", None), dict) else None
|
|
3042
3101
|
if li is None:
|
|
3043
3102
|
return None
|
|
3044
|
-
|
|
3103
|
+
# These two keys are deliberately live runtime objects (a connection,
|
|
3104
|
+
# its guarding lock), NOT persistable state - see
|
|
3105
|
+
# RUNTIME_ONLY_LOGIN_INFO_KEYS and AidotClient.serializable_login_info(),
|
|
3106
|
+
# which strips them before this shared dict is ever written to
|
|
3107
|
+
# disk/config storage.
|
|
3108
|
+
pm = li.get(LOGIN_INFO_PERSISTENT_MQTT_KEY)
|
|
3045
3109
|
if pm is not None:
|
|
3046
3110
|
return pm
|
|
3047
3111
|
# Serialize get-or-create. Without this, two concurrent first-callers
|
|
@@ -3050,9 +3114,9 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3050
3114
|
# clobbering the first and orphaning a connection on the single-client_id
|
|
3051
3115
|
# broker. dict.setdefault is atomic (no await between create and insert),
|
|
3052
3116
|
# so every caller for this account shares the one lock.
|
|
3053
|
-
lock = li.setdefault(
|
|
3117
|
+
lock = li.setdefault(LOGIN_INFO_PERSISTENT_MQTT_LOCK_KEY, asyncio.Lock())
|
|
3054
3118
|
async with lock:
|
|
3055
|
-
pm = li.get(
|
|
3119
|
+
pm = li.get(LOGIN_INFO_PERSISTENT_MQTT_KEY) # re-check under the lock
|
|
3056
3120
|
if pm is not None:
|
|
3057
3121
|
return pm
|
|
3058
3122
|
smarthome_auth = await self._async_get_smarthome_auth()
|
|
@@ -3064,7 +3128,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3064
3128
|
return None
|
|
3065
3129
|
from .protocol import _PersistentMqtt
|
|
3066
3130
|
pm = _PersistentMqtt(mqtt_url, mqtt_user, mqtt_pwd, client_id)
|
|
3067
|
-
li[
|
|
3131
|
+
li[LOGIN_INFO_PERSISTENT_MQTT_KEY] = pm
|
|
3068
3132
|
return pm
|
|
3069
3133
|
|
|
3070
3134
|
@staticmethod
|
|
@@ -3231,7 +3295,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3231
3295
|
self.device_id, _delay, exc,
|
|
3232
3296
|
)
|
|
3233
3297
|
try:
|
|
3234
|
-
await
|
|
3298
|
+
await self._backoff_or_offline_pause(_delay)
|
|
3235
3299
|
except asyncio.CancelledError:
|
|
3236
3300
|
return
|
|
3237
3301
|
continue
|
|
@@ -38,6 +38,7 @@ from .const import (
|
|
|
38
38
|
SUPPORTED_COUNTRYS,
|
|
39
39
|
DEFAULT_COUNTRY_CODE,
|
|
40
40
|
CONF_IS_OWNER,
|
|
41
|
+
RUNTIME_ONLY_LOGIN_INFO_KEYS,
|
|
41
42
|
ServerErrorCode,
|
|
42
43
|
)
|
|
43
44
|
|
|
@@ -346,6 +347,28 @@ class AidotClient:
|
|
|
346
347
|
raise AidotAuthFailed
|
|
347
348
|
return None
|
|
348
349
|
|
|
350
|
+
def serializable_login_info(self) -> dict[str, Any]:
|
|
351
|
+
"""A JSON-safe copy of ``login_info`` for persisting to disk/config storage.
|
|
352
|
+
|
|
353
|
+
``login_info`` doubles as the account-shared cache for the
|
|
354
|
+
persistent-MQTT connection and its guarding ``asyncio.Lock`` (see
|
|
355
|
+
``camera/client.py``'s ``_get_persistent_mqtt``, which reads/writes
|
|
356
|
+
this identical dict object via ``DeviceClient._user_info``) - live
|
|
357
|
+
runtime objects that raise ``TypeError: Object of type Lock is not
|
|
358
|
+
JSON serializable`` if ever handed to ``json.dump`` directly.
|
|
359
|
+
Confirmed live: a token-refresh callback that persisted the raw
|
|
360
|
+
``login_info`` dict hit exactly this once a persistent MQTT
|
|
361
|
+
connection was active (the default since 2026-06-17).
|
|
362
|
+
|
|
363
|
+
Anything that persists ``login_info`` - this library's own
|
|
364
|
+
standalone CLI, or an integration's config-entry storage - should
|
|
365
|
+
call this instead of serializing ``login_info`` directly.
|
|
366
|
+
"""
|
|
367
|
+
return {
|
|
368
|
+
k: v for k, v in self.login_info.items()
|
|
369
|
+
if k not in RUNTIME_ONLY_LOGIN_INFO_KEYS
|
|
370
|
+
}
|
|
371
|
+
|
|
349
372
|
async def async_ensure_token(self) -> bool:
|
|
350
373
|
"""Force a fresh access token for camera/smarthome HTTP calls.
|
|
351
374
|
|
|
@@ -230,6 +230,21 @@ class ServerErrorCode(IntEnum):
|
|
|
230
230
|
USER_PWD_INCORRECT = 560080
|
|
231
231
|
|
|
232
232
|
|
|
233
|
+
# AidotClient.login_info is also used as the account-shared cache for the
|
|
234
|
+
# persistent-MQTT connection + its guarding asyncio.Lock (see
|
|
235
|
+
# camera/client.py's _get_persistent_mqtt: one connection per account,
|
|
236
|
+
# looked up on this same shared dict via DeviceClient._user_info - the
|
|
237
|
+
# identical object, not a copy). Those are live runtime objects, never
|
|
238
|
+
# persistable state. Anything that serializes login_info to disk (this
|
|
239
|
+
# library's own standalone CLI, or an integration's config-entry storage)
|
|
240
|
+
# must exclude these keys first - see AidotClient.serializable_login_info().
|
|
241
|
+
LOGIN_INFO_PERSISTENT_MQTT_KEY = "_persistent_mqtt"
|
|
242
|
+
LOGIN_INFO_PERSISTENT_MQTT_LOCK_KEY = "_persistent_mqtt_lock"
|
|
243
|
+
RUNTIME_ONLY_LOGIN_INFO_KEYS = frozenset(
|
|
244
|
+
{LOGIN_INFO_PERSISTENT_MQTT_KEY, LOGIN_INFO_PERSISTENT_MQTT_LOCK_KEY}
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
233
248
|
# -- Cloud API credentials ---------------------------------------------------- #
|
|
234
249
|
|
|
235
250
|
from .login_const import APP_ID # noqa: E402, F401 - back-compat re-export
|
|
@@ -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.2"
|
|
8
8
|
description = "Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2/python_aidot_cameras.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.2
|
|
4
4
|
Summary: Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)
|
|
5
5
|
Author-email: cbrightly <chris.brightly@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -94,10 +94,18 @@ pipx install "python-aidot-cameras[webrtc]"
|
|
|
94
94
|
# or:
|
|
95
95
|
uv tool install "python-aidot-cameras[webrtc]"
|
|
96
96
|
|
|
97
|
+
export AIDOT_USERNAME=... AIDOT_PASSWORD=... # or AIDOT_TOKEN_FILE, see below
|
|
97
98
|
aidot-go2rtc --list # discover cameras + their transport
|
|
98
99
|
aidot-go2rtc <device_id> '{output}' # stream one camera (as a go2rtc exec: source)
|
|
99
100
|
```
|
|
100
101
|
|
|
102
|
+
Authenticates via `AIDOT_USERNAME`/`AIDOT_PASSWORD` (`AIDOT_COUNTRY`, default
|
|
103
|
+
`US`) - a dedicated login is recommended for long-running standalone use, so
|
|
104
|
+
it doesn't fight Home Assistant over a shared rotating refresh token. Set
|
|
105
|
+
`AIDOT_TOKEN_FILE=/path/to/token.json` to cache the login across restarts;
|
|
106
|
+
token rotations are written back automatically. Run `aidot-go2rtc --help` for
|
|
107
|
+
the full list of authentication and stream env vars.
|
|
108
|
+
|
|
101
109
|
## Usage
|
|
102
110
|
|
|
103
111
|
Open a live WebRTC stream from a camera device client:
|
|
@@ -91,6 +91,7 @@ tests/test_live_stream_param.py
|
|
|
91
91
|
tests/test_motion_poll.py
|
|
92
92
|
tests/test_narrow_pc_ice.py
|
|
93
93
|
tests/test_no_undefined_names.py
|
|
94
|
+
tests/test_offline_keepalive_pause.py
|
|
94
95
|
tests/test_open_gate_delay.py
|
|
95
96
|
tests/test_persistent_mqtt.py
|
|
96
97
|
tests/test_persistent_mqtt_robustness.py
|
|
@@ -107,6 +108,7 @@ tests/test_sdes_sprop.py
|
|
|
107
108
|
tests/test_sdes_talk.py
|
|
108
109
|
tests/test_sdes_watchdog.py
|
|
109
110
|
tests/test_security_hardening.py
|
|
111
|
+
tests/test_serializable_login_info.py
|
|
110
112
|
tests/test_serve_relay.py
|
|
111
113
|
tests/test_session_stats.py
|
|
112
114
|
tests/test_speak.py
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Offline keepalive pause: failed-open retries hold while the device is
|
|
2
|
+
cloud-offline (CameraMixin._backoff_or_offline_pause) instead of consuming an
|
|
3
|
+
open-gate slot on the normal reconnect cadence."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
|
|
9
|
+
import aidot.camera.client as camera_client
|
|
10
|
+
from aidot.camera.client import CameraMixin
|
|
11
|
+
from aidot.device_client import DeviceStatusData
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _stub(online: bool, explicit: bool, streaming: bool = True) -> SimpleNamespace:
|
|
15
|
+
s = SimpleNamespace(
|
|
16
|
+
status=DeviceStatusData(),
|
|
17
|
+
device_id="test-device",
|
|
18
|
+
_streaming_active=streaming,
|
|
19
|
+
)
|
|
20
|
+
s.status.online = online
|
|
21
|
+
if explicit:
|
|
22
|
+
s._cloud_online_explicit = True
|
|
23
|
+
return s
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _run(coro):
|
|
27
|
+
return asyncio.run(coro)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_online_device_sleeps_plain_delay():
|
|
31
|
+
stub = _stub(online=True, explicit=True)
|
|
32
|
+
t0 = time.monotonic()
|
|
33
|
+
_run(CameraMixin._backoff_or_offline_pause(stub, 0.05))
|
|
34
|
+
took = time.monotonic() - t0
|
|
35
|
+
assert 0.04 <= took < 1.0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_unreported_online_state_never_pauses(monkeypatch):
|
|
39
|
+
# DeviceStatusData.online defaults to False; without the explicit marker
|
|
40
|
+
# that must NOT read as "offline" (a long pause here would slow every
|
|
41
|
+
# camera whose device dict was never merged).
|
|
42
|
+
monkeypatch.setattr(camera_client, "_OFFLINE_PROBE_S", 60.0)
|
|
43
|
+
stub = _stub(online=False, explicit=False)
|
|
44
|
+
t0 = time.monotonic()
|
|
45
|
+
_run(CameraMixin._backoff_or_offline_pause(stub, 0.05))
|
|
46
|
+
took = time.monotonic() - t0
|
|
47
|
+
assert took < 1.0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_offline_device_holds_then_probes(monkeypatch):
|
|
51
|
+
# Explicitly offline: the pause holds past the plain delay and falls
|
|
52
|
+
# through after the probe interval so one probe open still runs.
|
|
53
|
+
monkeypatch.setattr(camera_client, "_OFFLINE_RECHECK_S", 0.05)
|
|
54
|
+
monkeypatch.setattr(camera_client, "_OFFLINE_PROBE_S", 0.3)
|
|
55
|
+
stub = _stub(online=False, explicit=True)
|
|
56
|
+
t0 = time.monotonic()
|
|
57
|
+
_run(CameraMixin._backoff_or_offline_pause(stub, 0.01))
|
|
58
|
+
took = time.monotonic() - t0
|
|
59
|
+
assert took >= 0.3 # held for the whole probe interval
|
|
60
|
+
assert took < 2.0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_offline_device_resumes_when_back_online(monkeypatch):
|
|
64
|
+
monkeypatch.setattr(camera_client, "_OFFLINE_RECHECK_S", 0.05)
|
|
65
|
+
monkeypatch.setattr(camera_client, "_OFFLINE_PROBE_S", 30.0)
|
|
66
|
+
stub = _stub(online=False, explicit=True)
|
|
67
|
+
|
|
68
|
+
async def scenario():
|
|
69
|
+
async def flip():
|
|
70
|
+
await asyncio.sleep(0.15)
|
|
71
|
+
stub.status.online = True
|
|
72
|
+
|
|
73
|
+
t0 = time.monotonic()
|
|
74
|
+
await asyncio.gather(
|
|
75
|
+
CameraMixin._backoff_or_offline_pause(stub, 0.01), flip()
|
|
76
|
+
)
|
|
77
|
+
return time.monotonic() - t0
|
|
78
|
+
|
|
79
|
+
took = _run(scenario())
|
|
80
|
+
# resumed within a couple of recheck ticks of the flip, far short of PROBE
|
|
81
|
+
assert 0.15 <= took < 2.0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_offline_pause_exits_when_streaming_stops(monkeypatch):
|
|
85
|
+
monkeypatch.setattr(camera_client, "_OFFLINE_RECHECK_S", 0.05)
|
|
86
|
+
monkeypatch.setattr(camera_client, "_OFFLINE_PROBE_S", 30.0)
|
|
87
|
+
stub = _stub(online=False, explicit=True)
|
|
88
|
+
|
|
89
|
+
async def scenario():
|
|
90
|
+
async def stop():
|
|
91
|
+
await asyncio.sleep(0.15)
|
|
92
|
+
stub._streaming_active = False
|
|
93
|
+
|
|
94
|
+
t0 = time.monotonic()
|
|
95
|
+
await asyncio.gather(
|
|
96
|
+
CameraMixin._backoff_or_offline_pause(stub, 0.01), stop()
|
|
97
|
+
)
|
|
98
|
+
return time.monotonic() - t0
|
|
99
|
+
|
|
100
|
+
took = _run(scenario())
|
|
101
|
+
assert took < 2.0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_update_status_sets_explicit_marker():
|
|
105
|
+
stub = SimpleNamespace(status=DeviceStatusData())
|
|
106
|
+
CameraMixin.update_status_from_device(stub, {"online": False})
|
|
107
|
+
assert stub.status.online is False
|
|
108
|
+
assert getattr(stub, "_cloud_online_explicit", False) is True
|
|
109
|
+
|
|
110
|
+
stub2 = SimpleNamespace(status=DeviceStatusData())
|
|
111
|
+
CameraMixin.update_status_from_device(stub2, {"name": "no online key"})
|
|
112
|
+
assert getattr(stub2, "_cloud_online_explicit", False) is False
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Regression test for a live bug: AidotClient.login_info doubles as the
|
|
2
|
+
account-shared cache for the persistent-MQTT connection and its guarding
|
|
3
|
+
asyncio.Lock (camera/client.py's _get_persistent_mqtt), so anything that
|
|
4
|
+
json.dumps login_info directly - once a persistent MQTT connection has been
|
|
5
|
+
created (the default since 2026-06-17) - hits
|
|
6
|
+
"TypeError: Object of type Lock is not JSON serializable".
|
|
7
|
+
|
|
8
|
+
Confirmed live (2026-07-07): a standalone aidot-go2rtc run's token-refresh
|
|
9
|
+
callback threw exactly this while persisting client.login_info to disk.
|
|
10
|
+
serializable_login_info() is the fix: a JSON-safe view that excludes the
|
|
11
|
+
known runtime-only keys.
|
|
12
|
+
"""
|
|
13
|
+
import asyncio
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
19
|
+
|
|
20
|
+
from aidot.client import AidotClient
|
|
21
|
+
from aidot.const import (
|
|
22
|
+
LOGIN_INFO_PERSISTENT_MQTT_KEY,
|
|
23
|
+
LOGIN_INFO_PERSISTENT_MQTT_LOCK_KEY,
|
|
24
|
+
RUNTIME_ONLY_LOGIN_INFO_KEYS,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _client_with_login_info(extra: dict) -> AidotClient:
|
|
29
|
+
c = AidotClient.__new__(AidotClient)
|
|
30
|
+
c.login_info = {
|
|
31
|
+
"id": "u1",
|
|
32
|
+
"accessToken": "tok",
|
|
33
|
+
"refreshToken": "rtok",
|
|
34
|
+
"mqttPassword": "mp",
|
|
35
|
+
**extra,
|
|
36
|
+
}
|
|
37
|
+
return c
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_serializable_login_info_strips_runtime_keys():
|
|
41
|
+
c = _client_with_login_info({
|
|
42
|
+
LOGIN_INFO_PERSISTENT_MQTT_LOCK_KEY: asyncio.Lock(),
|
|
43
|
+
LOGIN_INFO_PERSISTENT_MQTT_KEY: object(), # stand-in for a live _PersistentMqtt
|
|
44
|
+
})
|
|
45
|
+
result = c.serializable_login_info()
|
|
46
|
+
assert LOGIN_INFO_PERSISTENT_MQTT_KEY not in result
|
|
47
|
+
assert LOGIN_INFO_PERSISTENT_MQTT_LOCK_KEY not in result
|
|
48
|
+
# Everything else survives unchanged.
|
|
49
|
+
assert result == {
|
|
50
|
+
"id": "u1", "accessToken": "tok", "refreshToken": "rtok",
|
|
51
|
+
"mqttPassword": "mp",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_serializable_login_info_is_actually_json_dumpable():
|
|
56
|
+
# The exact failure mode this fixes: json.dumps on the raw dict must
|
|
57
|
+
# raise, but on the filtered view it must not.
|
|
58
|
+
c = _client_with_login_info({
|
|
59
|
+
LOGIN_INFO_PERSISTENT_MQTT_LOCK_KEY: asyncio.Lock(),
|
|
60
|
+
})
|
|
61
|
+
try:
|
|
62
|
+
json.dumps(c.login_info)
|
|
63
|
+
raise AssertionError("expected TypeError serializing the raw login_info")
|
|
64
|
+
except TypeError:
|
|
65
|
+
pass
|
|
66
|
+
json.dumps(c.serializable_login_info()) # must not raise
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_serializable_login_info_noop_when_no_runtime_keys_present():
|
|
70
|
+
# Common case (no persistent-MQTT connection created yet): unaffected.
|
|
71
|
+
c = _client_with_login_info({})
|
|
72
|
+
assert c.serializable_login_info() == c.login_info
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_runtime_only_keys_constant_matches_persistent_mqtt_key_names():
|
|
76
|
+
# Locks the two literal key names camera/client.py's _get_persistent_mqtt
|
|
77
|
+
# actually writes (see LOGIN_INFO_PERSISTENT_MQTT_KEY /
|
|
78
|
+
# LOGIN_INFO_PERSISTENT_MQTT_LOCK_KEY) against the shared frozenset, so a
|
|
79
|
+
# rename on one side without the other can't silently reopen this bug.
|
|
80
|
+
assert RUNTIME_ONLY_LOGIN_INFO_KEYS == {
|
|
81
|
+
LOGIN_INFO_PERSISTENT_MQTT_KEY, LOGIN_INFO_PERSISTENT_MQTT_LOCK_KEY,
|
|
82
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/base.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/g711.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/g722.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/h264.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/opus.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/vpx.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/contrib/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/contrib/media.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/exceptions.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/jitterbuffer.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/mediastreams.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcconfiguration.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcdatachannel.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcdtlstransport.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcicetransport.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcrtpparameters.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcrtpreceiver.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcrtpsender.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/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
|
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_deferred_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_device_login_guard.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_device_user_info_cache.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_dtls_not_ready_burst.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/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.0 → python_aidot_cameras-0.11.2}/tests/test_highport_nomination.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.0 → python_aidot_cameras-0.11.2}/tests/test_no_undefined_names.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_persistent_mqtt_robustness.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_post_merge_hardening.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/tests/test_sdes_echo_wait_timeout.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.0 → python_aidot_cameras-0.11.2}/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.0 → python_aidot_cameras-0.11.2}/tests/test_security_hardening.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
|