python-aidot-cameras 0.7.28__tar.gz → 0.7.30__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.28/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.7.30}/PKG-INFO +2 -2
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/README.md +1 -1
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/pyproject.toml +1 -1
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/camera/client.py +46 -17
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/camera/protocol.py +45 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30/src/python_aidot_cameras.egg-info}/PKG-INFO +2 -2
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_persistent_mqtt.py +12 -10
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/LICENSE +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/setup.cfg +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/__init__.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/aes_utils.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/camera/__init__.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/camera/constants.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/camera/controls.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/camera/models.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/camera/playback.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/camera/sdes.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/camera/tutk.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/client.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/const.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/credentials.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/device_client.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/discover.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/exceptions.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/g711.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/login_const.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/models/__init__.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/models/device_client_model.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/models/device_model.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/models/discover_model.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/py.typed +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/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.30
|
|
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
|
|
@@ -122,7 +122,7 @@ chosen to work out of the box; override only when tuning.
|
|
|
122
122
|
| `AIDOT_SPROP_DIR` | Directory where captured SPS/PPS (sprop) parameter sets are cached. Set this to a writable path (e.g. for Home Assistant) if the default location is read-only. | `<package dir>` |
|
|
123
123
|
| `AIDOT_DISABLE_HIGHPORT_FIX` | If set (any value), disables the DTLS high-port `USE-CANDIDATE` nomination fix and falls back to upstream aioice behavior (used to measure the baseline connect rate). | unset (fix enabled) |
|
|
124
124
|
| `AIDOT_FAST_CONNECT` | Enables LAN-direct "fast connect" mode (STUN-only, skips several cloud signaling waits) when set to a truthy value. | unset (off) |
|
|
125
|
-
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for device commands
|
|
125
|
+
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for device commands, attribute fetches, AND stream-open signaling (matching the official app) instead of connecting per operation, cutting cloud connect churn. **On by default** (the app's behaviour; live soak cut SDES NO_MEDIA ~57%→~19%); set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
126
126
|
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first (skip livePlay waits + TURN relay pre-alloc) and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed for a camera. Truthy value enables. | unset (off) |
|
|
127
127
|
| `AIDOT_SDES_FAST_LIVEPLAY` | Truthy value skips the `livePlayResp` blocking wait for eligible SDES cameras (~4.5 s faster cold start), while keeping the full ICE/SCTP handshake. Role-reversal models (A001064 PTZ) are always excluded. Soak-validated; opt-in. | unset (off) |
|
|
128
128
|
| `AIDOT_SERVE_RELAY` | Holds the public stream port via an internal relay that proxies to ffmpeg, so the first (cold) view connects instead of failing while ffmpeg can't pre-bind the port. Set to `0` to serve ffmpeg directly. | `1` (enabled) |
|
|
@@ -95,7 +95,7 @@ chosen to work out of the box; override only when tuning.
|
|
|
95
95
|
| `AIDOT_SPROP_DIR` | Directory where captured SPS/PPS (sprop) parameter sets are cached. Set this to a writable path (e.g. for Home Assistant) if the default location is read-only. | `<package dir>` |
|
|
96
96
|
| `AIDOT_DISABLE_HIGHPORT_FIX` | If set (any value), disables the DTLS high-port `USE-CANDIDATE` nomination fix and falls back to upstream aioice behavior (used to measure the baseline connect rate). | unset (fix enabled) |
|
|
97
97
|
| `AIDOT_FAST_CONNECT` | Enables LAN-direct "fast connect" mode (STUN-only, skips several cloud signaling waits) when set to a truthy value. | unset (off) |
|
|
98
|
-
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for device commands
|
|
98
|
+
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for device commands, attribute fetches, AND stream-open signaling (matching the official app) instead of connecting per operation, cutting cloud connect churn. **On by default** (the app's behaviour; live soak cut SDES NO_MEDIA ~57%→~19%); set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
99
99
|
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first (skip livePlay waits + TURN relay pre-alloc) and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed for a camera. Truthy value enables. | unset (off) |
|
|
100
100
|
| `AIDOT_SDES_FAST_LIVEPLAY` | Truthy value skips the `livePlayResp` blocking wait for eligible SDES cameras (~4.5 s faster cold start), while keeping the full ICE/SCTP handshake. Role-reversal models (A001064 PTZ) are always excluded. Soak-validated; opt-in. | unset (off) |
|
|
101
101
|
| `AIDOT_SERVE_RELAY` | Holds the public stream port via an internal relay that proxies to ffmpeg, so the first (cold) view connects instead of failing while ffmpeg can't pre-bind the port. Set to `0` to serve ffmpeg directly. | `1` (enabled) |
|
|
@@ -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.30"
|
|
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"
|
|
@@ -2682,16 +2682,21 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
2682
2682
|
"1", "true", "yes", "on")
|
|
2683
2683
|
|
|
2684
2684
|
def _resolve_persistent_mqtt(self) -> bool:
|
|
2685
|
-
"""Whether commands
|
|
2686
|
-
MQTT connection (matching the app's
|
|
2687
|
-
connecting per op.
|
|
2688
|
-
|
|
2689
|
-
|
|
2685
|
+
"""Whether commands, attribute fetches, AND stream-open signaling reuse ONE
|
|
2686
|
+
account-level persistent MQTT connection (matching the app's
|
|
2687
|
+
LDSBaseMqttServiceImpl) instead of connecting per op.
|
|
2688
|
+
|
|
2689
|
+
**Default ON** (2026-06-17): this is exactly how the official app behaves -
|
|
2690
|
+
one persistent connection per login session - and a live soak showed it
|
|
2691
|
+
cuts SDES NO_MEDIA from ~57% to ~19% (no regression). It is also safer
|
|
2692
|
+
than connect-per-op, which can collide on the single authorized client_id.
|
|
2693
|
+
Disable via ``AIDOT_PERSISTENT_MQTT`` in {0,false,no,off} or per-camera
|
|
2694
|
+
``_persistent_mqtt_opt=False`` (the explicit opt always wins)."""
|
|
2690
2695
|
opt = getattr(self, "_persistent_mqtt_opt", None)
|
|
2691
2696
|
if opt is not None:
|
|
2692
2697
|
return bool(opt)
|
|
2693
|
-
return os.environ.get("AIDOT_PERSISTENT_MQTT", "").strip().lower() in (
|
|
2694
|
-
"
|
|
2698
|
+
return os.environ.get("AIDOT_PERSISTENT_MQTT", "").strip().lower() not in (
|
|
2699
|
+
"0", "false", "no", "off")
|
|
2695
2700
|
|
|
2696
2701
|
async def _get_persistent_mqtt(self):
|
|
2697
2702
|
"""Get-or-create the account-shared ``_PersistentMqtt`` (one per account,
|
|
@@ -4093,16 +4098,40 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
4093
4098
|
)
|
|
4094
4099
|
)
|
|
4095
4100
|
|
|
4096
|
-
#
|
|
4097
|
-
#
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4101
|
+
# MQTT transport for the stream signaling. Default: a dedicated
|
|
4102
|
+
# connect-per-stream session in an executor (stopped via outgoing_q
|
|
4103
|
+
# sentinel on WebRTCSession.stop()). AIDOT_PERSISTENT_MQTT (Phase 2):
|
|
4104
|
+
# ride the SAME account-level persistent connection commands/attrs use
|
|
4105
|
+
# (the stream's mqtt_cid IS the authorized mqttClientId, so it's the same
|
|
4106
|
+
# connection) - subscribe + register a handler + drain outgoing_q through
|
|
4107
|
+
# it, and DON'T tear the connection down on stop (matching the app).
|
|
4108
|
+
_pm_stream = (await self._get_persistent_mqtt()
|
|
4109
|
+
if self._resolve_persistent_mqtt() else None)
|
|
4110
|
+
if _pm_stream is not None:
|
|
4111
|
+
await _pm_stream.subscribe(sub_topics)
|
|
4112
|
+
_pm_stream.add_handler(_on_mqtt_message)
|
|
4113
|
+
|
|
4114
|
+
async def _pm_stream_drain():
|
|
4115
|
+
try:
|
|
4116
|
+
while True:
|
|
4117
|
+
out = await loop.run_in_executor(None, outgoing_q.get)
|
|
4118
|
+
if out is None: # stop sentinel from WebRTCSession.stop()
|
|
4119
|
+
return
|
|
4120
|
+
await _pm_stream.publish(out[0], out[1])
|
|
4121
|
+
finally:
|
|
4122
|
+
_pm_stream.remove_handler(_on_mqtt_message)
|
|
4123
|
+
|
|
4124
|
+
mqtt_fut = asyncio.ensure_future(_pm_stream_drain())
|
|
4125
|
+
_on_mqtt_ready({"connected": True, "rc": 0, "rc_str": "persistent"})
|
|
4126
|
+
else:
|
|
4127
|
+
mqtt_fut = loop.run_in_executor(
|
|
4128
|
+
None,
|
|
4129
|
+
lambda: _mqtt_session_sync(
|
|
4130
|
+
mqtt_url, mqtt_user, mqtt_pwd, mqtt_cid,
|
|
4131
|
+
sub_topics, [], 3600.0, _on_mqtt_message,
|
|
4132
|
+
"/mqtt", _on_mqtt_ready, outgoing_q,
|
|
4133
|
+
),
|
|
4134
|
+
)
|
|
4106
4135
|
|
|
4107
4136
|
# Wait for MQTT to be connected and subscribed before proceeding.
|
|
4108
4137
|
# threading.Event.wait(timeout) returns True if set, False on timeout.
|
|
@@ -1270,6 +1270,7 @@ class _PersistentMqtt:
|
|
|
1270
1270
|
self._lock = threading.Lock()
|
|
1271
1271
|
self._subs = set() # topics to (re)subscribe on connect
|
|
1272
1272
|
self._collectors = [] # transient queues, each receives every msg
|
|
1273
|
+
self._handlers = [] # persistent on_message callbacks (e.g. a stream)
|
|
1273
1274
|
self._started = False
|
|
1274
1275
|
self.connects = 0 # observability: how many times we connected
|
|
1275
1276
|
|
|
@@ -1330,8 +1331,14 @@ class _PersistentMqtt:
|
|
|
1330
1331
|
item = (msg.topic, payload)
|
|
1331
1332
|
with self._lock:
|
|
1332
1333
|
cols = list(self._collectors)
|
|
1334
|
+
handlers = list(self._handlers)
|
|
1333
1335
|
for q in cols:
|
|
1334
1336
|
q.put(item)
|
|
1337
|
+
for h in handlers: # persistent subscribers (stream, etc.)
|
|
1338
|
+
try:
|
|
1339
|
+
h(msg.topic, payload)
|
|
1340
|
+
except Exception:
|
|
1341
|
+
_LOGGER.debug("persistent mqtt: handler raised", exc_info=True)
|
|
1335
1342
|
|
|
1336
1343
|
def _ensure_started_sync(self, timeout=15.0):
|
|
1337
1344
|
with self._lock:
|
|
@@ -1409,6 +1416,44 @@ class _PersistentMqtt:
|
|
|
1409
1416
|
return await loop.run_in_executor(
|
|
1410
1417
|
None, functools.partial(self._ensure_started_sync, timeout))
|
|
1411
1418
|
|
|
1419
|
+
# --- persistent subscriber API (for the stream signaling, Phase 2) -------- #
|
|
1420
|
+
def add_handler(self, callback):
|
|
1421
|
+
"""Register a persistent on_message callback ``callback(topic, payload)``
|
|
1422
|
+
that receives every message for the connection's lifetime (until removed).
|
|
1423
|
+
Use for a long-lived consumer like an open stream's signaling handler."""
|
|
1424
|
+
with self._lock:
|
|
1425
|
+
self._handlers.append(callback)
|
|
1426
|
+
return callback
|
|
1427
|
+
|
|
1428
|
+
def remove_handler(self, callback):
|
|
1429
|
+
with self._lock:
|
|
1430
|
+
try:
|
|
1431
|
+
self._handlers.remove(callback)
|
|
1432
|
+
except ValueError:
|
|
1433
|
+
pass
|
|
1434
|
+
|
|
1435
|
+
async def subscribe(self, topics):
|
|
1436
|
+
"""Ensure the connection is up and subscribe ``topics`` (tracked for replay)."""
|
|
1437
|
+
import functools
|
|
1438
|
+
loop = asyncio.get_running_loop()
|
|
1439
|
+
await loop.run_in_executor(None, self._ensure_started_sync, 15.0)
|
|
1440
|
+
await loop.run_in_executor(None, functools.partial(self._subscribe_sync, topics))
|
|
1441
|
+
|
|
1442
|
+
async def publish(self, topic, payload):
|
|
1443
|
+
"""Publish on the shared connection (ensures it's up first)."""
|
|
1444
|
+
import functools
|
|
1445
|
+
|
|
1446
|
+
def _pub():
|
|
1447
|
+
if not self._ensure_started_sync():
|
|
1448
|
+
return False
|
|
1449
|
+
try:
|
|
1450
|
+
self._client.publish(topic, payload)
|
|
1451
|
+
return True
|
|
1452
|
+
except Exception:
|
|
1453
|
+
return False
|
|
1454
|
+
loop = asyncio.get_running_loop()
|
|
1455
|
+
return await loop.run_in_executor(None, functools.partial(_pub))
|
|
1456
|
+
|
|
1412
1457
|
def close(self):
|
|
1413
1458
|
c = self._client
|
|
1414
1459
|
self._client = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.30
|
|
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
|
|
@@ -122,7 +122,7 @@ chosen to work out of the box; override only when tuning.
|
|
|
122
122
|
| `AIDOT_SPROP_DIR` | Directory where captured SPS/PPS (sprop) parameter sets are cached. Set this to a writable path (e.g. for Home Assistant) if the default location is read-only. | `<package dir>` |
|
|
123
123
|
| `AIDOT_DISABLE_HIGHPORT_FIX` | If set (any value), disables the DTLS high-port `USE-CANDIDATE` nomination fix and falls back to upstream aioice behavior (used to measure the baseline connect rate). | unset (fix enabled) |
|
|
124
124
|
| `AIDOT_FAST_CONNECT` | Enables LAN-direct "fast connect" mode (STUN-only, skips several cloud signaling waits) when set to a truthy value. | unset (off) |
|
|
125
|
-
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for device commands
|
|
125
|
+
| `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for device commands, attribute fetches, AND stream-open signaling (matching the official app) instead of connecting per operation, cutting cloud connect churn. **On by default** (the app's behaviour; live soak cut SDES NO_MEDIA ~57%→~19%); set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
|
|
126
126
|
| `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first (skip livePlay waits + TURN relay pre-alloc) and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed for a camera. Truthy value enables. | unset (off) |
|
|
127
127
|
| `AIDOT_SDES_FAST_LIVEPLAY` | Truthy value skips the `livePlayResp` blocking wait for eligible SDES cameras (~4.5 s faster cold start), while keeping the full ICE/SCTP handshake. Role-reversal models (A001064 PTZ) are always excluded. Soak-validated; opt-in. | unset (off) |
|
|
128
128
|
| `AIDOT_SERVE_RELAY` | Holds the public stream port via an internal relay that proxies to ffmpeg, so the first (cold) view connects instead of failing while ffmpeg can't pre-bind the port. Set to `0` to serve ffmpeg directly. | `1` (enabled) |
|
|
@@ -21,25 +21,27 @@ def _cam():
|
|
|
21
21
|
return _CAM.__new__(_CAM)
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def
|
|
24
|
+
def test_default_on(monkeypatch):
|
|
25
|
+
# default ON (matches the app's single persistent connection)
|
|
25
26
|
monkeypatch.delenv("AIDOT_PERSISTENT_MQTT", raising=False)
|
|
26
|
-
assert _cam()._resolve_persistent_mqtt() is
|
|
27
|
+
assert _cam()._resolve_persistent_mqtt() is True
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
def
|
|
30
|
-
for val in ("
|
|
30
|
+
def test_env_disables(monkeypatch):
|
|
31
|
+
for val in ("0", "false", "FALSE", "no", "off", " Off "):
|
|
31
32
|
monkeypatch.setenv("AIDOT_PERSISTENT_MQTT", val)
|
|
32
|
-
assert _cam()._resolve_persistent_mqtt() is
|
|
33
|
+
assert _cam()._resolve_persistent_mqtt() is False, val
|
|
33
34
|
|
|
34
35
|
|
|
35
|
-
def
|
|
36
|
-
for val in ("
|
|
36
|
+
def test_env_truthy_or_unknown_stays_on(monkeypatch):
|
|
37
|
+
for val in ("1", "true", "yes", "on", "anything"):
|
|
37
38
|
monkeypatch.setenv("AIDOT_PERSISTENT_MQTT", val)
|
|
38
|
-
assert _cam()._resolve_persistent_mqtt() is
|
|
39
|
+
assert _cam()._resolve_persistent_mqtt() is True, val
|
|
39
40
|
|
|
40
41
|
|
|
41
|
-
def
|
|
42
|
-
|
|
42
|
+
def test_kwarg_off_wins_over_default(monkeypatch):
|
|
43
|
+
# an explicit opt=False (e.g. user turned the HA toggle off) overrides default-on
|
|
44
|
+
monkeypatch.delenv("AIDOT_PERSISTENT_MQTT", raising=False)
|
|
43
45
|
cam = _cam()
|
|
44
46
|
cam._persistent_mqtt_opt = False
|
|
45
47
|
assert cam._resolve_persistent_mqtt() is False
|
|
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.28 → python_aidot_cameras-0.7.30}/src/aidot/models/device_client_model.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/src/aidot/models/device_model.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/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
|
|
File without changes
|
{python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_highport_nomination.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_no_undefined_names.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/tests/test_post_merge_hardening.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.7.28 → python_aidot_cameras-0.7.30}/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
|