python-aidot 0.3.55__tar.gz → 0.3.56__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.
- {python_aidot-0.3.55/python_aidot.egg-info → python_aidot-0.3.56}/PKG-INFO +4 -2
- python_aidot-0.3.56/aidot/aes_utils.py +50 -0
- python_aidot-0.3.56/aidot/client.py +314 -0
- {python_aidot-0.3.55 → python_aidot-0.3.56}/aidot/const.py +0 -14
- python_aidot-0.3.56/aidot/device_client.py +419 -0
- python_aidot-0.3.56/aidot/discover.py +138 -0
- python_aidot-0.3.56/aidot/login_const.py +16 -0
- python_aidot-0.3.56/aidot/models/__init__.py +37 -0
- {python_aidot-0.3.55 → python_aidot-0.3.56}/aidot/models/device_client_model.py +86 -83
- python_aidot-0.3.56/aidot/models/device_model.py +83 -0
- {python_aidot-0.3.55 → python_aidot-0.3.56/python_aidot.egg-info}/PKG-INFO +4 -2
- {python_aidot-0.3.55 → python_aidot-0.3.56}/python_aidot.egg-info/SOURCES.txt +3 -8
- python_aidot-0.3.56/python_aidot.egg-info/requires.txt +4 -0
- {python_aidot-0.3.55 → python_aidot-0.3.56}/setup.cfg +1 -1
- {python_aidot-0.3.55 → python_aidot-0.3.56}/setup.py +25 -23
- python_aidot-0.3.56/tests/test_client.py +147 -0
- python_aidot-0.3.55/aidot/api/__init__.py +0 -0
- python_aidot-0.3.55/aidot/api/cloud_api.py +0 -151
- python_aidot-0.3.55/aidot/client.py +0 -209
- python_aidot-0.3.55/aidot/device_client.py +0 -478
- python_aidot-0.3.55/aidot/discover.py +0 -138
- python_aidot-0.3.55/aidot/models/__init__.py +0 -1
- python_aidot-0.3.55/aidot/models/auth_model.py +0 -170
- python_aidot-0.3.55/aidot/models/base_model.py +0 -22
- python_aidot-0.3.55/aidot/models/device_model.py +0 -170
- python_aidot-0.3.55/aidot/utils/__init__.py +0 -12
- python_aidot-0.3.55/aidot/utils/async_timer.py +0 -142
- python_aidot-0.3.55/aidot/utils/crypto.py +0 -91
- python_aidot-0.3.55/python_aidot.egg-info/requires.txt +0 -2
- python_aidot-0.3.55/tests/test_cloud_api.py +0 -143
- {python_aidot-0.3.55 → python_aidot-0.3.56}/LICENSE +0 -0
- {python_aidot-0.3.55 → python_aidot-0.3.56}/README.md +0 -0
- {python_aidot-0.3.55 → python_aidot-0.3.56}/aidot/__init__.py +0 -0
- {python_aidot-0.3.55 → python_aidot-0.3.56}/aidot/exceptions.py +0 -0
- {python_aidot-0.3.55 → python_aidot-0.3.56}/aidot/models/discover_model.py +0 -0
- {python_aidot-0.3.55 → python_aidot-0.3.56}/python_aidot.egg-info/dependency_links.txt +0 -0
- {python_aidot-0.3.55 → python_aidot-0.3.56}/python_aidot.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.56
|
|
4
4
|
Summary: aidot control wifi lights
|
|
5
5
|
Home-page: https://github.com/Aidot-Development-Team/python-aidot
|
|
6
6
|
Author: aidotdev2024
|
|
@@ -9,8 +9,10 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
9
9
|
Classifier: Operating System :: OS Independent
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
License-File: LICENSE
|
|
12
|
-
Requires-Dist: requests
|
|
13
12
|
Requires-Dist: aiohttp
|
|
13
|
+
Requires-Dist: cryptography
|
|
14
|
+
Requires-Dist: dacite
|
|
15
|
+
Requires-Dist: requests
|
|
14
16
|
Dynamic: author
|
|
15
17
|
Dynamic: classifier
|
|
16
18
|
Dynamic: description
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
2
|
+
from cryptography.hazmat.backends import default_backend
|
|
3
|
+
from cryptography.hazmat.primitives import padding
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def aes_encrypt(plaintext, key):
|
|
9
|
+
padder = padding.PKCS7(algorithms.AES.block_size).padder()
|
|
10
|
+
padded_data = padder.update(plaintext) + padder.finalize()
|
|
11
|
+
|
|
12
|
+
cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend())
|
|
13
|
+
encryptor = cipher.encryptor()
|
|
14
|
+
|
|
15
|
+
ciphertext = encryptor.update(padded_data) + encryptor.finalize()
|
|
16
|
+
|
|
17
|
+
return ciphertext
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def aes_decrypt(ciphertext, key):
|
|
21
|
+
cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend())
|
|
22
|
+
decryptor = cipher.decryptor()
|
|
23
|
+
|
|
24
|
+
decrypted_data = decryptor.update(ciphertext) + decryptor.finalize()
|
|
25
|
+
|
|
26
|
+
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
|
|
27
|
+
plaintext = unpadder.update(decrypted_data) + unpadder.finalize()
|
|
28
|
+
|
|
29
|
+
return plaintext.decode()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def aes_decrypt_to_json(
|
|
33
|
+
ciphertext: bytes, key: Optional[bytes] = None
|
|
34
|
+
) -> dict[str, Any]:
|
|
35
|
+
"""Decrypt AES encrypted data and parse to JSON.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
ciphertext: AES encrypted data
|
|
39
|
+
key: AES key (optional, if None, assumes data is already decrypted)
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Parsed JSON dict
|
|
43
|
+
"""
|
|
44
|
+
if key:
|
|
45
|
+
decrypted_data = aes_decrypt(ciphertext, key)
|
|
46
|
+
else:
|
|
47
|
+
decrypted_data = (
|
|
48
|
+
ciphertext.decode() if isinstance(ciphertext, bytes) else ciphertext
|
|
49
|
+
)
|
|
50
|
+
return json.loads(decrypted_data)
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""The aidot integration."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import base64
|
|
6
|
+
import aiohttp
|
|
7
|
+
from aiohttp import ClientSession
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
from cryptography.hazmat.backends import default_backend
|
|
10
|
+
from cryptography.hazmat.primitives import serialization
|
|
11
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
12
|
+
import uuid
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import hashlib
|
|
15
|
+
from .exceptions import AidotAuthFailed, AidotUserOrPassIncorrect
|
|
16
|
+
from .device_client import DeviceClient
|
|
17
|
+
from .discover import Discover
|
|
18
|
+
from .login_const import APP_ID, PUBLIC_KEY_PEM, API_URL_TEMPLATE, DEFAULT_REGION
|
|
19
|
+
from .const import (
|
|
20
|
+
CONF_ACCESS_TOKEN,
|
|
21
|
+
CONF_APP_ID,
|
|
22
|
+
CONF_CODE,
|
|
23
|
+
CONF_COUNTRY,
|
|
24
|
+
CONF_DEVICE_LIST,
|
|
25
|
+
CONF_ID,
|
|
26
|
+
CONF_IPADDRESS,
|
|
27
|
+
CONF_PASSWORD,
|
|
28
|
+
CONF_PRODUCT,
|
|
29
|
+
CONF_PRODUCT_ID,
|
|
30
|
+
CONF_REFRESH_TOKEN,
|
|
31
|
+
CONF_REGION,
|
|
32
|
+
CONF_TERMINAL,
|
|
33
|
+
CONF_TOKEN,
|
|
34
|
+
CONF_USERNAME,
|
|
35
|
+
DEFAULT_COUNTRY_NAME,
|
|
36
|
+
SUPPORTED_COUNTRYS,
|
|
37
|
+
DEFAULT_COUNTRY_CODE,
|
|
38
|
+
CONF_IS_OWNER,
|
|
39
|
+
CONF_LOGIN_INFO,
|
|
40
|
+
ServerErrorCode,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
_LOGGER = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def rsa_password_encrypt(message: str) -> str:
|
|
47
|
+
"""Get password rsa encrypt."""
|
|
48
|
+
public_key = serialization.load_pem_public_key(
|
|
49
|
+
PUBLIC_KEY_PEM, backend=default_backend()
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
encrypted = public_key.encrypt(
|
|
53
|
+
message.encode("utf-8"),
|
|
54
|
+
padding.PKCS1v15(),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
encrypted_base64 = base64.b64encode(encrypted).decode("utf-8")
|
|
58
|
+
return encrypted_base64
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AidotClient:
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
session: Optional[ClientSession],
|
|
65
|
+
country_code: str | None = None,
|
|
66
|
+
username: str | None = None,
|
|
67
|
+
password: str | None = None,
|
|
68
|
+
token: dict | None = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
_LOGGER.info("Client Version: v0.3.56")
|
|
71
|
+
self.session = session
|
|
72
|
+
self.username = username
|
|
73
|
+
self.password = password
|
|
74
|
+
self.country_code = country_code or DEFAULT_COUNTRY_CODE
|
|
75
|
+
self.country_name = DEFAULT_COUNTRY_NAME
|
|
76
|
+
self._region = DEFAULT_REGION
|
|
77
|
+
self._base_url = API_URL_TEMPLATE.format(region=self._region)
|
|
78
|
+
self.login_info: dict[str, Any] = {}
|
|
79
|
+
self._device_clients = {}
|
|
80
|
+
self._discover: Discover | None = None
|
|
81
|
+
self._token_fresh_cb = None
|
|
82
|
+
for item in SUPPORTED_COUNTRYS:
|
|
83
|
+
if item["id"] == self.country_code:
|
|
84
|
+
self.country_name = item["name"]
|
|
85
|
+
self._region = item["region"].lower()
|
|
86
|
+
self._base_url = API_URL_TEMPLATE.format(region=self._region)
|
|
87
|
+
break
|
|
88
|
+
if token is not None:
|
|
89
|
+
# ✅ 兼容性处理: v1.0.8 数据结构迁移到 v1.1.3
|
|
90
|
+
# 旧版本: config_entry.data[CONF_LOGIN_INFO]
|
|
91
|
+
# 新版本: config_entry.data
|
|
92
|
+
if token.get(CONF_ID) is None and token.get(CONF_LOGIN_INFO) is not None:
|
|
93
|
+
token = token.get(CONF_LOGIN_INFO)
|
|
94
|
+
|
|
95
|
+
self.login_info = token.copy()
|
|
96
|
+
self.username = token[CONF_USERNAME]
|
|
97
|
+
self.password = token[CONF_PASSWORD]
|
|
98
|
+
self._region = token[CONF_REGION]
|
|
99
|
+
self.country_name = token[CONF_COUNTRY]
|
|
100
|
+
self._base_url = API_URL_TEMPLATE.format(region=self._region)
|
|
101
|
+
self.setup_discover()
|
|
102
|
+
|
|
103
|
+
def set_token_fresh_cb(self, callback) -> None:
|
|
104
|
+
self._token_fresh_cb = callback
|
|
105
|
+
|
|
106
|
+
def get_identifier(self) -> str:
|
|
107
|
+
return f"{self._region}-{self.username}"
|
|
108
|
+
|
|
109
|
+
def update_password(self, password: str) -> None:
|
|
110
|
+
self.password = password
|
|
111
|
+
|
|
112
|
+
async def get_terminal_id(self) -> str:
|
|
113
|
+
file_path = Path.home() / ".aidot_terminal_id"
|
|
114
|
+
|
|
115
|
+
def _read_or_create() -> str:
|
|
116
|
+
try:
|
|
117
|
+
if file_path.exists():
|
|
118
|
+
return file_path.read_text().strip()
|
|
119
|
+
node = uuid.getnode()
|
|
120
|
+
is_random = (node >> 40) & 1
|
|
121
|
+
raw_id = str(uuid.uuid4()) if is_random else format(node, "x")
|
|
122
|
+
file_path.write_text(raw_id)
|
|
123
|
+
return raw_id
|
|
124
|
+
except OSError:
|
|
125
|
+
return "gvz3gjae10l4zii00t7y0"
|
|
126
|
+
|
|
127
|
+
raw_id = await asyncio.to_thread(_read_or_create)
|
|
128
|
+
return hashlib.md5(raw_id.encode()).hexdigest()
|
|
129
|
+
|
|
130
|
+
async def async_post_login(self) -> dict[str, Any]:
|
|
131
|
+
"""Login the user input allows us to connect."""
|
|
132
|
+
url = f"{self._base_url}/users/loginWithFreeVerification"
|
|
133
|
+
headers = {CONF_APP_ID: APP_ID, CONF_TERMINAL: "app"}
|
|
134
|
+
# f"{region}:{self.country_name.strip()}",
|
|
135
|
+
terminalId = await self.get_terminal_id()
|
|
136
|
+
if terminalId is None:
|
|
137
|
+
terminalId = "gvz3gjae10l4zii00t7y0"
|
|
138
|
+
data = {
|
|
139
|
+
"countryKey": f"region:{self.country_name.strip()}",
|
|
140
|
+
"username": self.username,
|
|
141
|
+
"password": rsa_password_encrypt(self.password),
|
|
142
|
+
"terminalId": terminalId,
|
|
143
|
+
"webVersion": "0.5.0",
|
|
144
|
+
"area": "Asia/Shanghai",
|
|
145
|
+
"UTC": "UTC+8",
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
response_data: dict[str, Any] = {}
|
|
149
|
+
try:
|
|
150
|
+
response = await self.session.post(url, headers=headers, json=data)
|
|
151
|
+
response_data = await response.json()
|
|
152
|
+
response.raise_for_status()
|
|
153
|
+
self.login_info = response_data
|
|
154
|
+
self.login_info[CONF_PASSWORD] = self.password
|
|
155
|
+
self.login_info[CONF_REGION] = self._region
|
|
156
|
+
self.login_info[CONF_COUNTRY] = self.country_name
|
|
157
|
+
self.setup_discover()
|
|
158
|
+
return self.login_info
|
|
159
|
+
except aiohttp.ClientError as err:
|
|
160
|
+
_LOGGER.error("async_post_login ClientError: %s", err)
|
|
161
|
+
if response_data.get(CONF_CODE) == ServerErrorCode.USER_PWD_INCORRECT:
|
|
162
|
+
raise AidotUserOrPassIncorrect from err
|
|
163
|
+
raise
|
|
164
|
+
|
|
165
|
+
async def async_refresh_token(self) -> dict[str, Any]:
|
|
166
|
+
url = f"{self._base_url}/users/refreshToken"
|
|
167
|
+
headers = {CONF_APP_ID: APP_ID, CONF_TERMINAL: "app"}
|
|
168
|
+
data = {
|
|
169
|
+
CONF_REFRESH_TOKEN: self.login_info[CONF_REFRESH_TOKEN],
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
response_data: dict[str, Any] = {}
|
|
173
|
+
try:
|
|
174
|
+
response = await self.session.post(url, headers=headers, json=data)
|
|
175
|
+
response_data = await response.json()
|
|
176
|
+
response.raise_for_status()
|
|
177
|
+
self.login_info[CONF_ACCESS_TOKEN] = response_data[CONF_ACCESS_TOKEN]
|
|
178
|
+
if response_data[CONF_REFRESH_TOKEN] is not None:
|
|
179
|
+
self.login_info[CONF_REFRESH_TOKEN] = response_data[CONF_REFRESH_TOKEN]
|
|
180
|
+
_LOGGER.debug(f"refresh token: {response_data}")
|
|
181
|
+
if self._token_fresh_cb:
|
|
182
|
+
self._token_fresh_cb()
|
|
183
|
+
return response_data
|
|
184
|
+
except aiohttp.ClientError as err:
|
|
185
|
+
_LOGGER.error("async_refresh_token ClientError: %s %s", err, response_data)
|
|
186
|
+
if response_data.get(CONF_CODE) == ServerErrorCode.LOGIN_INVALID:
|
|
187
|
+
raise AidotAuthFailed from err
|
|
188
|
+
raise
|
|
189
|
+
|
|
190
|
+
async def async_session_get(
|
|
191
|
+
self, params: str, headers: str | None = None
|
|
192
|
+
) -> dict[str, Any]:
|
|
193
|
+
url = f"{self._base_url}{params}"
|
|
194
|
+
token = self.login_info[CONF_ACCESS_TOKEN]
|
|
195
|
+
if token is None:
|
|
196
|
+
raise AidotAuthFailed()
|
|
197
|
+
if headers is None:
|
|
198
|
+
headers = {
|
|
199
|
+
CONF_TERMINAL: "app",
|
|
200
|
+
CONF_TOKEN: token,
|
|
201
|
+
CONF_APP_ID: APP_ID,
|
|
202
|
+
}
|
|
203
|
+
response_data = {}
|
|
204
|
+
try:
|
|
205
|
+
response = await self.session.get(url, headers=headers)
|
|
206
|
+
response_data = await response.json()
|
|
207
|
+
response.raise_for_status()
|
|
208
|
+
return response_data
|
|
209
|
+
except aiohttp.ClientError as err:
|
|
210
|
+
_LOGGER.error("async_get ClientError: %s %s", err, response_data)
|
|
211
|
+
code = response_data.get(CONF_CODE)
|
|
212
|
+
if code == ServerErrorCode.TOKEN_EXPIRED:
|
|
213
|
+
try:
|
|
214
|
+
await self.async_refresh_token()
|
|
215
|
+
return await self.async_session_get(params)
|
|
216
|
+
except AidotAuthFailed as auth_err:
|
|
217
|
+
raise AidotAuthFailed from auth_err
|
|
218
|
+
elif (
|
|
219
|
+
code == ServerErrorCode.LOGIN_INVALID or code == 21027 or code == 21041
|
|
220
|
+
):
|
|
221
|
+
self.login_info[CONF_ACCESS_TOKEN] = None
|
|
222
|
+
raise AidotAuthFailed from err
|
|
223
|
+
raise
|
|
224
|
+
|
|
225
|
+
async def async_get_products(self, product_ids: str) -> list[dict[str, Any]]:
|
|
226
|
+
"""Get device list."""
|
|
227
|
+
params = f"/products/{product_ids}"
|
|
228
|
+
return await self.async_session_get(params)
|
|
229
|
+
|
|
230
|
+
async def async_get_devices(self, house_id: str) -> list[dict[str, Any]]:
|
|
231
|
+
"""Get device list."""
|
|
232
|
+
params = f"/devices?houseId={house_id}"
|
|
233
|
+
return await self.async_session_get(params)
|
|
234
|
+
|
|
235
|
+
async def async_get_houses(self) -> list[dict[str, Any]]:
|
|
236
|
+
"""Get house list."""
|
|
237
|
+
params = "/houses"
|
|
238
|
+
return await self.async_session_get(params)
|
|
239
|
+
|
|
240
|
+
async def async_get_all_device(self) -> dict[str, Any]:
|
|
241
|
+
final_device_list: list[dict[str, Any]] = []
|
|
242
|
+
try:
|
|
243
|
+
houses = await self.async_get_houses()
|
|
244
|
+
for house in houses:
|
|
245
|
+
if house.get(CONF_IS_OWNER) is False:
|
|
246
|
+
continue
|
|
247
|
+
# get device_list
|
|
248
|
+
device_list = await self.async_get_devices(house[CONF_ID])
|
|
249
|
+
if device_list:
|
|
250
|
+
final_device_list.extend(device_list)
|
|
251
|
+
|
|
252
|
+
# get product_list
|
|
253
|
+
if not final_device_list:
|
|
254
|
+
return {CONF_DEVICE_LIST: []}
|
|
255
|
+
productIds = ",".join([item[CONF_PRODUCT_ID] for item in final_device_list])
|
|
256
|
+
product_list = await self.async_get_products(productIds)
|
|
257
|
+
|
|
258
|
+
for product in product_list:
|
|
259
|
+
for device in final_device_list:
|
|
260
|
+
if device[CONF_PRODUCT_ID] == product[CONF_ID]:
|
|
261
|
+
device[CONF_PRODUCT] = product
|
|
262
|
+
|
|
263
|
+
except Exception as e:
|
|
264
|
+
raise e
|
|
265
|
+
return {CONF_DEVICE_LIST: final_device_list}
|
|
266
|
+
|
|
267
|
+
def get_device_client(self, device: dict[str, Any]) -> DeviceClient:
|
|
268
|
+
device_id = device.get(CONF_ID)
|
|
269
|
+
device_client: DeviceClient = self._device_clients.get(device_id)
|
|
270
|
+
if device_client is None:
|
|
271
|
+
device_client = DeviceClient(device, self.login_info)
|
|
272
|
+
self._device_clients[device_id] = device_client
|
|
273
|
+
if self._discover is not None:
|
|
274
|
+
ip = self._discover.discovered_device.get(device_id)
|
|
275
|
+
device_client.update_ip_address(ip)
|
|
276
|
+
return device_client
|
|
277
|
+
|
|
278
|
+
async def remove_device_client(self, dev_id: str) -> None:
|
|
279
|
+
device_client: DeviceClient = self._device_clients.get(dev_id)
|
|
280
|
+
if device_client is not None:
|
|
281
|
+
await device_client.close()
|
|
282
|
+
del self._device_clients[dev_id]
|
|
283
|
+
|
|
284
|
+
def setup_discover(self) -> None:
|
|
285
|
+
"""初始化完成后调用,启动设备发现"""
|
|
286
|
+
if self.login_info.get(CONF_ID) is None:
|
|
287
|
+
return
|
|
288
|
+
if self._discover is not None:
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
_LOGGER.warning("setup_discover")
|
|
292
|
+
|
|
293
|
+
def _discover_callback(dev_id, event: dict[str, str]) -> None:
|
|
294
|
+
device_ip = event[CONF_IPADDRESS]
|
|
295
|
+
device_client: DeviceClient = self._device_clients.get(dev_id)
|
|
296
|
+
if device_client is not None:
|
|
297
|
+
device_client.update_ip_address(device_ip)
|
|
298
|
+
|
|
299
|
+
self._discover = Discover(self.login_info, _discover_callback)
|
|
300
|
+
self._discover.start_repeat_broadcast()
|
|
301
|
+
|
|
302
|
+
async def async_close(self) -> None:
|
|
303
|
+
"""关闭客户端,清理资源"""
|
|
304
|
+
if self._discover is not None:
|
|
305
|
+
self._discover.close()
|
|
306
|
+
self._discover = None
|
|
307
|
+
for client in self._device_clients.values():
|
|
308
|
+
await client.close()
|
|
309
|
+
self._device_clients.clear()
|
|
310
|
+
|
|
311
|
+
async def async_cleanup(self) -> None:
|
|
312
|
+
"""清理所有资源"""
|
|
313
|
+
_LOGGER.debug("async_cleanup")
|
|
314
|
+
await self.async_close()
|
|
@@ -230,17 +230,3 @@ class ServerErrorCode(IntEnum):
|
|
|
230
230
|
TOKEN_EXPIRED = 21026
|
|
231
231
|
LOGIN_INVALID = 21025
|
|
232
232
|
USER_PWD_INCORRECT = 560080
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
# Login / API constants
|
|
236
|
-
APP_ID = "1383974540041977857"
|
|
237
|
-
API_URL_TEMPLATE = "https://prod-{region}-api.arnoo.com/v17"
|
|
238
|
-
DEFAULT_REGION = "us"
|
|
239
|
-
PUBLIC_KEY_PEM = b"""
|
|
240
|
-
-----BEGIN PUBLIC KEY-----
|
|
241
|
-
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCtQAnPCi8ksPnS1Du6z96PsKfN
|
|
242
|
-
p2Gp/f/bHwlrAdplbX3p7/TnGpnbJGkLq8uRxf6cw+vOthTsZjkPCF7CatRvRnTj
|
|
243
|
-
c9fcy7yE0oXa5TloYyXD6GkxgftBbN/movkJJGQCc7gFavuYoAdTRBOyQoXBtm0m
|
|
244
|
-
kXMSjXOldI/290b9BQIDAQAB
|
|
245
|
-
-----END PUBLIC KEY-----
|
|
246
|
-
"""
|