python-aidot-cameras 0.7.32__tar.gz → 0.7.33__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.7.32/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.7.33}/PKG-INFO +1 -1
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/pyproject.toml +1 -1
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/camera/sdes.py +11 -3
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/device_client.py +23 -2
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33/src/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
- python_aidot_cameras-0.7.33/tests/test_device_login_guard.py +95 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/LICENSE +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/README.md +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/setup.cfg +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/__init__.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/aes_utils.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/camera/__init__.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/camera/client.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/camera/constants.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/camera/controls.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/camera/models.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/camera/playback.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/camera/protocol.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/camera/tutk.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/client.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/const.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/credentials.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/discover.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/exceptions.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/g711.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/login_const.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/models/__init__.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/models/device_client_model.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/models/device_model.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/models/discover_model.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/py.typed +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_token_refresh.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.33
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-aidot-cameras"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.33"
|
|
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"
|
|
@@ -181,15 +181,23 @@ class SdesSession:
|
|
|
181
181
|
self._talk_state["stop"] = True
|
|
182
182
|
self._talk_state["provider"] = None
|
|
183
183
|
self._proc.terminate()
|
|
184
|
+
_stop_loop = asyncio.get_running_loop()
|
|
184
185
|
try:
|
|
185
|
-
_stop_loop = asyncio.get_running_loop()
|
|
186
186
|
await _stop_loop.run_in_executor(None, lambda: self._proc.wait(5))
|
|
187
187
|
except Exception:
|
|
188
188
|
self._proc.kill()
|
|
189
|
+
# Read drained stderr in the executor with a hard timeout: proc.stderr.read()
|
|
190
|
+
# blocks until EOF, which never arrives if the killed process is still a
|
|
191
|
+
# zombie / stuck in uninterruptible I/O - doing it inline would hang the
|
|
192
|
+
# whole event loop (and thus all of teardown). Bound it so a wedged ffmpeg
|
|
193
|
+
# can't stall the close; we lose only the diagnostic stderr in that case.
|
|
189
194
|
stderr_bytes = b""
|
|
190
195
|
try:
|
|
191
|
-
stderr_bytes =
|
|
192
|
-
|
|
196
|
+
stderr_bytes = await asyncio.wait_for(
|
|
197
|
+
_stop_loop.run_in_executor(None, self._proc.stderr.read),
|
|
198
|
+
timeout=2.0,
|
|
199
|
+
)
|
|
200
|
+
except Exception: # incl. asyncio.TimeoutError - never let teardown hang here
|
|
193
201
|
_LOGGER.debug("camera %s: swallowed exception", 'stop', exc_info=True)
|
|
194
202
|
if stderr_bytes:
|
|
195
203
|
_LOGGER.warning("ffmpeg SDES stderr:\n%s", stderr_bytes.decode(errors="replace"))
|
|
@@ -229,14 +229,35 @@ class DeviceClient(CameraMixin):
|
|
|
229
229
|
if ip is None:
|
|
230
230
|
return
|
|
231
231
|
self._ip_address = ip
|
|
232
|
-
if self._connecting is
|
|
233
|
-
|
|
232
|
+
if self._connecting is True or self._connect_and_login is True:
|
|
233
|
+
return
|
|
234
|
+
# Throttle (shared 30s window with the reconnect chain): a repeated
|
|
235
|
+
# discovery - or a camera that slipped the _is_camera gate because its
|
|
236
|
+
# model was momentarily unknown - must not spawn a fresh TCP:10000 login
|
|
237
|
+
# every broadcast tick. That hammers a device whose control port refuses
|
|
238
|
+
# (e.g. a battery camera) and spams "login read status error". A device
|
|
239
|
+
# that genuinely needs to connect still does so within 30s.
|
|
240
|
+
now = time.monotonic()
|
|
241
|
+
if now - self._last_login_attempt < 30:
|
|
242
|
+
return
|
|
243
|
+
self._last_login_attempt = now
|
|
244
|
+
self._login_task = asyncio.create_task(self.async_login())
|
|
234
245
|
|
|
235
246
|
|
|
236
247
|
async def async_login(self) -> None:
|
|
237
248
|
"""Connect and log in using the last-known IP if not already connected."""
|
|
238
249
|
if self._ip_address is None:
|
|
239
250
|
return
|
|
251
|
+
# The base TCP:10000 control channel is the LIGHT protocol. Cameras never
|
|
252
|
+
# use it - their local control is the separate CameraLanClient
|
|
253
|
+
# (camera/lan_control.py) and their LAN IP comes from WebRTC signaling. A
|
|
254
|
+
# camera reaching here means a discovered IP slipped the _is_camera gate;
|
|
255
|
+
# logging in would hammer a port the camera doesn't serve and spam
|
|
256
|
+
# "login read status error" (and never connect). This is the single
|
|
257
|
+
# chokepoint for both the discovery and reconnect-chain login paths.
|
|
258
|
+
model = getattr(getattr(self, "info", None), "model_id", "") or ""
|
|
259
|
+
if "IPC" in model:
|
|
260
|
+
return
|
|
240
261
|
if self._connecting is not True and self._connect_and_login is not True:
|
|
241
262
|
await self.connect(self._ip_address)
|
|
242
263
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.33
|
|
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
|
|
@@ -36,6 +36,7 @@ src/python_aidot_cameras.egg-info/requires.txt
|
|
|
36
36
|
src/python_aidot_cameras.egg-info/top_level.txt
|
|
37
37
|
tests/test_alarm_event.py
|
|
38
38
|
tests/test_backoff.py
|
|
39
|
+
tests/test_device_login_guard.py
|
|
39
40
|
tests/test_go2rtc.py
|
|
40
41
|
tests/test_highport_nomination.py
|
|
41
42
|
tests/test_lan_control.py
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Unit tests for the base-DeviceClient local-login guard + throttle.
|
|
2
|
+
|
|
3
|
+
The TCP:10000 control channel is the LIGHT protocol; cameras must never use it
|
|
4
|
+
(they use the separate CameraLanClient + WebRTC signaling for their LAN IP). A
|
|
5
|
+
camera reaching async_login would hammer a refusing port and spam
|
|
6
|
+
"login read status error". These tests lock the camera-exclusion and the
|
|
7
|
+
re-login throttle without a real socket.
|
|
8
|
+
"""
|
|
9
|
+
import asyncio
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from types import SimpleNamespace
|
|
13
|
+
|
|
14
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src"))
|
|
15
|
+
|
|
16
|
+
import aidot.device_client as dc_mod
|
|
17
|
+
|
|
18
|
+
DeviceClient = dc_mod.DeviceClient
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _client(model_id):
|
|
22
|
+
c = DeviceClient.__new__(DeviceClient)
|
|
23
|
+
c.info = SimpleNamespace(model_id=model_id)
|
|
24
|
+
c._ip_address = "192.168.1.50"
|
|
25
|
+
c._connecting = False
|
|
26
|
+
c._connect_and_login = False
|
|
27
|
+
c._last_login_attempt = 0.0
|
|
28
|
+
c._connected = [] # record connect() calls
|
|
29
|
+
async def _fake_connect(ip):
|
|
30
|
+
c._connected.append(ip)
|
|
31
|
+
c.connect = _fake_connect
|
|
32
|
+
return c
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_camera_never_does_base_login():
|
|
36
|
+
cam = _client("LK.IPC.A001513") # battery SDES camera
|
|
37
|
+
asyncio.run(cam.async_login())
|
|
38
|
+
assert cam._connected == [] # camera excluded - no TCP:10000 login
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_ptz_camera_excluded():
|
|
42
|
+
cam = _client("LK.IPC.A001064")
|
|
43
|
+
asyncio.run(cam.async_login())
|
|
44
|
+
assert cam._connected == []
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_light_still_logs_in():
|
|
48
|
+
light = _client("lk.WIFI-RGBWLight-D0006")
|
|
49
|
+
asyncio.run(light.async_login())
|
|
50
|
+
assert light._connected == ["192.168.1.50"] # lights keep the base login
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_unknown_model_logs_in():
|
|
54
|
+
# No "IPC" in model -> treated as a (light) device that uses the base channel.
|
|
55
|
+
dev = _client("")
|
|
56
|
+
asyncio.run(dev.async_login())
|
|
57
|
+
assert dev._connected == ["192.168.1.50"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_update_ip_address_throttles_relogin():
|
|
61
|
+
light = _client("lk.WIFI-RGBWLight-D0006")
|
|
62
|
+
spawned = []
|
|
63
|
+
# update_ip_address schedules async_login via create_task; capture the calls
|
|
64
|
+
# by counting how many times the throttle lets a login attempt through.
|
|
65
|
+
light._login_task = None
|
|
66
|
+
orig = dc_mod.asyncio.create_task
|
|
67
|
+
|
|
68
|
+
def _count_task(coro):
|
|
69
|
+
spawned.append(1)
|
|
70
|
+
coro.close() # don't actually run the login
|
|
71
|
+
return SimpleNamespace(done=lambda: True, cancel=lambda: None)
|
|
72
|
+
|
|
73
|
+
dc_mod.asyncio.create_task = _count_task
|
|
74
|
+
try:
|
|
75
|
+
light.update_ip_address("192.168.1.50") # first: allowed
|
|
76
|
+
light.update_ip_address("192.168.1.50") # immediate repeat: throttled
|
|
77
|
+
light.update_ip_address("192.168.1.50") # still throttled
|
|
78
|
+
finally:
|
|
79
|
+
dc_mod.asyncio.create_task = orig
|
|
80
|
+
assert len(spawned) == 1, f"expected 1 login within the 30s window, got {len(spawned)}"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
import traceback
|
|
85
|
+
_fail = 0
|
|
86
|
+
for _k, _v in sorted(globals().items()):
|
|
87
|
+
if _k.startswith("test_"):
|
|
88
|
+
try:
|
|
89
|
+
_v()
|
|
90
|
+
print(f"PASS {_k}")
|
|
91
|
+
except Exception:
|
|
92
|
+
_fail += 1
|
|
93
|
+
print(f"FAIL {_k}")
|
|
94
|
+
traceback.print_exc()
|
|
95
|
+
raise SystemExit(1 if _fail else 0)
|
|
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.7.32 → python_aidot_cameras-0.7.33}/src/aidot/models/device_client_model.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/models/device_model.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/src/aidot/models/discover_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
|
{python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_highport_nomination.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_no_undefined_names.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/tests/test_post_merge_hardening.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.7.32 → python_aidot_cameras-0.7.33}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|