pyimouapi 1.2.4__tar.gz → 1.2.6__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyimouapi
3
- Version: 1.2.4
3
+ Version: 1.2.6
4
4
  Summary: A package for imou open api
5
5
  Home-page: https://github.com/Imou-OpenPlatform/Py-Imou-Open-Api
6
6
  Author: Imou-OpenPlatform
@@ -1,3 +1,5 @@
1
+ __version__ = "1.2.5"
2
+
1
3
  from .device import ImouDeviceManager, ImouDevice, ImouChannel
2
4
  from .exceptions import (
3
5
  ConnectFailedException,
@@ -413,7 +413,15 @@ SENSOR_TYPE_REF = {
413
413
  "expression": "('e1' if data['14603']==0 else 'e2') if data['14603'] != 1 else int(data['14602'] / data['14601'] * 100)",
414
414
  }
415
415
  ],
416
- "battery": [{"ref": "11600", "default": "15", "ref_type": "properties"}],
416
+ "battery": [
417
+ {"ref": "11600", "default": "15", "ref_type": "properties"},
418
+ {
419
+ "ref": "106200",
420
+ "default": "0",
421
+ "ref_type": "properties",
422
+ "expression": "battery_106200(data)",
423
+ },
424
+ ],
417
425
  "temperature_current": [
418
426
  {"ref": "16000", "default": "10", "ref_type": "properties"}
419
427
  ],
@@ -1,3 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
1
5
  from .const import (
2
6
  API_ENDPOINT_LIST_DEVICE_DETAILS,
3
7
  PARAM_PAGE_SIZE,
@@ -137,7 +141,7 @@ class ImouDevice:
137
141
  return self._device_status
138
142
 
139
143
  @property
140
- def channels(self) -> []:
144
+ def channels(self) -> list[ImouChannel]:
141
145
  return self._channels
142
146
 
143
147
  @property
@@ -157,15 +161,15 @@ class ImouDevice:
157
161
  return self._device_version
158
162
 
159
163
  @property
160
- def product_id(self) -> str:
164
+ def product_id(self) -> str | None:
161
165
  return self._product_id
162
166
 
163
167
  @property
164
- def parent_product_id(self) -> str:
168
+ def parent_product_id(self) -> str | None:
165
169
  return self._parent_product_id
166
170
 
167
171
  @property
168
- def parent_device_id(self) -> str:
172
+ def parent_device_id(self) -> str | None:
169
173
  return self._parent_device_id
170
174
 
171
175
  @property
@@ -185,7 +189,7 @@ class ImouDevice:
185
189
  def set_product_id(self, product_id: str) -> None:
186
190
  self._product_id = product_id
187
191
 
188
- def set_channels(self, channels: []) -> None:
192
+ def set_channels(self, channels: list[ImouChannel]) -> None:
189
193
  self._channels = channels
190
194
 
191
195
  def set_channel_number(self, channel_number: int):
@@ -311,7 +315,7 @@ class ImouDeviceManager:
311
315
 
312
316
  async def async_get_device_status(
313
317
  self, device_id: str, channel_id: str, enable_type: str
314
- ) -> dict[any, any]:
318
+ ) -> dict[str, Any]:
315
319
  """obtain device capability switch status"""
316
320
  params = {
317
321
  PARAM_DEVICE_ID: device_id,
@@ -322,7 +326,7 @@ class ImouDeviceManager:
322
326
  API_ENDPOINT_GET_DEVICE_STATUS, params
323
327
  )
324
328
 
325
- async def async_get_device_online_status(self, device_id: str) -> dict[any, any]:
329
+ async def async_get_device_online_status(self, device_id: str) -> dict[str, Any]:
326
330
  """GET DEVICE ONLINE STATUS"""
327
331
  params = {
328
332
  PARAM_DEVICE_ID: device_id,
@@ -346,7 +350,7 @@ class ImouDeviceManager:
346
350
 
347
351
  async def async_get_device_night_vision_mode(
348
352
  self, device_id: str, channel_id: str
349
- ) -> dict[any, any]:
353
+ ) -> dict[str, Any]:
350
354
  """obtain device night vision mode"""
