python-aidot 0.3.52__py3-none-any.whl → 0.3.54__py3-none-any.whl

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.
aidot/api/__init__.py ADDED
File without changes
aidot/api/cloud_api.py ADDED
@@ -0,0 +1,180 @@
1
+ """Cloud API for AiDot - static class for HTTP requests."""
2
+
3
+ import logging
4
+ from typing import Any, Optional
5
+ import aiohttp
6
+ from aiohttp import ClientSession
7
+
8
+ from ..const import (
9
+ API_URL_TEMPLATE,
10
+ CONF_ACCESS_TOKEN,
11
+ CONF_CODE,
12
+ CONF_REFRESH_TOKEN,
13
+ ServerErrorCode,
14
+ )
15
+ from ..exceptions import (
16
+ AidotAuthFailed,
17
+ AidotAuthTokenExpired,
18
+ AidotUserOrPassIncorrect,
19
+ )
20
+ from ..models.auth_model import RequestHeaders, UserInformation
21
+
22
+ _LOGGER = logging.getLogger(__name__)
23
+
24
+
25
+ class CloudApi:
26
+ """Cloud API - static class for HTTP requests."""
27
+
28
+ BASE_URL: str = ""
29
+ SESSION: Optional[ClientSession] = None
30
+ USER_INFO: Optional[UserInformation] = None
31
+ _auth_failed_callback: Optional[callable] = None
32
+ _token_refreshed_callback: Optional[callable] = None
33
+
34
+ def __init__(self) -> None:
35
+ raise TypeError("CloudApi is a static class and cannot be instantiated")
36
+
37
+ @classmethod
38
+ def set_session(cls, session: ClientSession) -> None:
39
+ """Set the HTTP session."""
40
+ cls.SESSION = session
41
+
42
+ @classmethod
43
+ def set_user_info(cls, user_info: "UserInformation") -> None:
44
+ """Set user info and update base URL."""
45
+ cls.USER_INFO = user_info
46
+ cls.BASE_URL = API_URL_TEMPLATE.format(region=user_info.region)
47
+
48
+ @classmethod
49
+ def set_auth_failed_callback(cls, callback: callable) -> None:
50
+ """Set auth failure callback."""
51
+ cls._auth_failed_callback = callback
52
+
53
+ @classmethod
54
+ def set_token_refreshed_callback(cls, callback: callable) -> None:
55
+ """Set token refresh callback."""
56
+ cls._token_refreshed_callback = callback
57
+
58
+ @classmethod
59
+ def _get_headers(cls) -> dict[str, str]:
60
+ """Build request headers with access token."""
61
+ access_token = cls.USER_INFO.accessToken if cls.USER_INFO else None
62
+ return RequestHeaders.create(access_token)
63
+
64
+ @classmethod
65
+ async def login(cls, data: dict[str, Any]) -> dict[str, Any]:
66
+ """Login and return tokens."""
67
+ url = f"{cls.BASE_URL}/users/loginWithFreeVerification"
68
+ headers = cls._get_headers()
69
+
70
+ response_data = {}
71
+ try:
72
+ _LOGGER.info("POST %s body=%s", url, data)
73
+ response = await cls.SESSION.post(url, headers=headers, json=data)
74
+ response_data = await response.json()
75
+ response.raise_for_status()
76
+ _LOGGER.info("POST %s → %s resp=%s", url, response.status, response_data)
77
+ return response_data
78
+ except aiohttp.ClientError as e:
79
+ _LOGGER.error(f"login failed: {e} resp={response_data}")
80
+ if response_data.get(CONF_CODE) == ServerErrorCode.USER_PWD_INCORRECT:
81
+ raise AidotUserOrPassIncorrect
82
+ raise Exception
83
+
84
+ @classmethod
85
+ async def refresh_token(cls) -> dict[str, Any]:
86
+ """Refresh access token."""
87
+ if cls.USER_INFO is None:
88
+ raise AidotAuthFailed("No user info available")
89
+
90
+ url = f"{cls.BASE_URL}/users/refreshToken"
91
+ headers = cls._get_headers()
92
+ data = {
93
+ CONF_REFRESH_TOKEN: cls.USER_INFO.refreshToken,
94
+ }
95
+
96
+ response_data = {}
97
+ try:
98
+ _LOGGER.info("POST %s body=%s", url, data)
99
+ response = await cls.SESSION.post(url, headers=headers, json=data)
100
+ response_data = await response.json()
101
+ response.raise_for_status()
102
+
103
+ # Update tokens and notify
104
+ cls.USER_INFO.accessToken = response_data.get(CONF_ACCESS_TOKEN, "")
105
+ if response_data.get(CONF_REFRESH_TOKEN) is not None:
106
+ cls.USER_INFO.refreshToken = response_data[CONF_REFRESH_TOKEN]
107
+
108
+ # Notify callback
109
+ if cls._token_refreshed_callback:
110
+ cls._token_refreshed_callback()
111
+
112
+ _LOGGER.info("Token refreshed: %s", response_data)
113
+ return response_data
114
+ except aiohttp.ClientError as e:
115
+ _LOGGER.error(f"refresh_token failed: {e} resp={response_data}")
116
+ code = response_data.get(CONF_CODE)
117
+ if code == ServerErrorCode.LOGIN_INVALID or code in (21027, 21041):
118
+ raise AidotAuthFailed
119
+ return None
120
+
121
+ @classmethod
122
+ async def get(cls, params: str) -> dict[str, Any]:
123
+ """GET request with auto token refresh on 401."""
124
+ url = f"{cls.BASE_URL}{params}"
125
+ headers = cls._get_headers()
126
+
127
+ response_data = {}
128
+ try:
129
+ _LOGGER.info("GET %s", url)
130
+ response = await cls.SESSION.get(url, headers=headers)
131
+ response_data = await response.json()
132
+ response.raise_for_status()
133
+ _LOGGER.info("GET %s → %s", url, response.status)
134
+ return response_data
135
+ except aiohttp.ClientError as e:
136
+ _LOGGER.error(f"GET {params} failed: {e} resp={response_data}")
137
+ code = response_data.get(CONF_CODE)
138
+ if code == ServerErrorCode.TOKEN_EXPIRED:
139
+ try:
140
+ refresh_data = await cls.refresh_token()
141
+ if refresh_data:
142
+ _LOGGER.info("Retrying GET %s", url)
143
+ headers = cls._get_headers()
144
+ response = await cls.SESSION.get(url, headers=headers)
145
+ response_data = await response.json()
146
+ response.raise_for_status()
147
+ _LOGGER.info("GET %s → %s retry=ok", url, response.status)
148
+ return response_data
149
+ except AidotAuthFailed:
150
+ if cls._auth_failed_callback:
151
+ cls._auth_failed_callback()
152
+ raise
153
+ except Exception as refresh_error:
154
+ _LOGGER.error(f"Token refresh failed: {refresh_error}")
155
+ if cls._auth_failed_callback:
156
+ cls._auth_failed_callback()
157
+ raise AidotAuthFailed
158
+ elif code in (ServerErrorCode.LOGIN_INVALID, 21027, 21041):
159
+ if cls._auth_failed_callback:
160
+ cls._auth_failed_callback()
161
+ raise AidotAuthFailed
162
+ raise
163
+
164
+ @classmethod
165
+ async def get_products(cls, product_ids: str) -> list[dict[str, Any]]:
166
+ """Get products by comma-separated IDs."""
167
+ params = f"/products/{product_ids}"
168
+ return await cls.get(params)
169
+
170
+ @classmethod
171
+ async def get_devices(cls, house_id: str) -> list[dict[str, Any]]:
172
+ """Get devices in a house."""
173
+ params = f"/devices?houseId={house_id}"
174
+ return await cls.get(params)
175
+
176
+ @classmethod
177
+ async def get_houses(cls) -> list[dict[str, Any]]:
178
+ """Get all houses for the user."""
179
+ params = "/houses"
180
+ return await cls.get(params)
aidot/client.py CHANGED
@@ -2,72 +2,42 @@
2
2
 
