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.
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/PKG-INFO +1 -1
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/pyimouapi/__init__.py +1 -1
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/pyimouapi/const.py +1 -0
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/pyimouapi/device.py +41 -0
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/pyimouapi/openapi.py +28 -1
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/pyimouapi.egg-info/PKG-INFO +1 -1
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/pyimouapi.egg-info/SOURCES.txt +2 -0
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/pyproject.toml +1 -1
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/setup.py +1 -1
- pyimouapi-1.2.9/tests/test_device_summaries.py +89 -0
- pyimouapi-1.2.9/tests/test_message_callback.py +50 -0
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/LICENSE +0 -0
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/README.md +0 -0
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/pyimouapi/exceptions.py +0 -0
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/pyimouapi/ha_device.py +0 -0
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/pyimouapi.egg-info/dependency_links.txt +0 -0
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/pyimouapi.egg-info/requires.txt +0 -0
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/pyimouapi.egg-info/top_level.txt +0 -0
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/setup.cfg +0 -0
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/tests/test_collect_property_entities.py +0 -0
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/tests/test_lookup_property.py +0 -0
- {pyimouapi-1.2.8 → pyimouapi-1.2.9}/tests/test_update_from_detail.py +0 -0
|
@@ -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
|
|
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
|
|
@@ -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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|