351
355
  params = {
352
356
  PARAM_DEVICE_ID: device_id,
@@ -371,7 +375,7 @@ class ImouDeviceManager:
371
375
  API_ENDPOINT_SET_DEVICE_NIGHT_VISION_MODE, params
372
376
  )
373
377
 
374
- async def async_get_device_storage(self, device_id: str) -> dict[any, any]:
378
+ async def async_get_device_storage(self, device_id: str) -> dict[str, Any]:
375
379
  """obtain device storage media capacity information"""
376
380
  params = {PARAM_DEVICE_ID: device_id}
377
381
  return await self._imou_api_client.async_request_api(
@@ -387,7 +391,7 @@ class ImouDeviceManager:
387
391
 
388
392
  async def async_get_stream_url(
389
393
  self, device_id: str, channel_id: str
390
- ) -> dict[any, any]:
394
+ ) -> dict[str, Any]:
391
395
  """obtain the hls stream address of the device"""
392
396
  params = {PARAM_DEVICE_ID: device_id, PARAM_CHANNEL_ID: channel_id}
393
397
  return await self._imou_api_client.async_request_api(
@@ -396,7 +400,7 @@ class ImouDeviceManager:
396
400
 
397
401
  async def async_get_device_snap(
398
402
  self, device_id: str, channel_id: str
399
- ) -> dict[any, any]:
403
+ ) -> dict[str, Any]:
400
404
  params = {PARAM_DEVICE_ID: device_id, PARAM_CHANNEL_ID: channel_id}
401
405
  return await self._imou_api_client.async_request_api(
402
406
  API_ENDPOINT_SET_DEVICE_SNAP, params
@@ -404,7 +408,7 @@ class ImouDeviceManager:
404
408
 
405
409
  async def async_create_stream_url(
406
410
  self, device_id: str, channel_id: str, stream_id: int = 0
407
- ) -> dict[any, any]:
411
+ ) -> dict[str, Any]:
408
412
  """create device hls stream address"""
409
413
  params = {
410
414
  PARAM_DEVICE_ID: device_id,
@@ -425,8 +429,12 @@ class ImouDeviceManager:
425
429
  )
426
430
 
427
431
  async def async_get_iot_device_properties(
428
- self, device_id: str, channel_id: str | None, product_id: str, properties: []
429
- ) -> dict[any, any]:
432
+ self,
433
+ device_id: str,
434
+ channel_id: str | None,
435
+ product_id: str,
436
+ properties: list[Any],
437
+ ) -> dict[str, Any]:
430
438
  params = {
431
439
  PARAM_DEVICE_LIST: [
432
440
  {
@@ -462,15 +470,15 @@ class ImouDeviceManager:
462
470
  API_ENDPOINT_SET_IOT_DEVICE_PROPERTIES, params
463
471
  )
464
472
 
465
- async def async_get_device_sd_card_status(self, device_id: str) -> dict[any, any]:
473
+ async def async_get_device_sd_card_status(self, device_id: str) -> dict[str, Any]:
466
474
  params = {PARAM_DEVICE_ID: device_id}
467
475
  return await self._imou_api_client.async_request_api(
468
476
  API_ENDPOINT_DEVICE_SD_CARD_STATUS, params
469
477
  )
470
478
 
471
479
  async def async_iot_device_control(
472
- self, device_id: str, product_id: str, ref: str, content: dict
473
- ) -> dict[str, any]:
480
+ self, device_id: str, product_id: str, ref: str, content: dict[str, Any]
481
+ ) -> dict[str, Any]:
474
482
  params = {
475
483
  PARAM_DEVICE_ID: device_id,
476
484
  PARAM_PRODUCT_ID: product_id,
@@ -481,7 +489,7 @@ class ImouDeviceManager:
481
489
  API_ENDPOINT_IOT_DEVICE_CONTROL, params
482
490
  )
483
491
 
484
- async def async_get_device_power_info(self, device_id: str) -> dict[any, any]:
492
+ async def async_get_device_power_info(self, device_id: str) -> dict[str, Any]:
485
493
  params = {
486
494
  PARAM_DEVICE_ID: device_id,
487
495
  }
@@ -498,9 +506,7 @@ class ImouDeviceManager:
498
506
  API_ENDPOINT_WAKE_UP_DEVICE, params
499
507
  )