3
3
  import asyncio
4
4
  import logging
5
- import base64
6
- import aiohttp
7
5
  from aiohttp import ClientSession
8
6
  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
7
  import uuid
13
8
  from pathlib import Path
14
9
  import hashlib
15
- from .exceptions import AidotAuthFailed, AidotUserOrPassIncorrect
10
+
11
+ from .utils import rsa_encrypt
16
12
  from .device_client import DeviceClient
13
+ from .models.auth_model import UserInformation, LoginRequest
14
+ from .models.device_model import DeviceModel
15
+ from .api.cloud_api import CloudApi
17
16
  from .discover import Discover
18
- from .login_const import APP_ID, PUBLIC_KEY_PEM, API_URL_TEMPLATE, DEFAULT_REGION
17
+ from .const import PUBLIC_KEY_PEM
19
18
  from .const import (
20
- CONF_ACCESS_TOKEN,
21
- CONF_APP_ID,
22
- CONF_CODE,
23
- CONF_COUNTRY,
24
- CONF_DEVICE_LIST,
25
19
  CONF_ID,
26
20
  CONF_IPADDRESS,
27
- CONF_PASSWORD,
21
+ CONF_LOGIN_INFO,
22
+ CONF_DEVICE_LIST,
28
23
  CONF_PRODUCT,
29
24
  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
25
  CONF_IS_OWNER,
39
- ServerErrorCode,
26
+ SUPPORTED_COUNTRYS,
27
+ CONF_TYPE,
28
+ CONF_AES_KEY,
40
29
  )
