python-aidot 0.3.52__tar.gz → 0.3.54__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.52 → python_aidot-0.3.54}/PKG-INFO +1 -1
- python_aidot-0.3.54/aidot/api/__init__.py +0 -0
- python_aidot-0.3.54/aidot/api/cloud_api.py +180 -0
- python_aidot-0.3.54/aidot/client.py +208 -0
- {python_aidot-0.3.52 → python_aidot-0.3.54}/aidot/const.py +17 -0
- python_aidot-0.3.54/aidot/device_client.py +478 -0
- python_aidot-0.3.54/aidot/discover.py +138 -0
- python_aidot-0.3.54/aidot/models/__init__.py +1 -0
- python_aidot-0.3.54/aidot/models/auth_model.py +170 -0
- python_aidot-0.3.54/aidot/models/base_model.py +22 -0
- python_aidot-0.3.54/aidot/models/device_client_model.py +209 -0
- python_aidot-0.3.54/aidot/models/device_model.py +170 -0
- python_aidot-0.3.54/aidot/models/discover_model.py +103 -0
- python_aidot-0.3.54/aidot/utils/__init__.py +12 -0
- python_aidot-0.3.54/aidot/utils/async_timer.py +142 -0
- python_aidot-0.3.54/aidot/utils/crypto.py +91 -0
- {python_aidot-0.3.52 → python_aidot-0.3.54}/python_aidot.egg-info/PKG-INFO +1 -1
- python_aidot-0.3.54/python_aidot.egg-info/SOURCES.txt +26 -0
- {python_aidot-0.3.52 → python_aidot-0.3.54}/setup.py +2 -2
- python_aidot-0.3.52/aidot/aes_utils.py +0 -27
- python_aidot-0.3.52/aidot/client.py +0 -304
- python_aidot-0.3.52/aidot/device_client.py +0 -431
- python_aidot-0.3.52/aidot/discover.py +0 -149
- python_aidot-0.3.52/aidot/login_const.py +0 -16
- python_aidot-0.3.52/python_aidot.egg-info/SOURCES.txt +0 -17
- {python_aidot-0.3.52 → python_aidot-0.3.54}/LICENSE +0 -0
- {python_aidot-0.3.52 → python_aidot-0.3.54}/README.md +0 -0
- {python_aidot-0.3.52 → python_aidot-0.3.54}/aidot/__init__.py +0 -0
- {python_aidot-0.3.52 → python_aidot-0.3.54}/aidot/exceptions.py +0 -0
- {python_aidot-0.3.52 → python_aidot-0.3.54}/python_aidot.egg-info/dependency_links.txt +0 -0
- {python_aidot-0.3.52 → python_aidot-0.3.54}/python_aidot.egg-info/requires.txt +0 -0
- {python_aidot-0.3.52 → python_aidot-0.3.54}/python_aidot.egg-info/top_level.txt +0 -0
- {python_aidot-0.3.52 → python_aidot-0.3.54}/setup.cfg +0 -0
|
File without changes
|
|
@@ -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)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""The aidot integration."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from aiohttp import ClientSession
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
import uuid
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import hashlib
|
|
10
|
+
|
|
11
|
+
from .utils import rsa_encrypt
|
|
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
|
|
16
|
+
from .discover import Discover
|
|
17
|
+
from .const import PUBLIC_KEY_PEM
|
|
18
|
+
from .const import (
|
|
19
|
+
CONF_ID,
|
|
20
|
+
CONF_IPADDRESS,
|
|
21
|
+
CONF_LOGIN_INFO,
|
|
22
|
+
CONF_DEVICE_LIST,
|
|
23
|
+
CONF_PRODUCT,
|
|
24
|
+
CONF_PRODUCT_ID,
|
|
25
|
+
CONF_IS_OWNER,
|
|
26
|
+
SUPPORTED_COUNTRYS,
|
|
27
|
+
CONF_TYPE,
|
|
28
|
+
CONF_AES_KEY,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_LOGGER = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AidotClient:
|
|
35
|
+
"""AiDot client for managing devices and authentication."""
|
|
36
|
+
|
|
37
|
+
_device_clients: dict[str, DeviceClient]
|
|
38
|
+
user_info: UserInformation = None
|
|
39
|
+
_token_fresh_cb: Optional[callable] = None
|
|
40
|
+
_products: dict[str, dict[str, Any]] = {}
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
session: Optional[ClientSession],
|
|
45
|
+
country_code: str | None = None,
|
|
46
|
+
username: str | None = None,
|
|
47
|
+
password: str | None = None,
|
|
48
|
+
token: dict | None = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
_LOGGER.info("Client Version: v0.3.54")
|
|
51
|
+
self.country_code = country_code
|
|
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
|
|
58
|
+
for item in SUPPORTED_COUNTRYS:
|
|
59
|
+
if item[CONF_ID] == self.country_code:
|
|
60
|
+
self.user_info.country = item["name"]
|
|
61
|
+
self.user_info.region = item["region"].lower()
|
|
62
|
+
break
|
|
63
|
+
|
|
64
|
+
# Handle token (existing login info)
|
|
65
|
+
if token is not None:
|
|
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)
|
|
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
|
+
|
|
92
|
+
def set_token_fresh_cb(self, callback) -> None:
|
|
93
|
+
"""Set callback for token refresh events."""
|
|
94
|
+
self._token_fresh_cb = callback
|
|
95
|
+
|
|
96
|
+
async def get_terminal_id(self) -> str:
|
|
97
|
+
"""Get or create terminal ID for device identification."""
|
|
98
|
+
file_path = Path.home() / ".aidot_terminal_id"
|
|
99
|
+
|
|
100
|
+
def _read_or_create() -> str:
|
|
101
|
+
try:
|
|
102
|
+
if file_path.exists():
|
|
103
|
+
return file_path.read_text().strip()
|
|
104
|
+
node = uuid.getnode()
|
|
105
|
+
is_random = (node >> 40) & 1
|
|
106
|
+
raw_id = str(uuid.uuid4()) if is_random else format(node, "x")
|
|
107
|
+
file_path.write_text(raw_id)
|
|
108
|
+
return raw_id
|
|
109
|
+
except OSError:
|
|
110
|
+
return "gvz3gjae10l4zii00t7y0"
|
|
111
|
+
|
|
112
|
+
raw_id = await asyncio.to_thread(_read_or_create)
|
|
113
|
+
return hashlib.md5(raw_id.encode()).hexdigest()
|
|
114
|
+
|
|
115
|
+
async def async_post_login(self) -> dict[str, Any]:
|
|
116
|
+
"""Login the user input allows us to connect."""
|
|
117
|
+
terminal_id = await self.get_terminal_id()
|
|
118
|
+
|
|
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
|
+
)
|
|
125
|
+
|
|
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()
|
|
130
|
+
|
|
131
|
+
async def async_get_all_device(self) -> dict[str, Any]:
|
|
132
|
+
"""Get all devices for the user."""
|
|
133
|
+
filter_devices: dict[str, Any] = {}
|
|
134
|
+
filter_product_ids: set[str] = set()
|
|
135
|
+
try:
|
|
136
|
+
houses = await CloudApi.get_houses() or []
|
|
137
|
+
for house in houses:
|
|
138
|
+
if house.get(CONF_IS_OWNER) is False:
|
|
139
|
+
continue
|
|
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
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
raise e
|
|
162
|
+
return filter_devices
|
|
163
|
+
|
|
164
|
+
def get_device_client(self, device: dict[str, Any]) -> DeviceClient:
|
|
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)
|
|
168
|
+
if device_client is None:
|
|
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)
|
|
174
|
+
return device_client
|
|
175
|
+
|
|
176
|
+
async def remove_device_client(self, dev_id: str) -> None:
|
|
177
|
+
"""Remove and close device client."""
|
|
178
|
+
device_client: DeviceClient = self._device_clients.get(dev_id)
|
|
179
|
+
if device_client is not None:
|
|
180
|
+
await device_client.close()
|
|
181
|
+
del self._device_clients[dev_id]
|
|
182
|
+
|
|
183
|
+
def setup_discover(self) -> None:
|
|
184
|
+
"""Initialize device discovery after login."""
|
|
185
|
+
if not self.user_info.id:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
_LOGGER.warning("setup_discover")
|
|
189
|
+
|
|
190
|
+
def _discover_callback(dev_id: str, event: dict[str, str]) -> None:
|
|
191
|
+
device_ip = event[CONF_IPADDRESS]
|
|
192
|
+
device_client: DeviceClient = self._device_clients.get(dev_id)
|
|
193
|
+
if device_client is not None:
|
|
194
|
+
device_client.update_ip_address(device_ip)
|
|
195
|
+
|
|
196
|
+
Discover.set_call_back(_discover_callback)
|
|
197
|
+
Discover.set_user_info(self.user_info)
|
|
198
|
+
|
|
199
|
+
async def async_close(self) -> None:
|
|
200
|
+
"""Close client and cleanup resources."""
|
|
201
|
+
for client in self._device_clients.values():
|
|
202
|
+
await client.close()
|
|
203
|
+
self._device_clients.clear()
|
|
204
|
+
|
|
205
|
+
async def async_cleanup(self) -> None:
|
|
206
|
+
"""Cleanup all resources."""
|
|
207
|
+
_LOGGER.debug("async_cleanup")
|
|
208
|
+
await self.async_close()
|
|
@@ -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
|
+
"""
|