python-aidot-cameras 0.5.0__tar.gz → 0.5.2__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 (30) hide show
  1. {python_aidot_cameras-0.5.0/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.5.2}/PKG-INFO +1 -1
  2. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/pyproject.toml +1 -1
  3. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/src/aidot/__init__.py +2 -0
  4. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/src/aidot/client.py +20 -28
  5. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/src/aidot/device_client.py +26 -19
  6. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/src/aidot/discover.py +3 -3
  7. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2/src/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  8. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/src/python_aidot_cameras.egg-info/SOURCES.txt +0 -1
  9. python_aidot_cameras-0.5.0/src/aidot/login_const.py +0 -13
  10. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/LICENSE +0 -0
  11. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/README.md +0 -0
  12. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/setup.cfg +0 -0
  13. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/src/aidot/aes_utils.py +0 -0
  14. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/src/aidot/const.py +0 -0
  15. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/src/aidot/credentials.py +0 -0
  16. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/src/aidot/exceptions.py +0 -0
  17. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/src/aidot/g711.py +0 -0
  18. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/src/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  19. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/src/python_aidot_cameras.egg-info/requires.txt +0 -0
  20. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/src/python_aidot_cameras.egg-info/top_level.txt +0 -0
  21. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/tests/test_alarm_event.py +0 -0
  22. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/tests/test_highport_nomination.py +0 -0
  23. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/tests/test_motion_poll.py +0 -0
  24. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/tests/test_sdes_talk.py +0 -0
  25. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/tests/test_sdes_watchdog.py +0 -0
  26. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/tests/test_speak.py +0 -0
  27. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/tests/test_stream_cap.py +0 -0
  28. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/tests/test_talk.py +0 -0
  29. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/tests/test_terminal_ack.py +0 -0
  30. {python_aidot_cameras-0.5.0 → python_aidot_cameras-0.5.2}/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.0