41
30
 
42
31
  _LOGGER = logging.getLogger(__name__)
43
32
 
44
33
 
45
- def rsa_password_encrypt(message: str) -> str:
46
- """Get password rsa encrypt."""
47
- public_key = serialization.load_pem_public_key(
48
- PUBLIC_KEY_PEM, backend=default_backend()
49
- )
50
-
51
- encrypted = public_key.encrypt(
52
- message.encode("utf-8"),
53
- padding.PKCS1v15(),
54
- )
55
-
56
- encrypted_base64 = base64.b64encode(encrypted).decode("utf-8")
57
- return encrypted_base64
58
-
59
-
60
34
  class AidotClient:
61
- _base_url: str = API_URL_TEMPLATE.format(region=DEFAULT_REGION)
62
- _region: str = DEFAULT_REGION
63
- session: Optional[ClientSession] = None
64
- username: str = ""
65
- password: str = ""
66
- country_name: str = DEFAULT_COUNTRY_NAME
67
- country_code: str = DEFAULT_COUNTRY_CODE
68
- login_info: dict[str, Any] = {}
35
+ """AiDot client for managing devices and authentication."""
36
+
69
37
  _device_clients: dict[str, DeviceClient]
70
- _discover: Discover = None
38
+ user_info: UserInformation = None
39
+ _token_fresh_cb: Optional[callable] = None
40
+ _products: dict[str, dict[str, Any]] = {}
71
41
 
72
42
  def __init__(
73
43
  self,
@@ -77,36 +47,54 @@ class AidotClient:
77
47
  password: str | None = None,
78
48
  token: dict | None = None,
79
49
  ) -> None:
80
- _LOGGER.info("Client Version: v0.3.52")
81
- self.session = session
82
- self.username = username
83
- self.password = password
50
+ _LOGGER.info("Client Version: v0.3.54")
84
51
  self.country_code = country_code
85
52
  self._device_clients = {}
53
+ self.user_info = UserInformation(
54
+ username=username, password=password, country_code=country_code
55
+ )
56
+
57
+ # Set region and country from country_code
86
58
  for item in SUPPORTED_COUNTRYS:
87
- if item["id"] == self.country_code:
88
- self.country_name = item["name"]
89
- self._region = item["region"].lower()
90
- self._base_url = API_URL_TEMPLATE.format(region=self._region)
59
+ if item[CONF_ID] == self.country_code:
60
+ self.user_info.country = item["name"]
61
+ self.user_info.region = item["region"].lower()
91
62
  break
63
+
64
+ # Handle token (existing login info)
92
65
  if token is not None:
93
- self.login_info = token.copy()
94
- self.username = token[CONF_USERNAME]
95
- self.password = token[CONF_PASSWORD]
96
- self._region = token[CONF_REGION]
97
- self.country_name = token[CONF_COUNTRY]
98
- self._base_url = API_URL_TEMPLATE.format(region=self._region)
66
+ if token.get(CONF_ID) is None and token.get(CONF_LOGIN_INFO) is not None:
67
+ token = token.get(CONF_LOGIN_INFO)
68
+ self.user_info.update_from_json(token)
69
+
70
+ # Setup CloudApi
71
+ CloudApi.set_session(session)
72
+ CloudApi.set_user_info(self.user_info)
73
+ CloudApi.set_auth_failed_callback(self._on_auth_failed)
74
+ CloudApi.set_token_refreshed_callback(self._on_token_refreshed)
99
75
  self.setup_discover()
