python-aidot-cameras 0.7.14__tar.gz → 0.7.15__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.14/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.7.15}/PKG-INFO +1 -1
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/pyproject.toml +1 -1
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/camera/client.py +69 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15/src/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
- python_aidot_cameras-0.7.15/tests/test_live_stream_param.py +75 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/LICENSE +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/README.md +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/setup.cfg +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/__init__.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/aes_utils.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/camera/__init__.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/camera/constants.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/camera/controls.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/camera/models.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/camera/playback.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/camera/protocol.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/camera/sdes.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/camera/tutk.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/client.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/const.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/credentials.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/device_client.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/discover.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/exceptions.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/g711.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/login_const.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/models/__init__.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/models/device_client_model.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/models/device_model.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/models/discover_model.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/py.typed +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/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.15
|
|
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.15"
|
|
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"
|
|
@@ -678,6 +678,60 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
678
678
|
_LOGGER.debug("http wake failed for %s: %s", self.device_id, exc)
|
|
679
679
|
return False
|
|
680
680
|
|
|
681
|
+
def _live_stream_param_request(self):
|
|
682
|
+
"""Build ``(url, headers, body)`` for the liveStreamParam provision call.
|
|
683
|
+
|
|
684
|
+
Pure/deterministic - unit-tested in tests/test_live_stream_param.py. The
|
|
685
|
+
body MUST be a JSON array of device ids (object body -> HTTP 500) and the
|
|
686
|
+
``owner`` header is required (as on the wake endpoint).
|
|
687
|
+
"""
|
|
688
|
+
headers = self._leedarson_headers()
|
|
689
|
+
headers["owner"] = (
|
|
690
|
+
self._user_info.get("owner")
|
|
691
|
+
or self._user_info.get("id")
|
|
692
|
+
or self._user_info.get("userId")
|
|
693
|
+
or str(self.user_id)
|
|
694
|
+
)
|
|
695
|
+
url = f"{self._smarthome_base}/api/ipc/liveStream/liveStreamParam"
|
|
696
|
+
return url, headers, json.dumps([self.device_id])
|
|
697
|
+
|
|
698
|
+
async def _async_fetch_live_stream_param(self) -> bool:
|
|
699
|
+
"""Provision the camera's live-stream session via the cloud (app parity).
|
|
700
|
+
|
|
701
|
+
Battery cameras waking from deep sleep reject the MQTT ``livePlayReq``
|
|
702
|
+
with code ``-50019`` ("not ready") and then never run ICE - so no media
|
|
703
|
+
ever flows - unless the app's pre-connect HTTP call has armed the session
|
|
704
|
+
first. The official app (``KVSPreConnectStrategy.fetchKvsParams``) POSTs
|
|
705
|
+
the device id to ``/api/ipc/liveStream/liveStreamParam``; the cloud
|
|
706
|
+
provisions the session (and brings the camera online) and returns AWS KVS
|
|
707
|
+
credentials. We only need the provisioning side effect - the library
|
|
708
|
+
streams over the proprietary MQTT/SDES path, not AWS KVS - so the returned
|
|
709
|
+
credentials are ignored.
|
|
710
|
+
|
|
711
|
+
The request body is a JSON **array** of device ids (an object body returns
|
|
712
|
+
HTTP 500) and needs the ``owner`` header (like the wake endpoint).
|
|
713
|
+
Best-effort: returns True on HTTP 200, never raises.
|
|
714
|
+
"""
|
|
715
|
+
import aiohttp
|
|
716
|
+
|
|
717
|
+
url, headers, body = self._live_stream_param_request()
|
|
718
|
+
try:
|
|
719
|
+
async with aiohttp.ClientSession() as session:
|
|
720
|
+
async with session.post(
|
|
721
|
+
url, headers=headers, data=body,
|
|
722
|
+
timeout=aiohttp.ClientTimeout(total=10),
|
|
723
|
+
) as resp:
|
|
724
|
+
status = resp.status
|
|
725
|
+
data = await resp.json(content_type=None)
|
|
726
|
+
code = data.get("code") if isinstance(data, dict) else None
|
|
727
|
+
ok = status == 200 and (code in (None, 0, 200, "0", "200"))
|
|
728
|
+
_LOGGER.debug("liveStreamParam %s: status=%s code=%s ok=%s",
|
|
729
|
+
self.device_id, status, code, ok)
|
|
730
|
+
return bool(ok)
|
|
731
|
+
except Exception as exc:
|
|
732
|
+
_LOGGER.debug("liveStreamParam failed for %s: %s", self.device_id, exc)
|
|
733
|
+
return False
|
|
734
|
+
|
|
681
735
|
async def async_wake_camera(self, retries: int = 2) -> bool:
|
|
682
736
|
"""Wake a battery camera on demand via the cloud HTTP low-power endpoint.
|
|
683
737
|
|
|
@@ -3108,6 +3162,21 @@ class CameraMixin(_CameraControlsMixin):
|
|
|
3108
3162
|
|
|
3109
3163
|
use_sdes = force_sdes if force_sdes is not None else self.is_sdes_camera
|
|
3110
3164
|
|
|
3165
|
+
# Battery cameras must have their live-stream session provisioned by the
|
|
3166
|
+
# cloud BEFORE MQTT signaling, or the camera rejects livePlayReq with
|
|
3167
|
+
# -50019 ("not ready") and never runs ICE -> no media. App parity:
|
|
3168
|
+
# KVSPreConnectStrategy.fetchKvsParams calls liveStreamParam first. We
|
|
3169
|
+
# only need the provisioning side effect (we stream over MQTT/SDES, not
|
|
3170
|
+
# AWS KVS). Mains cameras are always stream-ready, so skip them. Disable
|
|
3171
|
+
# via AIDOT_LIVESTREAM_PARAM=0. Validated live on L2_170 (A001513): with
|
|
3172
|
+
# this call livePlay succeeds and decrypted RTP flows; without it,
|
|
3173
|
+
# persistent -50019.
|
|
3174
|
+
if (self.is_battery_camera
|
|
3175
|
+
and os.environ.get("AIDOT_LIVESTREAM_PARAM", "1") != "0"):
|
|
3176
|
+
_lsp_ok = await self._async_fetch_live_stream_param()
|
|
3177
|
+
_LOGGER.debug("camera %s: liveStreamParam provisioned ok=%s",
|
|
3178
|
+
self.device_id, _lsp_ok)
|
|
3179
|
+
|
|
3111
3180
|
# AIDOT_FAST_CONNECT (default off): LAN-direct mode. Both transports stall
|
|
3112
3181
|
# the offer on a TURN relay allocation to the cloud TURN server before ICE
|
|
3113
3182
|
# can even start - for DTLS, aiortc's setLocalDescription blocks until ICE
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.15
|
|
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
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Unit tests for the liveStreamParam provision-request builder.
|
|
2
|
+
|
|
3
|
+
Battery cameras (e.g. L2_170, A001513) must have their live-stream session
|
|
4
|
+
provisioned by the cloud before MQTT signaling, or they reject livePlayReq with
|
|
5
|
+
-50019 and never stream. The request shape is exact: a JSON ARRAY body of device
|
|
6
|
+
ids (an object body returns HTTP 500) plus an ``owner`` header. This locks that
|
|
7
|
+
format in. No network: builds the request from a bare instance.
|
|
8
|
+
"""
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src"))
|
|
14
|
+
|
|
15
|
+
import aidot.camera.client as cc
|
|
16
|
+
|
|
17
|
+
# The camera mixin class that defines the builder (avoid hard-coding the name).
|
|
18
|
+
_CAM = next(v for v in vars(cc).values()
|
|
19
|
+
if isinstance(v, type) and "_live_stream_param_request" in v.__dict__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _stub(**over):
|
|
23
|
+
cam = _CAM.__new__(_CAM)
|
|
24
|
+
cam._user_info = over.get("user_info", {"accessToken": "TKN", "id": "owner-123"})
|
|
25
|
+
cam._region = "us" # _smarthome_base is a property derived from _region
|
|
26
|
+
cam.device_id = over.get("device_id", "DEV1234")
|
|
27
|
+
cam.user_id = "U1"
|
|
28
|
+
return cam
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_url_is_livestreamparam_endpoint():
|
|
32
|
+
url, _, _ = _stub()._live_stream_param_request()
|
|
33
|
+
assert url.endswith("/api/ipc/liveStream/liveStreamParam")
|
|
34
|
+
assert url.startswith("https://")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_body_is_json_array_of_device_id():
|
|
38
|
+
# MUST be a JSON array - an object body ({"deviceId": id}) returns HTTP 500.
|
|
39
|
+
_, _, body = _stub()._live_stream_param_request()
|
|
40
|
+
parsed = json.loads(body)
|
|
41
|
+
assert isinstance(parsed, list)
|
|
42
|
+
assert parsed == ["DEV1234"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_owner_header_required():
|
|
46
|
+
# The endpoint needs owner (like the wake endpoint); falls back across keys.
|
|
47
|
+
_, headers, _ = _stub()._live_stream_param_request()
|
|
48
|
+
assert headers.get("owner") == "owner-123"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_owner_falls_back_to_user_id():
|
|
52
|
+
cam = _stub(user_info={"accessToken": "T"}) # no id/owner/userId
|
|
53
|
+
_, headers, _ = cam._live_stream_param_request()
|
|
54
|
+
assert headers.get("owner") == "U1"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_auth_headers_carried():
|
|
58
|
+
_, headers, _ = _stub()._live_stream_param_request()
|
|
59
|
+
assert headers.get("token") == "TKN"
|
|
60
|
+
assert headers.get("terminal") == "app"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
import traceback
|
|
65
|
+
_fail = 0
|
|
66
|
+
for _k, _v in sorted(globals().items()):
|
|
67
|
+
if _k.startswith("test_"):
|
|
68
|
+
try:
|
|
69
|
+
_v()
|
|
70
|
+
print(f"PASS {_k}")
|
|
71
|
+
except Exception:
|
|
72
|
+
_fail += 1
|
|
73
|
+
print(f"FAIL {_k}")
|
|
74
|
+
traceback.print_exc()
|
|
75
|
+
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
|
|
File without changes
|
{python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/models/device_client_model.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/src/aidot/models/device_model.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/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.14 → python_aidot_cameras-0.7.15}/tests/test_highport_nomination.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/tests/test_no_undefined_names.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.7.14 → python_aidot_cameras-0.7.15}/tests/test_post_merge_hardening.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|