3
+ Version: 0.5.2
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.0"
7
+ version = "0.5.2"
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"
@@ -6,6 +6,7 @@ from .discover import Discover
6
6
  from .exceptions import (
7
7
  AidotAuthFailed,
8
8
  AidotAuthTokenExpired,
9
+ AidotCameraBusy,
9
10
  AidotError,
10
11
  AidotNotLogin,
11
12
  AidotOSError,
@@ -19,6 +20,7 @@ __all__ = [
19
20
  "AidotClient",
20
21
  "DeviceClient",
21
22
  "Discover",
23
+ "AidotCameraBusy",
22
24
  "AidotError",
23
25
  "AidotAuthFailed",
24
26
  "AidotAuthTokenExpired",
@@ -1,7 +1,6 @@
1
1
  """The aidot integration."""
2
2
 
3
3
  import asyncio
4
- import hashlib
5
4
  import json
6
5
  import logging
7
6
  import base64
@@ -54,10 +53,6 @@ def rsa_password_encrypt(message: str) -> str:
54
53
  return base64.b64encode(encrypted).decode("utf-8")
55
54
 
56
55
 
57
- def md5_password(message: str) -> str:
58
- """MD5 hex digest of the password (for /users/login web-app flow)."""
59
- return hashlib.md5(message.encode("utf-8")).hexdigest()
60
-
61
56
 
62
57
  class AidotClient:
63
58
  _base_url: str = BASE_URL
@@ -128,7 +123,7 @@ class AidotClient:
128
123
  try:
129
124
  response = await self.session.post(url, headers=headers, json=data)
130
125
  response_data = await response.json(content_type=None)
131
- _LOGGER.debug("async_post_login HTTP=%d response: %s", response.status, response_data)
126
+ _LOGGER.debug("async_post_login HTTP=%d code=%s", response.status, response_data.get(CONF_CODE))
132
127
  app_code = response_data.get(CONF_CODE)
133
128
  if app_code == ServerErrorCode.USER_PWD_INCORRECT:
134
129
  raise AidotUserOrPassIncorrect
@@ -183,7 +178,7 @@ class AidotClient:
183
178
  async with self.session.get(url, headers=headers,
184
179
  timeout=aiohttp.ClientTimeout(total=10)) as resp:
185
180
  body = await resp.json(content_type=None)
186
- _LOGGER.debug("userConfig response: %s", body)
181
+ _LOGGER.debug("userConfig response keys=%s", list(body.keys()) if isinstance(body, dict) else type(body).__name__)
187
182
  # The MQTT password field may be named mqttPassword or similar.
188
183
  # Store the full response data alongside login_info for DeviceClient.
189
184
  data = body if isinstance(body, dict) else {}
@@ -253,7 +248,7 @@ class AidotClient:
253
248
  inflight = self._ensure_token_inflight
254
249
  if inflight is not None and not inflight.done():
255
250
  return await inflight
256
- fut = asyncio.get_event_loop().create_future()
251
+ fut = asyncio.get_running_loop().create_future()
257
252
  self._ensure_token_inflight = fut
258
253
  try:
259
254
  result = await self._do_ensure_token()
@@ -342,26 +337,23 @@ class AidotClient:
342
337
 
343
338
  async def async_get_all_device(self) -> dict[str, Any]:
344
339
  final_device_list: list[dict[str, Any]] = []
345
- try:
346
- houses = await self.async_get_houses()
347
- for house in houses:
348
- if house.get(CONF_IS_OWNER) is False:
349
- continue
350
- # get device_list
351
- device_list = await self.async_get_devices(house[CONF_ID])
352
- if device_list:
353
- final_device_list.extend(device_list)
354
-
355
- # get product_list
356
- productIds = ",".join([item[CONF_PRODUCT_ID] for item in final_device_list])
357
- product_list = await self.async_get_products(productIds)
358
-
359
- for product in product_list:
360
- for device in final_device_list:
361
- if device[CONF_PRODUCT_ID] == product[CONF_ID]:
362
- device[CONF_PRODUCT] = product
363
- except Exception:
364
- raise
340
+ houses = await self.async_get_houses()
341
+ for house in houses:
342
+ if house.get(CONF_IS_OWNER) is False:
343
+ continue
344
+ # get device_list
345
+ device_list = await self.async_get_devices(house[CONF_ID])
346
+ if device_list:
347
+ final_device_list.extend(device_list)
348
+
349
+ # get product_list
350
+ productIds = ",".join([item[CONF_PRODUCT_ID] for item in final_device_list])
351
+ product_list = await self.async_get_products(productIds)
352
+
353
+ for product in product_list:
354
+ for device in final_device_list:
355
+ if device[CONF_PRODUCT_ID] == product[CONF_ID]:
356
+ device[CONF_PRODUCT] = product
365
357
 
366
358
  # Share the full device ID list with every DeviceClient so that
367
359
  # batchGetDeviceUserInfo is called with all IDs (the server may return
@@ -992,7 +992,7 @@ class TutkStreamSession:
992
992
 
993
993
  async def start(self) -> bool:
994
994
  """Load native libs, connect P2P, and start the frame-receive thread."""
995
- return await asyncio.get_event_loop().run_in_executor(
995
+ return await asyncio.get_running_loop().run_in_executor(
996
996
  None, self._start_sync)
997
997
 
998
998
  def _start_sync(self) -> bool:
@@ -1211,7 +1211,7 @@ class TutkStreamSession:
1211
1211
  """Signal the receive thread to stop and wait for it."""
1212
1212
  self._stop_event.set()
1213
1213
  if self._thread is not None:
1214
- await asyncio.get_event_loop().run_in_executor(
1214
+ await asyncio.get_running_loop().run_in_executor(
1215
1215
  None, lambda: self._thread.join(timeout=5.0)
1216
1216
  )
1217
1217
 
@@ -1328,7 +1328,7 @@ class LiveStreamSession:
1328
1328
  return False
1329
1329
 
1330
1330
  # Start background receive/heartbeat task.
1331
- self._task = asyncio.get_event_loop().create_task(self._receive_loop())
1331
+ self._task = asyncio.get_running_loop().create_task(self._receive_loop())
1332
1332
  return True
1333
1333
 
1334
1334
  async def stop(self) -> None:
@@ -3433,7 +3433,7 @@ class DeviceClient(object):
3433
3433
  "MotionDetection_Enable", "1" if enabled else "0")
3434
3434
 
3435
3435
  async def async_set_floodlight(self, on: bool, brightness: int = 100) -> bool:
3436
- # Confirmed 2026-05-05: cameras with autoLightEnable=1 (PTZ, Deck) ignore
3436
+ # Confirmed 2026-05-05: cameras with autoLightEnable=1 (A001064 PTZ, A000088) ignore
3437
3437
  # manual LightOnOff commands unless auto-light is disabled first.
3438
3438
  # Turning on: disable auto-light, set LightOnOff=1 (and optional Dimming).
3439
3439
  # Turning off: set LightOnOff=0 only (leave autoLightEnable as-is so the
@@ -4248,7 +4248,7 @@ class DeviceClient(object):
4248
4248
  buf = _io.BytesIO()
4249
4249
  pil_img.save(buf, "JPEG")
4250
4250
  self.latest_jpeg = buf.getvalue()
4251
- self._last_frame_time = asyncio.get_event_loop().time()
4251
+ self._last_frame_time = asyncio.get_running_loop().time()
4252
4252
  except Exception as enc_exc:
4253
4253
  _LOGGER.debug("Streaming encode failed for %s: %s", self.device_id, enc_exc)
4254
4254
 
@@ -4287,7 +4287,7 @@ class DeviceClient(object):
4287
4287
  try:
4288
4288
  while self._streaming_active:
4289
4289
  await asyncio.sleep(5.0)
4290
- elapsed = asyncio.get_event_loop().time() - self._last_frame_time
4290
+ elapsed = asyncio.get_running_loop().time() - self._last_frame_time
4291
4291
  if self._last_frame_time > 0 and elapsed > _WATCHDOG:
4292
4292
  _LOGGER.warning(
4293
4293
  "No frames from %s in %.0fs - restarting stream",
@@ -4417,7 +4417,7 @@ class DeviceClient(object):
4417
4417
  serve_url = self._keepalive_rtsp_url
4418
4418
  _MIN_DELAY, _MAX_DELAY = 5.0, 300.0
4419
4419
  retry_delay = _MIN_DELAY
4420
- loop = asyncio.get_event_loop()
4420
+ loop = asyncio.get_running_loop()
4421
4421
  while self._streaming_active:
4422
4422
  # Thread-safe queues: taps run on the loop, the A/V mux in a thread.
4423
4423
  vq: "_queue.Queue" = _queue.Queue(maxsize=600)
@@ -4600,6 +4600,13 @@ class DeviceClient(object):
4600
4600
  return None
4601
4601
  cmd = [
4602
4602
  "ffmpeg", "-y", "-loglevel", "warning",
4603
+ # Suppress input-side buffering: the PyAV mux already writes
4604
+ # correctly-interleaved, timestamped MPEG-TS to the pipe every
4605
+ # ~20ms. Without +nobuffer ffmpeg's mpegts demuxer accumulates a
4606
+ # read-ahead window (and the output mpegts muxer defaults to 700ms
4607
+ # of A/V interleave delay) before flushing to go2rtc - exactly the
4608
+ # bursty/choppy audio pattern. Matches the SDES serve's approach.
4609
+ "-fflags", "+nobuffer",
4603
4610
  "-i", "pipe:0",
4604
4611
  "-c", "copy", "-f", "mpegts", "-listen", "1", serve_url,
4605
4612
  ]
@@ -5363,7 +5370,7 @@ class DeviceClient(object):
5363
5370
  liveplay_resp_fut: asyncio.Future = loop.create_future() # set on livePlayResp
5364
5371
  camera_reconnect_ev: asyncio.Event = asyncio.Event() # set when camera sends device/connect
5365
5372
  # Mutable flag: set True when setDevAttrNotif delivers sptPreconn:1.
5366
- # Confirmed 2026-05-02: both A000088 Deck and A001064 PTZ report
5373
+ # Confirmed 2026-05-02: both A000088 and A001064 PTZ report
5367
5374
  # sptPreconn:1. AVIO LIVING (SESSION_MODE_REQ=5376) must be sent
5368
5375
  # via the data channel to trigger streaming in PreCon cameras.
5369
5376
  _spt_preconn: list = [False]
@@ -5563,7 +5570,7 @@ class DeviceClient(object):
5563
5570
  # and won't send its own binding requests.
5564
5571
  _extract_cam_ip("setDevAttrNotif", inner, msg)
5565
5572
  # Also capture sptPreconn (PreCon / PreConnect support flag).
5566
- # Confirmed 2026-05-02: both A001064 PTZ and A000088 Deck have
5573
+ # Confirmed 2026-05-02: both A001064 PTZ and A000088 have
5567
5574
  # sptPreconn:1. Per BaseKVSCameraView.k():805-815, the official
5568
5575
  # client sends AVIO LIVING (SESSION_MODE_REQ=5376) via the data
5569
5576
  # channel only when isSupportPreCon() is true. Set the flag
@@ -6312,7 +6319,7 @@ class DeviceClient(object):
6312
6319
 
6313
6320
  # AVIO LIVING (E_CMD_AVIO_CTRL_SESSION_MODE_REQ=5376) is sent
6314
6321
  # unconditionally when DC opens. 2026-05-03 testing confirmed:
6315
- # A000088 cameras (Deck + Bedroom M3 Pro) connect via DTLS,
6322
+ # A000088 cameras connect via DTLS,
6316
6323
  # SCTP comes up, DC opens at t+1-2s - but camera tears down at
6317
6324
  # t+22s if LIVING is not sent. This is the PreCon watchdog:
6318
6325
  # without LIVING the camera gives up waiting for a viewer.
@@ -7162,10 +7169,10 @@ class DeviceClient(object):
7162
7169
  # the camera silently ignores the original request and we time out.
7163
7170
  _init_done: set = set()
7164
7171
  _init_pending: set = {answer_fut, camera_offer_fut, webrtc_req_echo_fut}
7165
- _init_deadline = asyncio.get_event_loop().time() + timeout
7172
+ _init_deadline = asyncio.get_running_loop().time() + timeout
7166
7173
  _init_reconnect_resends = 0
7167
- while asyncio.get_event_loop().time() < _init_deadline:
7168
- _init_remaining = _init_deadline - asyncio.get_event_loop().time()
7174
+ while asyncio.get_running_loop().time() < _init_deadline:
7175
+ _init_remaining = _init_deadline - asyncio.get_running_loop().time()
7169
7176
  _init_done, _init_pending = await asyncio.wait(
7170
7177
  _init_pending,
7171
7178
  timeout=min(1.0, max(0.01, _init_remaining)),
@@ -7201,12 +7208,12 @@ class DeviceClient(object):
7201
7208
  and camera_offer_fut not in _rr_done):
7202
7209
  _status("webrtcReq echo received - waiting for camera webrtcResp...")
7203
7210
  _rr_secondary_limit = 20.0
7204
- _rr_secondary_deadline = asyncio.get_event_loop().time() + _rr_secondary_limit
7211
+ _rr_secondary_deadline = asyncio.get_running_loop().time() + _rr_secondary_limit
7205
7212
  _rr_done2: set = set()
7206
7213
  _rr_pending2 = _rr_pending # {answer_fut, camera_offer_fut}
7207
7214
  _rr_reconnect_resends = 0
7208
- while asyncio.get_event_loop().time() < _rr_secondary_deadline:
7209
- _remaining = _rr_secondary_deadline - asyncio.get_event_loop().time()
7215
+ while asyncio.get_running_loop().time() < _rr_secondary_deadline:
7216
+ _remaining = _rr_secondary_deadline - asyncio.get_running_loop().time()
7210
7217
  _rr_done2, _rr_pending2 = await asyncio.wait(
7211
7218
  _rr_pending2,
7212
7219
  timeout=min(1.0, max(0.01, _remaining)),
@@ -7246,11 +7253,11 @@ class DeviceClient(object):
7246
7253
  outgoing_q.put_nowait(_di_p)
7247
7254
 
7248
7255
  _rr_ext_limit = 20.0
7249
- _rr_ext_deadline = asyncio.get_event_loop().time() + _rr_ext_limit
7256
+ _rr_ext_deadline = asyncio.get_running_loop().time() + _rr_ext_limit
7250
7257
  _rr_done3: set = set()
7251
7258
  _rr_reconnect_ext = 0
7252
- while asyncio.get_event_loop().time() < _rr_ext_deadline:
7253
- _ext_rem = _rr_ext_deadline - asyncio.get_event_loop().time()
7259
+ while asyncio.get_running_loop().time() < _rr_ext_deadline:
7260
+ _ext_rem = _rr_ext_deadline - asyncio.get_running_loop().time()
7254
7261
  _rr_done3, _rr_pending2 = await asyncio.wait(
7255
7262
  _rr_pending2,
7256
7263
  timeout=min(1.0, max(0.01, _ext_rem)),
@@ -93,7 +93,7 @@ class BroadcastProtocol:
93
93
  "service": "device",
94
94
  "method": "devDiscoveryReq",
95
95
  "seq": seq,
96
- "srcAddr": f"0.{self.user_id}]",
96
+ "srcAddr": f"0.{self.user_id}",
97
97
  "tst": current_timestamp_milliseconds,
98
98
  "payload": {
99
99
  "extends": {},
@@ -166,7 +166,7 @@ class Discover:
166
166
  self._discover_callback, user_id, broadcast_addr=broadcast_ip
167
167
  )
168
168
  try:
169
- await asyncio.get_event_loop().create_datagram_endpoint(
169
+ await asyncio.get_running_loop().create_datagram_endpoint(
170
170
  lambda p=protocol: p,
171
171
  local_addr=(bind_ip, 0),
172
172
  )
@@ -183,7 +183,7 @@ class Discover:
183
183
  # Last-resort fallback
184
184
  protocol = BroadcastProtocol(self._discover_callback, user_id)
185
185
  try:
186
- await asyncio.get_event_loop().create_datagram_endpoint(
186
+ await asyncio.get_running_loop().create_datagram_endpoint(
187
187
  lambda: protocol,
188
188
  local_addr=("0.0.0.0", 0),
189
189
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 0.5.0
3
+ Version: 0.5.2
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
@@ -10,7 +10,6 @@ src/aidot/device_client.py
10
10
  src/aidot/discover.py
11
11
  src/aidot/exceptions.py
12
12
  src/aidot/g711.py
13
- src/aidot/login_const.py
14
13
  src/python_aidot_cameras.egg-info/PKG-INFO
15
14
  src/python_aidot_cameras.egg-info/SOURCES.txt
16
15
  src/python_aidot_cameras.egg-info/dependency_links.txt
@@ -1,13 +0,0 @@
1
- """Constants for the aidot integration."""
2
-
3
- APP_ID = "1383974540041977857"
4
- BASE_URL = "https://prod-us-api.arnoo.com/v17"
5
-
6
- PUBLIC_KEY_PEM = b"""
7
- -----BEGIN PUBLIC KEY-----
8
- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCtQAnPCi8ksPnS1Du6z96PsKfN
9
- p2Gp/f/bHwlrAdplbX3p7/TnGpnbJGkLq8uRxf6cw+vOthTsZjkPCF7CatRvRnTj
10
- c9fcy7yE0oXa5TloYyXD6GkxgftBbN/movkJJGQCc7gFavuYoAdTRBOyQoXBtm0m
11
- kXMSjXOldI/290b9BQIDAQAB
12
- -----END PUBLIC KEY-----
13
- """