76
+
77
+ @property
78
+ def login_info(self) -> dict:
79
+ return self.user_info.to_dict()
80
+
81
+ def _on_auth_failed(self) -> None:
82
+ """Handle authentication failed event from CloudApi."""
83
+ _LOGGER.warning("Authentication failed, clearing user info")
84
+ self.user_info.accessToken = ""
85
+
86
+ def _on_token_refreshed(self) -> None:
87
+ """Handle token refreshed event from CloudApi."""
88
+ _LOGGER.debug("Token refreshed successfully")
89
+ if self._token_fresh_cb:
90
+ self._token_fresh_cb()
91
+
100
92
  def set_token_fresh_cb(self, callback) -> None:
93
+ """Set callback for token refresh events."""
101
94
  self._token_fresh_cb = callback
102
95
 
103
- def get_identifier(self) -> str:
104
- return f"{self._region}-{self.username}"
105
-
106
- def update_password(self, password: str) -> None:
107
- self.password = password
108
-
109
96
  async def get_terminal_id(self) -> str:
97
+ """Get or create terminal ID for device identification."""
110
98
  file_path = Path.home() / ".aidot_terminal_id"
111
99
 
112
100
  def _read_or_create() -> str:
@@ -119,186 +107,102 @@ class AidotClient:
119
107
  file_path.write_text(raw_id)
120
108
  return raw_id
121
109
  except OSError:
122
- return 'gvz3gjae10l4zii00t7y0'
110
+ return "gvz3gjae10l4zii00t7y0"
123
111
 
124
112
  raw_id = await asyncio.to_thread(_read_or_create)
125
113
  return hashlib.md5(raw_id.encode()).hexdigest()
126
114
 
127
115
  async def async_post_login(self) -> dict[str, Any]:
128
116
  """Login the user input allows us to connect."""
129
- url = f"{self._base_url}/users/loginWithFreeVerification"
130
- headers = {CONF_APP_ID: APP_ID, CONF_TERMINAL: "app"}
131
- # f"{region}:{self.country_name.strip()}",
132
- terminalId = await self.get_terminal_id()
133
- if terminalId is None:
134
- terminalId = "gvz3gjae10l4zii00t7y0"
135
- data = {
136
- "countryKey": f"region:{self.country_name.strip()}",
137
- "username": self.username,
138
- "password": rsa_password_encrypt(self.password),
139
- "terminalId": terminalId,
140
- "webVersion": "0.5.0",
141
- "area": "Asia/Shanghai",
142
- "UTC": "UTC+8",
143
- }
117
+ terminal_id = await self.get_terminal_id()
144
118
 
145
- try:
146
- response = await self.session.post(url, headers=headers, json=data)
147
- response_data = await response.json()
148
- response.raise_for_status()
149
- self.login_info = response_data
150
- self.login_info[CONF_PASSWORD] = self.password
151
- self.login_info[CONF_REGION] = self._region
152
- self.login_info[CONF_COUNTRY] = self.country_name
153
- self.setup_discover()
154
- return self.login_info
155
- except aiohttp.ClientError as e:
156
- _LOGGER.error(f"async_post_login ClientError: {e}")
157
- if response_data[CONF_CODE] == ServerErrorCode.USER_PWD_INCORRECT:
158
- raise AidotUserOrPassIncorrect
159
- raise Exception
160
-
161
- async def async_refresh_token(self) -> dict[str, Any]:
162
- url = f"{self._base_url}/users/refreshToken"
163
- headers = {CONF_APP_ID: APP_ID, CONF_TERMINAL: "app"}
164
- data = {
165
- CONF_REFRESH_TOKEN: self.login_info[CONF_REFRESH_TOKEN],
166
- }
119
+ login_request = LoginRequest.create(
120
+ username=self.user_info.username,
121
+ encrypted_password=rsa_encrypt(self.user_info.password, PUBLIC_KEY_PEM),
122
+ country_name=self.user_info.country,
123
+ terminal_id=terminal_id,
124
+ )
167
125
 
