python-aidot-cameras 0.11.1__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.1/python_aidot_cameras.egg-info → python_aidot_cameras-0.11.2}/PKG-INFO +9 -1
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/README.md +8 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/__main__.py +14 -4
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/camera/client.py +13 -4
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/client.py +23 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/const.py +15 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/pyproject.toml +1 -1
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2/python_aidot_cameras.egg-info}/PKG-INFO +9 -1
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
- python_aidot_cameras-0.11.2/tests/test_serializable_login_info.py +82 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/LICENSE +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/__init__.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/aes_utils.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/camera/__init__.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/camera/constants.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/camera/controls.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/camera/models.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/camera/playback.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/camera/protocol.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/camera/sdes.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/camera/sdes_open.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/camera/tutk.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/credentials.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/device_client.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/discover.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/exceptions.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/g711.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/login_const.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/models/__init__.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/models/device_client_model.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/models/device_model.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/models/discover_model.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/py.typed +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/setup.cfg +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_wait_or_event.py +0 -0
{python_aidot_cameras-0.11.1/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.
|
|
@@ -3096,7 +3100,12 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3096
3100
|
li = self._user_info if isinstance(getattr(self, "_user_info", None), dict) else None
|
|
3097
3101
|
if li is None:
|
|
3098
3102
|
return None
|
|
3099
|
-
|
|
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)
|
|
3100
3109
|
if pm is not None:
|
|
3101
3110
|
return pm
|
|
3102
3111
|
# Serialize get-or-create. Without this, two concurrent first-callers
|
|
@@ -3105,9 +3114,9 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3105
3114
|
# clobbering the first and orphaning a connection on the single-client_id
|
|
3106
3115
|
# broker. dict.setdefault is atomic (no await between create and insert),
|
|
3107
3116
|
# so every caller for this account shares the one lock.
|
|
3108
|
-
lock = li.setdefault(
|
|
3117
|
+
lock = li.setdefault(LOGIN_INFO_PERSISTENT_MQTT_LOCK_KEY, asyncio.Lock())
|
|
3109
3118
|
async with lock:
|
|
3110
|
-
pm = li.get(
|
|
3119
|
+
pm = li.get(LOGIN_INFO_PERSISTENT_MQTT_KEY) # re-check under the lock
|
|
3111
3120
|
if pm is not None:
|
|
3112
3121
|
return pm
|
|
3113
3122
|
smarthome_auth = await self._async_get_smarthome_auth()
|
|
@@ -3119,7 +3128,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3119
3128
|
return None
|
|
3120
3129
|
from .protocol import _PersistentMqtt
|
|
3121
3130
|
pm = _PersistentMqtt(mqtt_url, mqtt_user, mqtt_pwd, client_id)
|
|
3122
|
-
li[
|
|
3131
|
+
li[LOGIN_INFO_PERSISTENT_MQTT_KEY] = pm
|
|
3123
3132
|
return pm
|
|
3124
3133
|
|
|
3125
3134
|
@staticmethod
|
|
@@ -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.1 → 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:
|
|
@@ -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.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/base.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/g711.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/g722.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/h264.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/opus.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/codecs/vpx.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/contrib/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → 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.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/exceptions.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/jitterbuffer.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/mediastreams.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcconfiguration.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcdatachannel.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcdtlstransport.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcicetransport.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcrtpparameters.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcrtpreceiver.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/aidot/_vendor/aiortc/rtcrtpsender.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.1 → 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.1 → 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.1 → python_aidot_cameras-0.11.2}/tests/test_deferred_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_device_login_guard.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_device_user_info_cache.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → 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.1 → 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.1 → 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.1 → python_aidot_cameras-0.11.2}/tests/test_no_undefined_names.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_offline_keepalive_pause.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.1 → python_aidot_cameras-0.11.2}/tests/test_persistent_mqtt_robustness.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.1 → 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.1 → python_aidot_cameras-0.11.2}/tests/test_sdes_echo_wait_timeout.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.1 → 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.1 → 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
|