python-aidot-cameras 0.12.0__tar.gz → 0.12.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.12.0/python_aidot_cameras.egg-info → python_aidot_cameras-0.12.2}/PKG-INFO +1 -1
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/client.py +32 -3
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/protocol.py +30 -1
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/client.py +19 -4
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/pyproject.toml +1 -1
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
- python_aidot_cameras-0.12.2/tests/test_mqtt_credential_self_heal.py +52 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/LICENSE +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/README.md +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/sdes_open.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/setup.cfg +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_upstream_compat.py +0 -0
- {python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_wait_or_event.py +0 -0
{python_aidot_cameras-0.12.0/python_aidot_cameras.egg-info → python_aidot_cameras-0.12.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.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
|
|
@@ -2645,9 +2645,15 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2645
2645
|
|
|
2646
2646
|
async def _deregister_go2rtc(self) -> None:
|
|
2647
2647
|
"""Remove this camera's stream from go2rtc (best-effort)."""
|
|
2648
|
-
base
|
|
2648
|
+
base = self._go2rtc_url
|
|
2649
2649
|
self._go2rtc_pull_url = None
|
|
2650
|
-
|
|
2650
|
+
# Deliberately NOT gated on `pull`: go2rtc answers the register call with
|
|
2651
|
+
# 400 when it cannot immediately validate the source (a camera that has
|
|
2652
|
+
# not started producing yet), yet it still keeps the stream registered.
|
|
2653
|
+
# ensure_stream reports that as failure, so pull stays None - and gating
|
|
2654
|
+
# on it here leaked one dead stream per attempt, which then sat in go2rtc
|
|
2655
|
+
# with a producer nothing was feeding.
|
|
2656
|
+
if not base:
|
|
2651
2657
|
return
|
|
2652
2658
|
import aiohttp
|
|
2653
2659
|
from .go2rtc import Go2rtcClient
|
|
@@ -3396,7 +3402,30 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3396
3402
|
if not mqtt_url:
|
|
3397
3403
|
return None
|
|
3398
3404
|
from .protocol import _PersistentMqtt
|
|
3399
|
-
|
|
3405
|
+
|
|
3406
|
+
def _invalidate_mqtt_credentials(rc) -> None:
|
|
3407
|
+
"""Drop the cached MQTT password so the next use re-fetches it.
|
|
3408
|
+
|
|
3409
|
+
The broker rotates this password on every account login and
|
|
3410
|
+
allows one connection at a time, so any other login (the phone
|
|
3411
|
+
app, a second Home Assistant, a script) invalidates ours. The
|
|
3412
|
+
cached copy lives in login_info, where it short-circuits the
|
|
3413
|
+
credential fetch, so it has to be cleared or every retry reuses
|
|
3414
|
+
the dead password. The persistent client itself is dropped too,
|
|
3415
|
+
so the next request rebuilds it with fresh credentials.
|
|
3416
|
+
"""
|
|
3417
|
+
for key in ("mqttPassword", "mqttPwd"):
|
|
3418
|
+
if li.pop(key, None) is not None:
|
|
3419
|
+
_LOGGER.warning(
|
|
3420
|
+
"cleared the cached MQTT password after the broker "
|
|
3421
|
+
"rejected it (rc=%s); it will be re-fetched", rc,
|
|
3422
|
+
)
|
|
3423
|
+
self._smarthome_auth = None
|
|
3424
|
+
if li.get(LOGIN_INFO_PERSISTENT_MQTT_KEY) is pm:
|
|
3425
|
+
li.pop(LOGIN_INFO_PERSISTENT_MQTT_KEY, None)
|
|
3426
|
+
|
|
3427
|
+
pm = _PersistentMqtt(mqtt_url, mqtt_user, mqtt_pwd, client_id,
|
|
3428
|
+
on_auth_failure=_invalidate_mqtt_credentials)
|
|
3400
3429
|
li[LOGIN_INFO_PERSISTENT_MQTT_KEY] = pm
|
|
3401
3430
|
return pm
|
|
3402
3431
|
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/protocol.py
RENAMED
|
@@ -1414,12 +1414,26 @@ class _PersistentMqtt:
|
|
|
1414
1414
|
N operations cost ONE connect instead of N. The client_id is account-level,
|
|
1415
1415
|
so exactly one of these should exist per account (per AidotClient)."""
|
|
1416
1416
|
|
|
1417
|
-
|
|
1417
|
+
# Connect return codes meaning "these credentials are not acceptable"
|
|
1418
|
+
# rather than "try again later". 4/5 are MQTT 3.1.1 (bad user/pass, not
|
|
1419
|
+
# authorized); 134/135 are the MQTT 5 equivalents (0x86/0x87). The AiDot
|
|
1420
|
+
# broker rotates the MQTT password on every account login and allows one
|
|
1421
|
+
# connection at a time, so a cached password can go stale at any moment -
|
|
1422
|
+
# a login from the phone app is enough. Retrying it forever is useless;
|
|
1423
|
+
# the owner has to fetch a fresh credential.
|
|
1424
|
+
_AUTH_REFUSAL_RCS = frozenset({4, 5, 134, 135})
|
|
1425
|
+
|
|
1426
|
+
def __init__(self, mqtt_url, mqtt_user, mqtt_pwd, client_id, ws_path="/mqtt",
|
|
1427
|
+
on_auth_failure=None):
|
|
1418
1428
|
self._url = mqtt_url
|
|
1419
1429
|
self._user = mqtt_user
|
|
1420
1430
|
self._pwd = mqtt_pwd
|
|
1421
1431
|
self._cid = client_id
|
|
1422
1432
|
self._ws_path = ws_path
|
|
1433
|
+
# Invoked (once per instance) when the broker rejects our credentials so
|
|
1434
|
+
# the owner can invalidate its cache and re-authenticate.
|
|
1435
|
+
self._on_auth_failure = on_auth_failure
|
|
1436
|
+
self._auth_failed_reported = False
|
|
1423
1437
|
self._client = None
|
|
1424
1438
|
self._host = None
|
|
1425
1439
|
self._port = None
|
|
@@ -1476,6 +1490,21 @@ class _PersistentMqtt:
|
|
|
1476
1490
|
except Exception:
|
|
1477
1491
|
_LOGGER.debug("persistent mqtt: resubscribe %s failed", t)
|
|
1478
1492
|
self._connected.set()
|
|
1493
|
+
elif rc in self._AUTH_REFUSAL_RCS:
|
|
1494
|
+
# Stale credential: paho would retry this password forever. Tell the
|
|
1495
|
+
# owner once so it can invalidate its cache and re-authenticate.
|
|
1496
|
+
_LOGGER.warning(
|
|
1497
|
+
"persistent mqtt: broker rejected our credentials (rc=%s) - the "
|
|
1498
|
+
"MQTT password has almost certainly been rotated by another "
|
|
1499
|
+
"login; requesting a fresh one", rc,
|
|
1500
|
+
)
|
|
1501
|
+
if self._on_auth_failure and not self._auth_failed_reported:
|
|
1502
|
+
self._auth_failed_reported = True
|
|
1503
|
+
try:
|
|
1504
|
+
self._on_auth_failure(rc)
|
|
1505
|
+
except Exception:
|
|
1506
|
+
_LOGGER.debug("persistent mqtt: on_auth_failure raised",
|
|
1507
|
+
exc_info=True)
|
|
1479
1508
|
else:
|
|
1480
1509
|
_LOGGER.warning("persistent mqtt: broker refused rc=%s", rc)
|
|
1481
1510
|
|
|
@@ -464,6 +464,13 @@ class CameraClient(_UpstreamAidotClient):
|
|
|
464
464
|
The HTTP calls themselves are still upstream's ``CloudApi``.
|
|
465
465
|
"""
|
|
466
466
|
final_device_list: list[dict[str, Any]] = []
|
|
467
|
+
# Devices upstream's client cannot construct (no usable aesKey): Zigbee
|
|
468
|
+
# sub-devices, remotes, and other accessories that were never supported
|
|
469
|
+
# here. They are counted and reported once, not warned about
|
|
470
|
+
# individually - an account with a dozen Zigbee sensors would otherwise
|
|
471
|
+
# log a dozen WARNINGs on every refresh for devices nothing was ever
|
|
472
|
+
# going to use.
|
|
473
|
+
unbuildable: dict[str, int] = {}
|
|
467
474
|
houses = await self._cloud_api.get_houses() or []
|
|
468
475
|
for house in houses:
|
|
469
476
|
if house.get(CONF_IS_OWNER) is False:
|
|
@@ -473,11 +480,19 @@ class CameraClient(_UpstreamAidotClient):
|
|
|
473
480
|
if _is_camera_device(device) or _upstream_can_build(device):
|
|
474
481
|
final_device_list.append(device)
|
|
475
482
|
else:
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
device
|
|
483
|
+
model = device.get(CONF_MODEL_ID) or "unknown"
|
|
484
|
+
unbuildable[model] = unbuildable.get(model, 0) + 1
|
|
485
|
+
_LOGGER.debug(
|
|
486
|
+
"skipping device %s (%s): no usable aesKey, upstream's "
|
|
487
|
+
"device client cannot be built for it",
|
|
488
|
+
device.get(CONF_ID), model,
|
|
480
489
|
)
|
|
490
|
+
if unbuildable:
|
|
491
|
+
_LOGGER.info(
|
|
492
|
+
"skipped %d device(s) with no usable aesKey (not supported here): %s",
|
|
493
|
+
sum(unbuildable.values()),
|
|
494
|
+
", ".join(f"{m} x{n}" for m, n in sorted(unbuildable.items())),
|
|
495
|
+
)
|
|
481
496
|
|
|
482
497
|
product_ids = ",".join(
|
|
483
498
|
sorted(
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-aidot-cameras"
|
|
7
|
-
version = "0.12.
|
|
7
|
+
version = "0.12.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.12.0 → python_aidot_cameras-0.12.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.12.
|
|
3
|
+
Version: 0.12.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
|
|
@@ -91,6 +91,7 @@ tests/test_light_active_color_mode.py
|
|
|
91
91
|
tests/test_live_stream_param.py
|
|
92
92
|
tests/test_logging_caps.py
|
|
93
93
|
tests/test_motion_poll.py
|
|
94
|
+
tests/test_mqtt_credential_self_heal.py
|
|
94
95
|
tests/test_mqtt_session_reconnect.py
|
|
95
96
|
tests/test_narrow_pc_ice.py
|
|
96
97
|
tests/test_no_undefined_names.py
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""The broker rotates the MQTT password on every account login and allows one
|
|
2
|
+
connection at a time, so a cached password goes stale whenever anything else
|
|
3
|
+
logs in (the phone app is enough). paho would retry the dead password forever,
|
|
4
|
+
so a credential refusal has to invalidate the cache instead.
|
|
5
|
+
"""
|
|
6
|
+
import aidot_cameras.camera.protocol as proto
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _pm(on_auth_failure=None):
|
|
10
|
+
return proto._PersistentMqtt(
|
|
11
|
+
"wss://broker.example:8443/mqtt", "user", "stale-pwd", "app-user",
|
|
12
|
+
on_auth_failure=on_auth_failure,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_auth_refusal_codes_cover_mqtt3_and_mqtt5():
|
|
17
|
+
# 4/5 are MQTT 3.1.1 bad-user-pass / not-authorized; 134/135 are the MQTT 5
|
|
18
|
+
# equivalents (0x86/0x87) - the AiDot broker answers with 134.
|
|
19
|
+
for rc in (4, 5, 134, 135):
|
|
20
|
+
assert rc in proto._PersistentMqtt._AUTH_REFUSAL_RCS, rc
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_credential_refusal_invokes_the_callback_once():
|
|
24
|
+
seen = []
|
|
25
|
+
pm = _pm(on_auth_failure=seen.append)
|
|
26
|
+
pm._on_connect(None, None, {}, 134)
|
|
27
|
+
pm._on_connect(None, None, {}, 134) # repeated refusal must not re-notify
|
|
28
|
+
assert seen == [134]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_transient_refusal_does_not_invalidate_credentials():
|
|
32
|
+
# rc=3 is "server unavailable" - retrying the same password is correct.
|
|
33
|
+
seen = []
|
|
34
|
+
pm = _pm(on_auth_failure=seen.append)
|
|
35
|
+
pm._on_connect(None, None, {}, 3)
|
|
36
|
+
assert seen == []
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_successful_connect_does_not_invalidate_credentials():
|
|
40
|
+
seen = []
|
|
41
|
+
pm = _pm(on_auth_failure=seen.append)
|
|
42
|
+
pm._on_connect(None, None, {}, 0)
|
|
43
|
+
assert seen == []
|
|
44
|
+
assert pm._connected.is_set()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_callback_exception_is_contained():
|
|
48
|
+
def boom(rc):
|
|
49
|
+
raise RuntimeError("callback exploded")
|
|
50
|
+
|
|
51
|
+
pm = _pm(on_auth_failure=boom)
|
|
52
|
+
pm._on_connect(None, None, {}, 134) # must not propagate into paho's loop
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/clock.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.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/events.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/rate.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.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/rtp.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/sdp.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/stats.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/_vendor/aiortc/utils.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/constants.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/controls.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/lan_control.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/playback.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/sdes_open.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/aidot_cameras/camera/webrtc_open.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
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_deferred_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_device_login_guard.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_device_user_info_cache.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_dtls_not_ready_burst.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_dtls_serve_decode_skip.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_dtls_serve_open_timeout.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_dtls_skip_signaling_wait.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_h264_decode_rate_limit.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/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.12.0 → python_aidot_cameras-0.12.2}/tests/test_light_active_color_mode.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_mqtt_session_reconnect.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_no_undefined_names.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_offline_keepalive_pause.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_persistent_mqtt_robustness.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_post_merge_hardening.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_sdes_bridge_break_gate.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_sdes_echo_wait_timeout.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_sdes_fast_liveplay.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_sdes_ffmpeg_stderr_log.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.12.0 → python_aidot_cameras-0.12.2}/tests/test_sdes_teardown_exit_log.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_security_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_serializable_login_info.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_stream_drain_ownership.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.0 → python_aidot_cameras-0.12.2}/tests/test_token_file_robustness.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|