168
- try:
169
- response = await self.session.post(url, headers=headers, json=data)
170
- response_data = await response.json()
171
- response.raise_for_status()
172
- self.login_info[CONF_ACCESS_TOKEN] = response_data[CONF_ACCESS_TOKEN]
173
- if response_data[CONF_REFRESH_TOKEN] is not None:
174
- self.login_info[CONF_REFRESH_TOKEN] = response_data[CONF_REFRESH_TOKEN]
175
- _LOGGER.debug(f"refresh token: {response_data}")
176
- if self._token_fresh_cb:
177
- self._token_fresh_cb()
178
- return response_data
179
- except aiohttp.ClientError as e:
180
- _LOGGER.error(f"async_refresh_token ClientError: {e} {response_data}")
181
- if response_data[CONF_CODE] == ServerErrorCode.LOGIN_INVALID:
182
- raise AidotAuthFailed
183
- return None
184
-
185
- async def async_session_get(
186
- self, params: str, headers: str | None = None
187
- ) -> dict[str, Any]:
188
- url = f"{self._base_url}{params}"
189
- token = self.login_info[CONF_ACCESS_TOKEN]
190
- if token is None:
191
- raise AidotAuthFailed()
192
- if headers is None:
193
- headers = {
194
- CONF_TERMINAL: "app",
195
- CONF_TOKEN: token,
196
- CONF_APP_ID: APP_ID,
197
- }
198
- response_data = {}
199
- try:
200
- response = await self.session.get(url, headers=headers)
201
- response_data = await response.json()
202
- response.raise_for_status()
203
- return response_data
204
- except aiohttp.ClientError as e:
205
- _LOGGER.error(f"async_get ClientError: {e} {response_data}")
206
- code = response_data.get(CONF_CODE)
207
- if code == ServerErrorCode.TOKEN_EXPIRED:
208
- try:
209
- await self.async_refresh_token()
210
- return await self.async_session_get(params)
211
- except AidotAuthFailed:
212
- raise AidotAuthFailed
213
- elif (
214
- code == ServerErrorCode.LOGIN_INVALID or code == 21027 or code == 21041
215
- ):
216
- self.login_info[CONF_ACCESS_TOKEN] = None
217
- raise AidotAuthFailed
218
- return aiohttp.ClientError
219
-
220
- async def async_get_products(self, product_ids: str) -> list[dict[str, Any]]:
221
- """Get device list."""
222
- params = f"/products/{product_ids}"
223
- return await self.async_session_get(params)
224
-
225
- async def async_get_devices(self, house_id: str) -> list[dict[str, Any]]:
226
- """Get device list."""
227
- params = f"/devices?houseId={house_id}"
228
- return await self.async_session_get(params)
229
-
230
- async def async_get_houses(self) -> list[dict[str, Any]]:
231
- """Get house list."""
232
- params = "/houses"
233
- return await self.async_session_get(params)
126
+ response_data = await CloudApi.login(login_request.to_dict())
127
+ self.user_info.update_from_json(response_data)
128
+ self.setup_discover()
129
+ return self.user_info.to_dict()
234
130
 
235
131
  async def async_get_all_device(self) -> dict[str, Any]:
236
- final_device_list: list[dict[str, Any]] = []
132
+ """Get all devices for the user."""
133
+ filter_devices: dict[str, Any] = {}
134
+ filter_product_ids: set[str] = set()
237
135
  try:
238
- houses = await self.async_get_houses()
136
+ houses = await CloudApi.get_houses() or []
239
137
  for house in houses:
240
138
  if house.get(CONF_IS_OWNER) is False:
241
139
  continue
242
- # get device_list
243
- device_list = await self.async_get_devices(house[CONF_ID])
244
- if device_list:
245
- final_device_list.extend(device_list)
246
-
247
- # get product_list
248
- productIds = ",".join([item[CONF_PRODUCT_ID] for item in final_device_list])
249
- product_list = await self.async_get_products(productIds)
250
-
251
- for product in product_list:
252
- for device in final_device_list:
253
- if device[CONF_PRODUCT_ID] == product[CONF_ID]:
254
- device[CONF_PRODUCT] = product
140
+ device_list = await CloudApi.get_devices(house[CONF_ID]) or []
141
+ for device in device_list:
142
+ filter_device: dict[str, Any] = None
143
+ if (
144
+ device.get(CONF_TYPE) == "light"
145
+ and device.get(CONF_AES_KEY, [None])[0] is not None
146
+ ):
147
+ filter_device = device
148
+ if filter_device is not None:
149
+ filter_devices[device[CONF_ID]] = device
150
+ filter_product_ids.add(device[CONF_PRODUCT_ID])
151
+
152
+ # Get product info and merge into devices
153
+ if filter_product_ids:
154
+ product_ids = ",".join(filter_product_ids)
155
+ product_list = await CloudApi.get_products(product_ids) or []
156
+ product_map = {p[CONF_ID]: p for p in product_list}
157
+ for device in filter_devices.values():
158
+ device[CONF_PRODUCT] = product_map.get(device[CONF_PRODUCT_ID])
159
+
255
160
  except Exception as e:
256
161
  raise e
257
- return {CONF_DEVICE_LIST: final_device_list}
162
+ return filter_devices
258
163
 
259
164
  def get_device_client(self, device: dict[str, Any]) -> DeviceClient:
260
- device_id = device.get(CONF_ID)
261
- device_client: DeviceClient = self._device_clients.get(device_id)
165
+ """Get or create device client for a device."""
166
+ _device: DeviceModel = DeviceModel.from_json(data=device)
167
+ device_client: DeviceClient = self._device_clients.get(_device.id)
262
168
  if device_client is None:
263
- device_client = DeviceClient(device, self.login_info)
264
- self._device_clients[device_id] = device_client
265
- if self._discover is not None:
266
- ip = self._discover.discovered_device.get(device_id)
267
- device_client.update_ip_address(ip)
169
+ device_client = DeviceClient(_device, self.user_info)
170
+ self._device_clients[_device.id] = device_client
171
+
172
+ ip = Discover.DISCOVERED_DEVICE.get(_device.id)
173
+ device_client.update_ip_address(ip)
268
174
  return device_client
269
175
 
270
176
  async def remove_device_client(self, dev_id: str) -> None:
177
+ """Remove and close device client."""
271
178
  device_client: DeviceClient = self._device_clients.get(dev_id)
272
179
  if device_client is not None:
273
180
  await device_client.close()
274
181
  del self._device_clients[dev_id]
275
182
 
276
183
  def setup_discover(self) -> None:
277
- """初始化完成后调用,启动设备发现"""
278
- if self.login_info.get(CONF_ID) is None:
279
- return
280
- if self._discover is not None:
184
+ """Initialize device discovery after login."""
185
+ if not self.user_info.id:
281
186
  return
282
187
 
283
- def _discover_callback(dev_id, event: dict[str, str]) -> None:
188
+ _LOGGER.warning("setup_discover")
189
+
190
+ def _discover_callback(dev_id: str, event: dict[str, str]) -> None:
284
191
  device_ip = event[CONF_IPADDRESS]
285
192
  device_client: DeviceClient = self._device_clients.get(dev_id)
286
193
  if device_client is not None:
287
194
  device_client.update_ip_address(device_ip)
288
195
 
289
- self._discover = Discover(self.login_info, _discover_callback)
290
- self._discover.start_repeat_broadcast()
196
+ Discover.set_call_back(_discover_callback)
197
+ Discover.set_user_info(self.user_info)
291
198
 
292
199
  async def async_close(self) -> None:
293
- """关闭客户端,清理资源"""
294
- if self._discover is not None:
295
- self._discover.close()
296
- self._discover = None
200
+ """Close client and cleanup resources."""
297
201
  for client in self._device_clients.values():
298
202
  await client.close()
299
203
  self._device_clients.clear()
300
204
 
301
205
  async def async_cleanup(self) -> None:
302
- """清理所有资源"""
206
+ """Cleanup all resources."""
303
207
  _LOGGER.debug("async_cleanup")
304
208
  await self.async_close()
aidot/const.py CHANGED
@@ -188,6 +188,7 @@ CONF_IS_DEFAULT = "isDefault"
188
188
  CONF_TYPE = "type"
189
189
  CONF_MODEL_ID = "modelId"
190
190
  CONF_MAC = "mac"
191
+ CONF_LOGIN_INFO = "loginInfo"
191
192
  CONF_AES_KEY = "aesKey"
192
193
  CONF_MODEL_ID = "modelId"
193
194
  CONF_HARDWARE_VERSION = "hardwareVersion"
@@ -207,6 +208,8 @@ CONF_RGBW = "RGBW"
207
208
  CONF_CCT = "CCT"
208
209
  CONF_ACK = "ack"
209
210
  CONF_IS_OWNER = "isOwner"
211
+ CONF_GET_DEV_ATTR_REQ = "getDevAttrReq"
212
+ CONF_SET_DEV_ATTR_REQ = "setDevAttrReq"
210
213
 
211
214
 
212
215
  class Identity(StrEnum):
@@ -227,3 +230,17 @@ class ServerErrorCode(IntEnum):
227
230
  TOKEN_EXPIRED = 21026
228
231
  LOGIN_INVALID = 21025
229
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
+ """