500
508
 
501
-
502
-
503
- async def async_get_product_model(self, product_id: str) -> dict[any, any]:
509
+ async def async_get_product_model(self, product_id: str) -> dict[str, Any]:
504
510
  params = {
505
511
  PARAM_PRODUCT_ID: product_id,
506
512
  }
@@ -510,7 +516,7 @@ class ImouDeviceManager:
510
516
 
511
517
  async def async_get_iot_device_detail_info(
512
518
  self, device_id: str, product_id: str
513
- ) -> dict[any, any]:
519
+ ) -> dict[str, Any]:
514
520
  params = {
515
521
  PARAM_DEVICE_ID: device_id,
516
522
  PARAM_PRODUCT_ID: product_id,
@@ -14,8 +14,10 @@ class ImouException(Exception):
14
14
 
15
15
  def traceback(self) -> str:
16
16
  """Return the traceback as a string."""
17
- etype, value, trace = sys.exc_info()
18
- return "".join(traceback.format_exception(etype, value, trace, None))
17
+ exc_info = sys.exc_info()
18
+ if exc_info[0] is None:
19
+ return ""
20
+ return "".join(traceback.format_exception(*exc_info))
19
21
 
20
22
  def get_title(self) -> str:
21
23
  """Return the title of the exception which will be then translated."""
@@ -69,6 +69,53 @@ from simpleeval import SimpleEval
69
69
 
70
70
  _LOGGER: logging.Logger = logging.getLogger(__package__)
71
71
 
72
+
73
+ def _battery_level_from_106200_list(data) -> int:
74
+ """解析 ref 106200 电量属性: [{"106202": 电池类型, "106203": 电量}, ...]。
75
+ 多条时取 106202==0 的 106203;仅一条时取该条的 106203。
76
+ 键均为字符串。
77
+ """
78
+ if not isinstance(data, list) or not data:
79
+ return 0
80
+
81
+ k_type, k_level = "106202", "106203"
82
+
83
+ def _to_int(v):
84
+ if v is None:
85
+ return None
86
+ if isinstance(v, bool):
87
+ return int(v)
88
+ if isinstance(v, int):
89
+ return v
90
+ if isinstance(v, float):
91
+ return int(v)
92
+ try:
93
+ return int(str(v).strip())
94
+ except ValueError:
95
+ return None
96
+
97
+ if len(data) == 1:
98
+ row = data[0]
99
+ if not isinstance(row, dict):
100
+ return 0
101
+ level = _to_int(row.get(k_level))
102
+ return level if level is not None else 0
103
+
104
+ for row in data:
105
+ if not isinstance(row, dict):
106
+ continue
107
+ if _to_int(row.get(k_type)) == 0:
108
+ level = _to_int(row.get(k_level))
109
+ if level is not None:
110
+ return level
111
+
112
+ row0 = data[0]
113
+ if not isinstance(row0, dict):
114
+ return 0
115
+ level = _to_int(row0.get(k_level))
116
+ return level if level is not None else 0
117
+
118
+
72
119
  NUMBER_TYPE = [
73
120
  PARAM_STORAGE_USED,
74
121
  PARAM_TEMPERATURE_CURRENT,
@@ -323,10 +370,8 @@ class ImouHaDeviceManager(object):
323
370
  return await self._async_get_device_exist_stream(
324
371
  device, live_resolution, live_protocol
325
372
  )
326
- else:
327
- raise exception
328
- else:
329
- raise exception
373
+ raise ex
374
+ raise exception
330
375
 
331
376
  async def _async_get_device_exist_stream(
332
377
  self, device: ImouHaDevice, resolution: str, protocol: str
@@ -352,8 +397,9 @@ class ImouHaDeviceManager(object):
352
397
  _LOGGER.debug(f"wait {wait_seconds} seconds to download a picture")
353
398
  await asyncio.sleep(wait_seconds)
354
399
  try:
355
- async with aiohttp.ClientSession() as session:
356
- response = await session.request("GET", data[PARAM_URL])
400
+ timeout = aiohttp.ClientTimeout(total=120)
401
+ async with aiohttp.ClientSession(timeout=timeout) as session:
402
+ response = await session.get(data[PARAM_URL])
357
403
  if response.status != 200:
358
404
  raise RequestFailedException(
359
405
  f"request failed,status code {response.status}"
@@ -413,9 +459,15 @@ class ImouHaDeviceManager(object):
413
459
  return devices
414
460
 
415
461
  @staticmethod
416
- def get_expression_value(expression: str, data: dict):
462
+ def get_expression_value(expression: str, data):
417
463
  s = SimpleEval(
418
- names={"data": data}, functions={"round": round, "int": int, "str": str}
464
+ names={"data": data},
465
+ functions={
466
+ "round": round,
467
+ "int": int,
468
+ "str": str,
469
+ "battery_106200": _battery_level_from_106200_list,
470
+ },
419
471
  )
420
472
  return s.eval(expression)
421
473
 
@@ -1041,7 +1093,7 @@ class ImouHaDeviceManager(object):
1041
1093
  device_id, device.channel_id, device.product_id, [value[PARAM_REF]]
1042
1094
  )
1043
1095
  data = result[PARAM_PROPERTIES][value[PARAM_REF]]
1044
- if value.get(PARAM_EXPRESSION) and isinstance(data, dict):
1096
+ if value.get(PARAM_EXPRESSION) and isinstance(data, (dict, list)):
1045
1097
  state = self.get_expression_value(value[PARAM_EXPRESSION], data)
1046
1098
  else:
1047
1099
  state = data
@@ -5,8 +5,10 @@ import logging
5
5
  import secrets
6
6
  import time
7
7
  import uuid
8
+ from typing import Any
8
9
 
9
10
  import aiohttp
11
+ from urllib.parse import urlparse
10
12
 
11
13
  from .const import (
12
14
  API_ENDPOINT_ACCESS_TOKEN,
@@ -40,28 +42,49 @@ _LOGGER: logging.Logger = logging.getLogger(__package__)
40
42
 
41
43
 
42
44
  class ImouOpenApiClient:
45
+ """Async client for Imou Open Platform HTTP API."""
46
+
43
47
  def __init__(self, app_id: str, app_secret: str, api_url: str) -> None:
44
48
  self._app_id = app_id
45
49
  self._app_secret = app_secret
46
50
  self._api_url = api_url
47
- # token
48
- self._access_token = None
51
+ self._access_token: str | None = None
52
+ self._session: aiohttp.ClientSession | None = None
53
+
54
+ async def _async_get_session(self) -> aiohttp.ClientSession:
55
+ if self._session is None or self._session.closed:
56
+ self._session = aiohttp.ClientSession(
57
+ headers={"Client-Type": "HomeAssistant"},
58
+ )
59
+ return self._session
60
+
61
+ async def async_close(self) -> None:
62
+ """Close the HTTP session (call when done with the client)."""
63
+ if self._session is not None and not self._session.closed:
64
+ await self._session.close()
65
+ self._session = None
49
66
 
50
67
  async def async_get_token(self) -> None:
51
- """get accessToken"""
68
+ """Fetch and store accessToken."""
52
69
  response = await self.async_request_api(API_ENDPOINT_ACCESS_TOKEN, {})
53
70
  self._access_token = response[PARAM_ACCESS_TOKEN]
54
71
  if PARAM_CURRENT_DOMAIN in response:
55
- self._api_url = response[PARAM_CURRENT_DOMAIN].split("://")[1]
72
+ raw = response[PARAM_CURRENT_DOMAIN]
73
+ if "://" not in raw:
74
+ raw = f"https://{raw}"
75
+ parsed = urlparse(raw)
76
+ if parsed.netloc:
77
+ self._api_url = parsed.netloc
56
78
 
57
79
  async def async_request_api(
58
- self, endpoint: str, params: dict[any, any] = None
59
- ) -> dict[any, any]:
60
- # if accessToken is None , get first
80
+ self, endpoint: str, params: dict[str, Any] | None = None
81
+ ) -> dict[str, Any]:
82
+ """POST to an API endpoint; returns the result data object."""
83
+ payload = dict(params) if params else {}
61
84
  if self._access_token is None and endpoint != API_ENDPOINT_ACCESS_TOKEN:
62
85
  await self.async_get_token()
63
86
  if endpoint != API_ENDPOINT_ACCESS_TOKEN:
64
- params[PARAM_TOKEN] = self._access_token
87
+ payload[PARAM_TOKEN] = self._access_token
65
88
  timestamp = round(time.time())
66
89
  nonce = secrets.token_urlsafe()
67
90
  sign = hashlib.md5(
@@ -70,7 +93,7 @@ class ImouOpenApiClient:
70
93
  )
71
94
  ).hexdigest()
72
95
  request_id = str(uuid.uuid4())
73
- headers = {"Content-Type": "application/json", "Client-Type": "HomeAssistant"}
96
+ headers = {"Content-Type": "application/json"}
74
97
  body = {
75
98
  PARAM_SYSTEM: {
76
99
  PARAM_VER: "1.0",
@@ -79,20 +102,16 @@ class ImouOpenApiClient:
79
102
  PARAM_TIME: timestamp,
80
103
  PARAM_NONCE: nonce,
81
104
  },
82
- PARAM_PARAMS: params,
105
+ PARAM_PARAMS: payload,
83
106
  PARAM_ID: request_id,
84
107
  }
85
108
  url = f"https://{self._api_url}{endpoint}"
109
+ session = await self._async_get_session()
86
110
  try:
87
- async with aiohttp.ClientSession() as session:
88
- async with asyncio.timeout(30):
89
- response = await session.request(
90
- "POST", url, json=body, headers=headers
91
- )
92
- response_body = json.loads(await response.text())
93
- _LOGGER.debug(
94
- f"url: {url} request body: {body} response: {response_body}"
95
- )
111
+ async with asyncio.timeout(30):
112
+ response = await session.request("POST", url, json=body, headers=headers)
113
+ response_body = json.loads(await response.text())
114
+ _LOGGER.debug("url: %s request body: %s response: %s", url, body, response_body)
96
115
  except Exception as exception:
97
116
  raise ConnectFailedException(f"connect failed,{exception}") from exception
98
117
  if response.status != 200:
@@ -117,5 +136,5 @@ class ImouOpenApiClient:
117
136
  return response_data
118
137
 
119
138
  @property
120
- def access_token(self):
139
+ def access_token(self) -> str | None:
121
140
  return self._access_token
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyimouapi
3
- Version: 1.2.4
3
+ Version: 1.2.6
4
4
  Summary: A package for imou open api
5
5
  Home-page: https://github.com/Imou-OpenPlatform/Py-Imou-Open-Api
6
6
  Author: Imou-OpenPlatform
@@ -1,5 +1,6 @@
1
1
  LICENSE
2
2
  README.md
3
+ pyproject.toml
3
4
  setup.py
4
5
  pyimouapi/__init__.py
5
6
  pyimouapi/const.py
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="pyimouapi",
5
- version="1.2.4",
5
+ version="1.2.6",
6
6
  packages=find_packages(),
7
7
  python_requires=">=3.11",
8
8
  install_requires=[
File without changes
File without changes
File without changes