python-aidot-cameras 0.11.3__tar.gz → 0.11.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_aidot_cameras-0.11.3/python_aidot_cameras.egg-info → python_aidot_cameras-0.11.5}/PKG-INFO +2 -2
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/camera/client.py +81 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/camera/protocol.py +6 -1
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/camera/webrtc_open.py +6 -25
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/pyproject.toml +2 -2
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5/python_aidot_cameras.egg-info}/PKG-INFO +2 -2
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/python_aidot_cameras.egg-info/requires.txt +1 -1
- python_aidot_cameras-0.11.5/tests/test_keepalive_renew.py +119 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/LICENSE +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/README.md +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/__init__.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/__main__.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/aes_utils.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/camera/__init__.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/camera/constants.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/camera/controls.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/camera/models.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/camera/playback.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/camera/sdes.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/camera/sdes_open.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/camera/tutk.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/client.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/const.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/credentials.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/device_client.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/discover.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/exceptions.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/g711.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/login_const.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/models/__init__.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/models/device_client_model.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/models/device_model.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/models/discover_model.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/py.typed +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/setup.cfg +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_wait_or_event.py +0 -0
{python_aidot_cameras-0.11.3/python_aidot_cameras.egg-info → python_aidot_cameras-0.11.5}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.5
|
|
4
4
|
Summary: Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)
|
|
5
5
|
Author-email: cbrightly <chris.brightly@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -12,7 +12,7 @@ Requires-Python: >=3.11
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: aiohttp>=3.9
|
|
15
|
-
Requires-Dist: paho-mqtt>=
|
|
15
|
+
Requires-Dist: paho-mqtt>=1.6.1
|
|
16
16
|
Requires-Dist: cryptography>=42.0
|
|
17
17
|
Requires-Dist: pycryptodome>=3.20
|
|
18
18
|
Requires-Dist: dacite>=1.8
|
|
@@ -565,6 +565,14 @@ def _get_stream_slots() -> "asyncio.Semaphore":
|
|
|
565
565
|
|
|
566
566
|
|
|
567
567
|
|
|
568
|
+
# Battery cameras re-sleep on their own low-power timer ~25 s after the last
|
|
569
|
+
# keep-alive, even mid-view. Renew setKeepAliveTime INSIDE that window so there
|
|
570
|
+
# is no sleep gap (the official app renews every 30 s, which leaves a ~5 s gap
|
|
571
|
+
# against the 25 s window). See CameraMixin._keepalive_renew_loop.
|
|
572
|
+
_KEEPALIVE_WINDOW_S = 25
|
|
573
|
+
_KEEPALIVE_RENEW_S = 20
|
|
574
|
+
|
|
575
|
+
|
|
568
576
|
class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
569
577
|
"""All camera/streaming methods, mixed into DeviceClient via inheritance."""
|
|
570
578
|
|
|
@@ -2169,6 +2177,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2169
2177
|
if self._stream_task is not None and not self._stream_task.done():
|
|
2170
2178
|
return
|
|
2171
2179
|
self._streaming_active = True
|
|
2180
|
+
self._start_keepalive_renew() # battery: renew keep-alive on this path too
|
|
2172
2181
|
self._stream_task = asyncio.ensure_future(self._streaming_loop())
|
|
2173
2182
|
|
|
2174
2183
|
async def async_stop_streaming(self) -> None:
|
|
@@ -2196,6 +2205,13 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2196
2205
|
await task
|
|
2197
2206
|
except (asyncio.CancelledError, Exception):
|
|
2198
2207
|
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), 'async_stop_streaming', exc_info=True)
|
|
2208
|
+
ka_task, self._keepalive_task = getattr(self, "_keepalive_task", None), None
|
|
2209
|
+
if ka_task is not None and not ka_task.done():
|
|
2210
|
+
ka_task.cancel()
|
|
2211
|
+
try:
|
|
2212
|
+
await ka_task
|
|
2213
|
+
except (asyncio.CancelledError, Exception):
|
|
2214
|
+
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), 'async_stop_streaming', exc_info=True)
|
|
2199
2215
|
# Reap a persistent-MQTT stream drain that no session stopped (e.g. an
|
|
2200
2216
|
# open cancelled mid-handshake) so its handler is removed from the shared
|
|
2201
2217
|
# connection and its blocked executor thread is released.
|
|
@@ -2307,6 +2323,65 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2307
2323
|
except asyncio.CancelledError:
|
|
2308
2324
|
return
|
|
2309
2325
|
|
|
2326
|
+
async def _async_set_keep_alive(self, keep_alive_s: int = _KEEPALIVE_WINDOW_S) -> None:
|
|
2327
|
+
"""POST setKeepAliveTime so a battery camera stays awake for keep_alive_s
|
|
2328
|
+
seconds. Best-effort (errors swallowed). Wire format matches the app
|
|
2329
|
+
(n.java: keepAliveTime=25)."""
|
|
2330
|
+
try:
|
|
2331
|
+
import aiohttp as _aiohttp
|
|
2332
|
+
async with _aiohttp.ClientSession() as _s:
|
|
2333
|
+
async with _s.post(
|
|
2334
|
+
f"{self._aidot_v32_base}/devices/{self.device_id}/setKeepAliveTime",
|
|
2335
|
+
json={"keepAliveTime": keep_alive_s},
|
|
2336
|
+
headers=self._aidot_headers(),
|
|
2337
|
+
timeout=_aiohttp.ClientTimeout(total=8),
|
|
2338
|
+
) as _r:
|
|
2339
|
+
_LOGGER.debug(
|
|
2340
|
+
"setKeepAliveTime HTTP %d for %s", _r.status, self.device_id
|
|
2341
|
+
)
|
|
2342
|
+
except Exception as _exc:
|
|
2343
|
+
_LOGGER.debug(
|
|
2344
|
+
"setKeepAliveTime HTTP failed for %s: %s", self.device_id, _exc
|
|
2345
|
+
)
|
|
2346
|
+
|
|
2347
|
+
def _start_keepalive_renew(self) -> None:
|
|
2348
|
+
"""Start the battery keep-alive renew loop - battery-only and
|
|
2349
|
+
single-instance: cancel any prior loop first so a re-view within the
|
|
2350
|
+
renew sleep window can't orphan one (an orphan keeps POSTing
|
|
2351
|
+
setKeepAliveTime until _streaming_active next clears). No-op for mains
|
|
2352
|
+
cameras, which never sleep."""
|
|
2353
|
+
if not self.is_battery_camera:
|
|
2354
|
+
return
|
|
2355
|
+
prev = getattr(self, "_keepalive_task", None)
|
|
2356
|
+
if prev is not None and not prev.done():
|
|
2357
|
+
prev.cancel()
|
|
2358
|
+
self._keepalive_task = asyncio.ensure_future(self._keepalive_renew_loop())
|
|
2359
|
+
|
|
2360
|
+
def _cancel_keepalive_renew(self) -> None:
|
|
2361
|
+
"""Cancel the battery keep-alive renew loop if running (fire-and-forget)."""
|
|
2362
|
+
task, self._keepalive_task = getattr(self, "_keepalive_task", None), None
|
|
2363
|
+
if task is not None and not task.done():
|
|
2364
|
+
task.cancel()
|
|
2365
|
+
|
|
2366
|
+
async def _keepalive_renew_loop(self) -> None:
|
|
2367
|
+
"""Renew setKeepAliveTime while a BATTERY camera is streaming (app parity).
|
|
2368
|
+
|
|
2369
|
+
The camera's own low-power timer returns it to sleep ~25 s after the last
|
|
2370
|
+
keep-alive even mid-view, so a one-shot keep-alive at open lets the stream
|
|
2371
|
+
drop partway through. The official app renews it throughout a live view;
|
|
2372
|
+
we renew every _KEEPALIVE_RENEW_S seconds (inside the 25 s window, so no
|
|
2373
|
+
sleep gap). Mains cameras never sleep, so this is battery-only."""
|
|
2374
|
+
if not self.is_battery_camera:
|
|
2375
|
+
return
|
|
2376
|
+
try:
|
|
2377
|
+
while self._streaming_active:
|
|
2378
|
+
await asyncio.sleep(_KEEPALIVE_RENEW_S)
|
|
2379
|
+
if not self._streaming_active:
|
|
2380
|
+
break
|
|
2381
|
+
await self._async_set_keep_alive()
|
|
2382
|
+
except asyncio.CancelledError:
|
|
2383
|
+
pass
|
|
2384
|
+
|
|
2310
2385
|
async def start_keepalive(
|
|
2311
2386
|
self,
|
|
2312
2387
|
rtsp_push_url: Optional[str] = None,
|
|
@@ -2381,6 +2456,10 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2381
2456
|
self._keepalive_rtsp_url = rtsp_push_url
|
|
2382
2457
|
self._go2rtc_url = go2rtc_url
|
|
2383
2458
|
self._streaming_active = True
|
|
2459
|
+
# App parity: keep a battery camera awake for the whole view (mains cams
|
|
2460
|
+
# never sleep, so this is a no-op there). Single-instance; cancelled in
|
|
2461
|
+
# stop / idle-release.
|
|
2462
|
+
self._start_keepalive_renew()
|
|
2384
2463
|
if self.is_sdes_camera:
|
|
2385
2464
|
self._stream_task = asyncio.ensure_future(self._sdes_keepalive_loop())
|
|
2386
2465
|
elif rtsp_push_url and rtsp_push_url.startswith("http"):
|
|
@@ -2703,6 +2782,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2703
2782
|
except Exception:
|
|
2704
2783
|
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_sdes_keepalive_loop', exc_info=True)
|
|
2705
2784
|
self._streaming_active = False
|
|
2785
|
+
self._cancel_keepalive_renew()
|
|
2706
2786
|
self._keepalive_rtsp_url = None
|
|
2707
2787
|
self._serve_ready.clear()
|
|
2708
2788
|
_LOGGER.debug(
|
|
@@ -3490,6 +3570,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3490
3570
|
# No viewer - go dormant; camera.stream_source() restarts us when
|
|
3491
3571
|
# someone opens the live view again.
|
|
3492
3572
|
self._streaming_active = False
|
|
3573
|
+
self._cancel_keepalive_renew()
|
|
3493
3574
|
self._keepalive_rtsp_url = None
|
|
3494
3575
|
self._serve_ready.clear()
|
|
3495
3576
|
_LOGGER.debug(
|
|
@@ -1219,7 +1219,12 @@ def _mqtt_session_sync(
|
|
|
1219
1219
|
else str(msg.payload))
|
|
1220
1220
|
msg_q.put((msg.topic, payload))
|
|
1221
1221
|
|
|
1222
|
-
def _on_disconnect(c, ud,
|
|
1222
|
+
def _on_disconnect(c, ud, *args):
|
|
1223
|
+
# paho >=2 calls (disconnect_flags, reason_code, properties); paho <2
|
|
1224
|
+
# calls (rc,). Pull the reason code from whichever slot it lands in so
|
|
1225
|
+
# the log is truthful on both (a fixed 5-arg signature put paho 1.x's rc
|
|
1226
|
+
# in disconnect_flags and logged reason_code as None).
|
|
1227
|
+
reason_code = args[1] if len(args) >= 2 else (args[0] if args else None)
|
|
1223
1228
|
# If _on_connect was never fired (WebSocket upgrade failed, auth refused
|
|
1224
1229
|
# at TCP level, etc.) signal conn_ev now so the caller doesn't time out.
|
|
1225
1230
|
if not conn_ev.is_set():
|
|
@@ -1000,32 +1000,13 @@ class _WebRTCOpenMixin:
|
|
|
1000
1000
|
"lowPowerActiveState HTTP failed for %s: %s", device_id, _we
|
|
1001
1001
|
)
|
|
1002
1002
|
|
|
1003
|
-
async def _http_keepalive() -> None:
|
|
1004
|
-
# setKeepAliveTime keeps the camera in active state for 25 s
|
|
1005
|
-
# after wake (n.java:224 - keepAliveTime=25, not 20).
|
|
1006
|
-
# Without this call the camera's built-in timer may return it
|
|
1007
|
-
# to sleep before SCTP + LIVING completes (~10-15 s).
|
|
1008
|
-
try:
|
|
1009
|
-
import aiohttp as _aiohttp_k
|
|
1010
|
-
async with _aiohttp_k.ClientSession() as _ks:
|
|
1011
|
-
async with _ks.post(
|
|
1012
|
-
f"{self._aidot_v32_base}/devices/{device_id}"
|
|
1013
|
-
"/setKeepAliveTime",
|
|
1014
|
-
json={"keepAliveTime": 25},
|
|
1015
|
-
headers=self._aidot_headers(),
|
|
1016
|
-
timeout=_aiohttp_k.ClientTimeout(total=8),
|
|
1017
|
-
) as _kr:
|
|
1018
|
-
_LOGGER.debug(
|
|
1019
|
-
"setKeepAliveTime HTTP %d for %s",
|
|
1020
|
-
_kr.status, device_id,
|
|
1021
|
-
)
|
|
1022
|
-
except Exception as _ke:
|
|
1023
|
-
_LOGGER.debug(
|
|
1024
|
-
"setKeepAliveTime HTTP failed for %s: %s", device_id, _ke
|
|
1025
|
-
)
|
|
1026
|
-
|
|
1027
1003
|
_spawn_bg(_http_wake())
|
|
1028
|
-
|
|
1004
|
+
# setKeepAliveTime keeps the camera active for ~25 s after wake so the
|
|
1005
|
+
# SCTP + LIVING handshake can complete. For BATTERY cameras a renewal
|
|
1006
|
+
# loop (CameraMixin._keepalive_renew_loop, started by start_keepalive)
|
|
1007
|
+
# then re-issues it every 20 s for the whole view - app parity so the
|
|
1008
|
+
# camera doesn't return to sleep mid-stream.
|
|
1009
|
+
_spawn_bg(self._async_set_keep_alive())
|
|
1029
1010
|
|
|
1030
1011
|
outgoing_q.put_nowait(
|
|
1031
1012
|
(f"iot/v1/s/{user_id}/IPC/getIceConfigReq", _ice_req_payload)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-aidot-cameras"
|
|
7
|
-
version = "0.11.
|
|
7
|
+
version = "0.11.5"
|
|
8
8
|
description = "Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -22,7 +22,7 @@ classifiers = [
|
|
|
22
22
|
# after re-validating against it.
|
|
23
23
|
dependencies = [
|
|
24
24
|
"aiohttp>=3.9",
|
|
25
|
-
"paho-mqtt>=
|
|
25
|
+
"paho-mqtt>=1.6.1",
|
|
26
26
|
"cryptography>=42.0",
|
|
27
27
|
"pycryptodome>=3.20",
|
|
28
28
|
"dacite>=1.8",
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5/python_aidot_cameras.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.5
|
|
4
4
|
Summary: Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)
|
|
5
5
|
Author-email: cbrightly <chris.brightly@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -12,7 +12,7 @@ Requires-Python: >=3.11
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: aiohttp>=3.9
|
|
15
|
-
Requires-Dist: paho-mqtt>=
|
|
15
|
+
Requires-Dist: paho-mqtt>=1.6.1
|
|
16
16
|
Requires-Dist: cryptography>=42.0
|
|
17
17
|
Requires-Dist: pycryptodome>=3.20
|
|
18
18
|
Requires-Dist: dacite>=1.8
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Battery-camera setKeepAliveTime renewal during streaming (app parity).
|
|
2
|
+
|
|
3
|
+
The official app re-issues setKeepAliveTime throughout a live view so a battery
|
|
4
|
+
camera's low-power timer doesn't return it to sleep mid-stream. We renew inside
|
|
5
|
+
the 25 s window; mains cameras never sleep and are skipped.
|
|
6
|
+
"""
|
|
7
|
+
import asyncio
|
|
8
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
9
|
+
|
|
10
|
+
from aidot.camera.client import CameraMixin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _obj(battery=True, streaming=True):
|
|
14
|
+
o = MagicMock()
|
|
15
|
+
o.is_battery_camera = battery
|
|
16
|
+
o._streaming_active = streaming
|
|
17
|
+
o.device_id = "cam1"
|
|
18
|
+
o._async_set_keep_alive = AsyncMock()
|
|
19
|
+
return o
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_renew_loop_skips_mains_cameras():
|
|
23
|
+
o = _obj(battery=False)
|
|
24
|
+
asyncio.run(CameraMixin._keepalive_renew_loop(o))
|
|
25
|
+
o._async_set_keep_alive.assert_not_awaited()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_renew_loop_renews_battery_until_stream_stops():
|
|
29
|
+
o = _obj(battery=True)
|
|
30
|
+
n = {"i": 0}
|
|
31
|
+
|
|
32
|
+
async def fake_sleep(_s):
|
|
33
|
+
n["i"] += 1
|
|
34
|
+
if n["i"] >= 3:
|
|
35
|
+
o._streaming_active = False # stop after the 3rd sleep
|
|
36
|
+
|
|
37
|
+
with patch("aidot.camera.client.asyncio.sleep", fake_sleep):
|
|
38
|
+
asyncio.run(CameraMixin._keepalive_renew_loop(o))
|
|
39
|
+
# sleep(renew), sleep(renew), sleep(stop -> break): 2 renewals
|
|
40
|
+
assert o._async_set_keep_alive.await_count == 2
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_renew_loop_swallows_cancel():
|
|
44
|
+
o = _obj(battery=True)
|
|
45
|
+
|
|
46
|
+
async def _run():
|
|
47
|
+
t = asyncio.ensure_future(CameraMixin._keepalive_renew_loop(o))
|
|
48
|
+
await asyncio.sleep(0) # let it reach the first (real) sleep
|
|
49
|
+
t.cancel() # cancel interrupts the sleep immediately
|
|
50
|
+
await t # CancelledError is swallowed inside the loop
|
|
51
|
+
|
|
52
|
+
asyncio.run(_run())
|
|
53
|
+
o._async_set_keep_alive.assert_not_awaited()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_start_renew_is_noop_for_mains_cameras():
|
|
57
|
+
# Mains cameras never sleep - no throwaway task should be scheduled.
|
|
58
|
+
o = _obj(battery=False)
|
|
59
|
+
with patch("aidot.camera.client.asyncio.ensure_future") as ef:
|
|
60
|
+
CameraMixin._start_keepalive_renew(o)
|
|
61
|
+
ef.assert_not_called()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_start_renew_cancels_a_still_running_prior_loop():
|
|
65
|
+
# Re-view within the renew sleep window must not orphan the prior loop.
|
|
66
|
+
o = _obj(battery=True)
|
|
67
|
+
prior = MagicMock()
|
|
68
|
+
prior.done.return_value = False
|
|
69
|
+
o._keepalive_task = prior
|
|
70
|
+
with patch("aidot.camera.client.asyncio.ensure_future", return_value="new") as ef:
|
|
71
|
+
CameraMixin._start_keepalive_renew(o)
|
|
72
|
+
prior.cancel.assert_called_once() # old loop stopped
|
|
73
|
+
ef.assert_called_once() # exactly one new loop
|
|
74
|
+
assert o._keepalive_task == "new"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_cancel_renew_stops_the_loop_and_clears_the_ref():
|
|
78
|
+
o = _obj(battery=True)
|
|
79
|
+
task = MagicMock()
|
|
80
|
+
task.done.return_value = False
|
|
81
|
+
o._keepalive_task = task
|
|
82
|
+
CameraMixin._cancel_keepalive_renew(o)
|
|
83
|
+
task.cancel.assert_called_once()
|
|
84
|
+
assert o._keepalive_task is None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_set_keep_alive_posts_expected_request():
|
|
88
|
+
o = MagicMock()
|
|
89
|
+
o.device_id = "cam1"
|
|
90
|
+
o._aidot_v32_base = "https://api.example/v3.2"
|
|
91
|
+
o._aidot_headers = MagicMock(return_value={"Authorization": "x"})
|
|
92
|
+
cap = {}
|
|
93
|
+
|
|
94
|
+
class _Resp:
|
|
95
|
+
status = 200
|
|
96
|
+
|
|
97
|
+
async def __aenter__(self):
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
async def __aexit__(self, *a):
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
class _Sess:
|
|
104
|
+
async def __aenter__(self):
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
async def __aexit__(self, *a):
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
def post(self, url, json=None, headers=None, timeout=None):
|
|
111
|
+
cap.update(url=url, json=json, headers=headers)
|
|
112
|
+
return _Resp()
|
|
113
|
+
|
|
114
|
+
with patch("aiohttp.ClientSession", lambda: _Sess()):
|
|
115
|
+
asyncio.run(CameraMixin._async_set_keep_alive(o))
|
|
116
|
+
|
|
117
|
+
assert cap["url"] == "https://api.example/v3.2/devices/cam1/setKeepAliveTime"
|
|
118
|
+
assert cap["json"] == {"keepAliveTime": 25}
|
|
119
|
+
assert cap["headers"] == {"Authorization": "x"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/base.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/g711.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/g722.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/h264.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/opus.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/codecs/vpx.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/contrib/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/contrib/media.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/exceptions.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/jitterbuffer.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/mediastreams.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcconfiguration.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcdatachannel.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcdtlstransport.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcicetransport.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcrtpparameters.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcrtpreceiver.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcrtpsender.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/_vendor/aiortc/rtcsctptransport.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/aidot/models/device_client_model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_deferred_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_device_login_guard.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_device_user_info_cache.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_dtls_not_ready_burst.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_dtls_skip_signaling_wait.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_highport_nomination.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_no_undefined_names.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_offline_keepalive_pause.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_persistent_mqtt_robustness.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_post_merge_hardening.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_sdes_echo_wait_timeout.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_sdes_fast_liveplay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_security_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_serializable_login_info.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.3 → python_aidot_cameras-0.11.5}/tests/test_token_file_robustness.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|