python-aidot-cameras 0.5.14__tar.gz → 0.5.16__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.
Files changed (32) hide show
  1. {python_aidot_cameras-0.5.14/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.5.16}/PKG-INFO +1 -1
  2. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/pyproject.toml +1 -1
  3. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/src/aidot/device_client.py +47 -11
  4. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16/src/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  5. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/LICENSE +0 -0
  6. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/README.md +0 -0
  7. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/setup.cfg +0 -0
  8. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/src/aidot/__init__.py +0 -0
  9. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/src/aidot/aes_utils.py +0 -0
  10. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/src/aidot/client.py +0 -0
  11. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/src/aidot/const.py +0 -0
  12. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/src/aidot/credentials.py +0 -0
  13. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/src/aidot/discover.py +0 -0
  14. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/src/aidot/exceptions.py +0 -0
  15. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/src/aidot/g711.py +0 -0
  16. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/src/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
  17. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/src/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  18. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/src/python_aidot_cameras.egg-info/requires.txt +0 -0
  19. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/src/python_aidot_cameras.egg-info/top_level.txt +0 -0
  20. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/tests/test_alarm_event.py +0 -0
  21. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/tests/test_highport_nomination.py +0 -0
  22. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/tests/test_motion_poll.py +0 -0
  23. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/tests/test_no_undefined_names.py +0 -0
  24. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/tests/test_sdes_idle_release.py +0 -0
  25. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/tests/test_sdes_sprop.py +0 -0
  26. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/tests/test_sdes_talk.py +0 -0
  27. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/tests/test_sdes_watchdog.py +0 -0
  28. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/tests/test_speak.py +0 -0
  29. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/tests/test_stream_cap.py +0 -0
  30. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/tests/test_talk.py +0 -0
  31. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/tests/test_terminal_ack.py +0 -0
  32. {python_aidot_cameras-0.5.14 → python_aidot_cameras-0.5.16}/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.5.14
3
+ Version: 0.5.16
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.5.14"
7
+ version = "0.5.16"
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"
@@ -3844,20 +3844,25 @@ class DeviceClient(object):
3844
3844
  )
3845
3845
  return []
3846
3846
 
3847
- async def async_get_event_video_url(self, event_uuid: str) -> Optional[str]:
3848
- """Return a direct playback URL (MP4 or M3U8) for a specific recording.
3847
+ async def async_get_event_video_media(
3848
+ self, event_uuid: str
3849
+ ) -> Optional[tuple[str, str]]:
3850
+ """Return (url, mime_type) for a specific cloud recording.
3849
3851
 
3850
- event_uuid: the eventUuid field from an async_get_cloud_recordings() item.
3851
- Returns the first video URL from getEventVideoUrl, or None on error.
3852
+ Prefers MP4 (type=1) over M3U8 (type=2) because HA's media player and
3853
+ most browsers handle direct MP4 more reliably than HLS with signed CDN
3854
+ segments. Falls back to whatever is available.
3852
3855
 
3853
3856
  From CloudPlaybackFragment.java / EventListRepos.java:
3854
3857
  POST /api/ipc/playback/getEventVideoUrl
3855
3858
  body: {deviceId, eventList: [{eventUuid}]}
3856
- response: data.eventUrlList[0].videoUrlList[0].url
3857
- type 1 = MP4, type 2 = M3U8
3859
+ response: data.eventUrlList[0].videoUrlList[]
3860
+ each item: {url, type} (type 1=MP4, type 2=M3U8)
3858
3861
  """
3859
3862
  import aiohttp
3860
3863
 
3864
+ _MIME = {1: "video/mp4", 2: "application/x-mpegURL"}
3865
+
3861
3866
  try:
3862
3867
  async with aiohttp.ClientSession() as session:
3863
3868
  async with session.post(
@@ -3871,6 +3876,8 @@ class DeviceClient(object):
3871
3876
  ) as resp:
3872
3877
  body = await resp.json(content_type=None)
3873
3878
 
3879
+ _LOGGER.debug("getEventVideoUrl raw for %s uuid=%s: %s", self.device_id, event_uuid, body)
3880
+
3874
3881
  if body.get("code") != 200:
3875
3882
  _LOGGER.warning(
3876
3883
  "getEventVideoUrl code=%s for %s uuid=%s: %s",
@@ -3884,15 +3891,35 @@ class DeviceClient(object):
3884
3891
  video_url_list = (event_url_list[0] or {}).get("videoUrlList") or []
3885
3892
  if not video_url_list:
3886
3893
  return None
3887
- return video_url_list[0].get("url") or None
3894
+
3895
+ # Prefer MP4 (type=1), fall back to first available entry
3896
+ mp4 = next((v for v in video_url_list if v.get("type") == 1), None)
3897
+ entry = mp4 or video_url_list[0]
3898
+ url = entry.get("url")
3899
+ if not url:
3900
+ return None
3901
+ mime = _MIME.get(entry.get("type"), "video/mp4")
3902
+ _LOGGER.debug(
3903
+ "getEventVideoUrl resolved for %s: type=%s mime=%s url=%s",
3904
+ self.device_id, entry.get("type"), mime, url,
3905
+ )
3906
+ return url, mime
3888
3907
 
3889
3908
  except Exception as exc:
3890
3909
  _LOGGER.error(
3891
- "async_get_event_video_url failed for %s uuid=%s: %s",
3910
+ "async_get_event_video_media failed for %s uuid=%s: %s",
3892
3911
  self.device_id, event_uuid, exc,
3893
3912
  )
3894
3913
  return None
3895
3914
 
3915
+ async def async_get_event_video_url(self, event_uuid: str) -> Optional[str]:
3916
+ """Return the video URL for a cloud recording (URL only, no MIME type).
3917
+
3918
+ Prefer async_get_event_video_media() when you also need the MIME type.
3919
+ """
3920
+ result = await self.async_get_event_video_media(event_uuid)
3921
+ return result[0] if result else None
3922
+
3896
3923
  async def async_get_latest_thumbnail(self) -> Optional[str]:
3897
3924
  """Return the URL of the most recent event/motion thumbnail for this camera.
3898
3925
 
@@ -12464,7 +12491,12 @@ class DeviceClient(object):
12464
12491
  return
12465
12492
  data_len = len(data)
12466
12493
  if data_len <= 0:
12467
- _LOGGER.error("recv data error len, exit socket")
12494
+ # Peer closed the control socket (idle cloud socket, device
12495
+ # reboot, or a brief Wi-Fi blip). reset() reconnects, so this is
12496
+ # a recoverable hiccup, not an error-level condition.
12497
+ _LOGGER.debug(
12498
+ "%s control socket closed by peer; reconnecting", self.device_id
12499
+ )
12468
12500
  await self.reset()
12469
12501
  self.status.online = False
12470
12502
  return
@@ -12580,8 +12612,12 @@ class DeviceClient(object):
12580
12612
  }
12581
12613
  try:
12582
12614
  if self.ping_count >= 2:
12583
- _LOGGER.error(
12584
- f"Last ping did not return within 20 seconds. device id:{self.device_id}"
12615
+ # Two pings (~20s) went unanswered: the device is briefly
12616
+ # unreachable (Wi-Fi congestion, device busy). reset() reconnects
12617
+ # automatically, so this is a recoverable hiccup, not an error.
12618
+ _LOGGER.warning(
12619
+ "No ping response within 20s from %s; resetting connection "
12620
+ "(will reconnect)", self.device_id,
12585
12621
  )
12586
12622
  await self.reset()
12587
12623
  return -1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 0.5.14
3
+ Version: 0.5.16
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