pyimouapi 1.2.8__tar.gz → 1.2.9__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.8
3
+ Version: 1.2.9
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,4 +1,4 @@
1
- __version__ = "1.2.8"
1
+ __version__ = "1.2.9"
2
2
 
3
3
  from .device import ImouChannel, ImouDevice, ImouDeviceManager
4
4
  from .exceptions import (
@@ -22,6 +22,7 @@ API_ENDPOINT_GET_DEVICE_POWER_INFO = "/openapi/getDevicePowerInfo"
22
22
  API_ENDPOINT_GET_PRODUCT_MODEL = "/openapi/getProductModel"
23
23
  API_ENDPOINT_GET_IOT_DEVICE_DETAIL_INFO = "/openapi/getIotDeviceDetailInfo"
24
24
  API_ENDPOINT_WAKE_UP_DEVICE = "/openapi/wakeUpDevice"
25
+ API_ENDPOINT_SET_MESSAGE_CALLBACK = "/openapi/setMessageCallback"
25
26
 
26
27
  # error_codes
27
28
  ERROR_CODE_SUCCESS = "0"
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from dataclasses import dataclass
3
4
  from typing import Any
4
5
 
5
6
  from .const import (
@@ -64,6 +65,14 @@ from .const import (
64
65
  from .openapi import ImouOpenApiClient
65
66
 
66
67
 
68
+ @dataclass(frozen=True)
69
+ class ImouDeviceSummary:
70
+ device_id: str
71
+ name: str
72
+ model: str
73
+ status: str # "0" offline, "1" online
74
+
75
+
67
76
  class ImouChannel:
68
77
  def __init__(
69
78
  self,
@@ -286,6 +295,38 @@ class ImouDeviceManager:
286
295
  devices.extend(await self.async_get_devices(page + 1, page_size))
287
296
  return devices
288
297
 
298
+ async def async_get_device_summaries(
299
+ self, page: int = 1, page_size: int = 50
300
+ ) -> list[ImouDeviceSummary]:
301
+ params = {
302
+ PARAM_PAGE: page,
303
+ PARAM_PAGE_SIZE: page_size,
304
+ }
305
+ data = await self._imou_api_client.async_request_api(
306
+ API_ENDPOINT_LIST_DEVICE_DETAILS, params
307
+ )
308
+ if data[PARAM_COUNT] == 0:
309
+ return []
310
+ summaries: list[ImouDeviceSummary] = []
311
+ for device in data[PARAM_DEVICE_LIST]:
312
+ device_id = device.get(PARAM_DEVICE_ID)
313
+ if not device_id:
314
+ continue
315
+ name = device.get(PARAM_DEVICE_NAME) or device_id
316
+ model = device.get(PARAM_DEVICE_MODEL, "")
317
+ status = str(device.get(PARAM_DEVICE_STATUS, ""))
318
+ summaries.append(
319
+ ImouDeviceSummary(
320
+ device_id=device_id,
321
+ name=name,
322
+ model=model,
323
+ status=status,
324
+ )
325
+ )
326
+ if data[PARAM_COUNT] >= page_size:
327
+ summaries.extend(await self.async_get_device_summaries(page + 1, page_size))
328
+ return summaries
329
+
289
330
  async def async_control_device_ptz(
290
331
  self, device_id: str, channel_id: str, operation: int, duration: int
291
332
  ) -> None:
@@ -5,13 +5,15 @@ import logging
5
5
  import secrets
6
6
  import time
7
7
  import uuid
8
- from typing import Any
8
+ from collections.abc import Iterable
9
+ from typing import Any, Literal
9
10
  from urllib.parse import urlparse
10
11
 
11
12
  import aiohttp
12
13
 
13
14
  from .const import (
14
15
  API_ENDPOINT_ACCESS_TOKEN,
16
+ API_ENDPOINT_SET_MESSAGE_CALLBACK,
15
17
  ERROR_CODE_INVALID_APP,
16
18
  ERROR_CODE_INVALID_SIGN,
17
19
  ERROR_CODE_SUCCESS,
@@ -133,6 +135,31 @@ class ImouOpenApiClient:
133
135
  response_data = response_body[PARAM_RESULT].get(PARAM_DATA, {})
134
136
  return response_data
135
137
 
138
+ async def async_set_message_callback(
139
+ self,
140
+ *,
141
+ status: Literal["on", "off"],
142
+ callback_url: str | None = None,
143
+ callback_flag: str | Iterable[str] | None = None,
144
+ base_push: str = "2",
145
+ ) -> dict[str, Any]:
146
+ """Register or unregister Imou Open Platform message callback."""
147
+ params: dict[str, Any] = {
148
+ "status": status,
149
+ "basePush": base_push,
150
+ }
151
+ if status == "on":
152
+ if callback_url is None:
153
+ raise ValueError("callback_url is required when status is 'on'")
154
+ params["callbackUrl"] = callback_url
155
+ if callback_flag is None:
156
+ params["callbackFlag"] = "alarm,deviceStatus"
157
+ elif isinstance(callback_flag, str):
158
+ params["callbackFlag"] = callback_flag
159
+ else:
160
+ params["callbackFlag"] = ",".join(callback_flag)
161
+ return await self.async_request_api(API_ENDPOINT_SET_MESSAGE_CALLBACK, params)
162
+
136
163
  @property
137
164
  def access_token(self) -> str | None:
138
165
  return self._access_token
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyimouapi
3
- Version: 1.2.8
3
+ Version: 1.2.9
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
@@ -14,5 +14,7 @@ pyimouapi.egg-info/dependency_links.txt
14
14
  pyimouapi.egg-info/requires.txt
15
15
  pyimouapi.egg-info/top_level.txt
16
16
  tests/test_collect_property_entities.py
17
+ tests/test_device_summaries.py
17
18
  tests/test_lookup_property.py
19
+ tests/test_message_callback.py
18
20
  tests/test_update_from_detail.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyimouapi"
7
- version = "1.2.8"
7
+ version = "1.2.9"
8
8
  description = "A package for imou open api"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -4,7 +4,7 @@ from setuptools import find_packages, setup
4
4
 
5
5
  setup(
6
6
  name="pyimouapi",
7
- version="1.2.8",
7
+ version="1.2.9",
8
8
  packages=find_packages(),
9
9
  python_requires=">=3.11",
10
10
  install_requires=[
@@ -0,0 +1,89 @@
1
+ """Tests for ImouDeviceManager.async_get_device_summaries."""
2
+
3
+ from unittest.mock import AsyncMock, MagicMock
4
+
5
+ import pytest
6
+ from pyimouapi.const import (
7
+ API_ENDPOINT_LIST_DEVICE_DETAILS,
8
+ PARAM_COUNT,
9
+ PARAM_DEVICE_ID,
10
+ PARAM_DEVICE_LIST,
11
+ PARAM_DEVICE_MODEL,
12
+ PARAM_DEVICE_NAME,
13
+ PARAM_DEVICE_STATUS,
14
+ PARAM_PAGE,
15
+ PARAM_PAGE_SIZE,
16
+ )
17
+ from pyimouapi.device import ImouDeviceManager, ImouDeviceSummary
18
+
19
+
20
+ def _make_device(
21
+ device_id: str,
22
+ name: str | None = "Camera",
23
+ model: str = "IPC-A1",
24
+ status: str | int = "1",
25
+ ) -> dict:
26
+ entry: dict = {
27
+ PARAM_DEVICE_ID: device_id,
28
+ PARAM_DEVICE_STATUS: status,
29
+ }
30
+ if name is not None:
31
+ entry[PARAM_DEVICE_NAME] = name
32
+ if model:
33
+ entry[PARAM_DEVICE_MODEL] = model
34
+ return entry
35
+
36
+
37
+ @pytest.mark.asyncio
38
+ async def test_get_device_summaries_single_page():
39
+ client = MagicMock()
40
+ client.async_request_api = AsyncMock(
41
+ return_value={
42
+ PARAM_COUNT: 2,
43
+ PARAM_DEVICE_LIST: [
44
+ _make_device("dev1", "Front Door", "IPC-A1", "1"),
45
+ _make_device("dev2", None, "", "0"),
46
+ {},
47
+ ],
48
+ }
49
+ )
50
+ manager = ImouDeviceManager(client)
51
+
52
+ summaries = await manager.async_get_device_summaries()
53
+
54
+ client.async_request_api.assert_awaited_once_with(
55
+ API_ENDPOINT_LIST_DEVICE_DETAILS,
56
+ {PARAM_PAGE: 1, PARAM_PAGE_SIZE: 50},
57
+ )
58
+ assert summaries == [
59
+ ImouDeviceSummary(
60
+ device_id="dev1", name="Front Door", model="IPC-A1", status="1"
61
+ ),
62
+ ImouDeviceSummary(device_id="dev2", name="dev2", model="", status="0"),
63
+ ]
64
+
65
+
66
+ @pytest.mark.asyncio
67
+ async def test_get_device_summaries_pagination():
68
+ page1_devices = [
69
+ _make_device(f"dev{i}", f"Device {i}", "IPC", "1") for i in range(50)
70
+ ]
71
+ page2_devices = [_make_device("dev50", "Device 50", "IPC", "1")]
72
+
73
+ async def mock_request(endpoint, params):
74
+ if params[PARAM_PAGE] == 1:
75
+ return {PARAM_COUNT: 50, PARAM_DEVICE_LIST: page1_devices}
76
+ if params[PARAM_PAGE] == 2:
77
+ return {PARAM_COUNT: 1, PARAM_DEVICE_LIST: page2_devices}
78
+ raise AssertionError(f"Unexpected page: {params[PARAM_PAGE]}")
79
+
80
+ client = MagicMock()
81
+ client.async_request_api = AsyncMock(side_effect=mock_request)
82
+ manager = ImouDeviceManager(client)
83
+
84
+ summaries = await manager.async_get_device_summaries()
85
+
86
+ assert client.async_request_api.await_count == 2
87
+ assert len(summaries) == 51
88
+ assert summaries[0].device_id == "dev0"
89
+ assert summaries[50].device_id == "dev50"
@@ -0,0 +1,50 @@
1
+ """Tests for ImouOpenApiClient.async_set_message_callback."""
2
+
3
+ from unittest.mock import AsyncMock
4
+
5
+ import pytest
6
+ from pyimouapi.const import API_ENDPOINT_SET_MESSAGE_CALLBACK
7
+ from pyimouapi.openapi import ImouOpenApiClient
8
+
9
+
10
+ @pytest.fixture
11
+ def client() -> ImouOpenApiClient:
12
+ return ImouOpenApiClient("app_id", "app_secret", "api.example.com")
13
+
14
+
15
+ @pytest.mark.asyncio
16
+ async def test_set_message_callback_on(client: ImouOpenApiClient) -> None:
17
+ client.async_request_api = AsyncMock(return_value={})
18
+ await client.async_set_message_callback(
19
+ status="on",
20
+ callback_url="https://example.com/webhook",
21
+ callback_flag=["alarm", "deviceStatus"],
22
+ )
23
+ client.async_request_api.assert_awaited_once_with(
24
+ API_ENDPOINT_SET_MESSAGE_CALLBACK,
25
+ {
26
+ "status": "on",
27
+ "basePush": "2",
28
+ "callbackUrl": "https://example.com/webhook",
29
+ "callbackFlag": "alarm,deviceStatus",
30
+ },
31
+ )
32
+
33
+
34
+ @pytest.mark.asyncio
35
+ async def test_set_message_callback_off(client: ImouOpenApiClient) -> None:
36
+ client.async_request_api = AsyncMock(return_value={})
37
+ await client.async_set_message_callback(status="off")
38
+ client.async_request_api.assert_awaited_once_with(
39
+ API_ENDPOINT_SET_MESSAGE_CALLBACK,
40
+ {
41
+ "status": "off",
42
+ "basePush": "2",
43
+ },
44
+ )
45
+
46
+
47
+ @pytest.mark.asyncio
48
+ async def test_set_message_callback_on_requires_url(client: ImouOpenApiClient) -> None:
49
+ with pytest.raises(ValueError):
50
+ await client.async_set_message_callback(status="on")
File without changes
File without changes
File without changes