python-aidot 0.2.4__tar.gz → 0.2.6__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.2.4 → python_aidot-0.2.6}/PKG-INFO +1 -1
- python_aidot-0.2.6/aidot/client.py +210 -0
- {python_aidot-0.2.4 → python_aidot-0.2.6}/aidot/const.py +17 -3
- python_aidot-0.2.6/aidot/exceptions.py +22 -0
- {python_aidot-0.2.4 → python_aidot-0.2.6}/python_aidot.egg-info/PKG-INFO +1 -1
- {python_aidot-0.2.4 → python_aidot-0.2.6}/python_aidot.egg-info/SOURCES.txt +2 -0
- {python_aidot-0.2.4 → python_aidot-0.2.6}/setup.py +1 -1
- {python_aidot-0.2.4 → python_aidot-0.2.6}/LICENSE +0 -0
- {python_aidot-0.2.4 → python_aidot-0.2.6}/README.md +0 -0
- {python_aidot-0.2.4 → python_aidot-0.2.6}/aidot/__init__.py +0 -0
- {python_aidot-0.2.4 → python_aidot-0.2.6}/aidot/aes_utils.py +0 -0
- {python_aidot-0.2.4 → python_aidot-0.2.6}/aidot/discover.py +0 -0
- {python_aidot-0.2.4 → python_aidot-0.2.6}/aidot/lan.py +0 -0
- {python_aidot-0.2.4 → python_aidot-0.2.6}/aidot/login_const.py +0 -0
- {python_aidot-0.2.4 → python_aidot-0.2.6}/aidot/login_control.py +0 -0
- {python_aidot-0.2.4 → python_aidot-0.2.6}/aidot/login_data.py +0 -0
- {python_aidot-0.2.4 → python_aidot-0.2.6}/python_aidot.egg-info/dependency_links.txt +0 -0
- {python_aidot-0.2.4 → python_aidot-0.2.6}/python_aidot.egg-info/requires.txt +0 -0
- {python_aidot-0.2.4 → python_aidot-0.2.6}/python_aidot.egg-info/top_level.txt +0 -0
- {python_aidot-0.2.4 → python_aidot-0.2.6}/setup.cfg +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""The aidot integration."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
from aiohttp import ClientResponseError, ClientSession
|
|
6
|
+
import base64
|
|
7
|
+
import aiohttp
|
|
8
|
+
from cryptography.hazmat.backends import default_backend
|
|
9
|
+
from cryptography.hazmat.primitives import serialization
|
|
10
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
11
|
+
from .login_const import APP_ID, PUBLIC_KEY_PEM, BASE_URL
|
|
12
|
+
from .const import (
|
|
13
|
+
SUPPORTED_COUNTRYS,
|
|
14
|
+
DEFAULT_COUNTRY_NAME,
|
|
15
|
+
CONF_PRODUCT_ID,
|
|
16
|
+
CONF_ID,
|
|
17
|
+
CONF_PRODUCT,
|
|
18
|
+
CONF_ACCESS_TOKEN,
|
|
19
|
+
CONF_REFRESH_TOKEN,
|
|
20
|
+
CONF_TERMINAL,
|
|
21
|
+
CONF_APP_ID,
|
|
22
|
+
CONF_REGION,
|
|
23
|
+
CONF_COUNTRY,
|
|
24
|
+
CONF_USERNAME,
|
|
25
|
+
CONF_PASSWORD,
|
|
26
|
+
CONF_CODE,
|
|
27
|
+
CONF_TOKEN,
|
|
28
|
+
ServerErrorCode
|
|
29
|
+
)
|
|
30
|
+
from .exceptions import AidotAuthFailed,AidotUserOrPassIncorrect
|
|
31
|
+
|
|
32
|
+
_LOGGER = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def rsa_password_encrypt(message: str):
|
|
36
|
+
"""Get password rsa encrypt."""
|
|
37
|
+
public_key = serialization.load_pem_public_key(
|
|
38
|
+
PUBLIC_KEY_PEM, backend=default_backend()
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
encrypted = public_key.encrypt(
|
|
42
|
+
message.encode("utf-8"),
|
|
43
|
+
padding.PKCS1v15(),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
encrypted_base64 = base64.b64encode(encrypted).decode("utf-8")
|
|
47
|
+
return encrypted_base64
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AidotClient:
|
|
51
|
+
_base_url: str = BASE_URL
|
|
52
|
+
_region: str = "us"
|
|
53
|
+
session: Optional[ClientSession] = None
|
|
54
|
+
username: str = ""
|
|
55
|
+
password: str = ""
|
|
56
|
+
country_name: str = DEFAULT_COUNTRY_NAME
|
|
57
|
+
login_info: dict[str, Any] = {}
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
session: Optional[ClientSession],
|
|
62
|
+
country_name: str | None = None,
|
|
63
|
+
username: str | None = None,
|
|
64
|
+
password: str | None = None,
|
|
65
|
+
token: dict | None = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
self.session = session
|
|
68
|
+
self.country_name = country_name
|
|
69
|
+
self.username = username
|
|
70
|
+
self.password = password
|
|
71
|
+
self.login_info = token
|
|
72
|
+
for item in SUPPORTED_COUNTRYS:
|
|
73
|
+
if item["name"] == self.country_name:
|
|
74
|
+
self._region = item["region"].lower()
|
|
75
|
+
self._base_url = f"https://prod-{self._region}-api.arnoo.com/v17"
|
|
76
|
+
break
|
|
77
|
+
if token is not None:
|
|
78
|
+
self.username = token[CONF_USERNAME]
|
|
79
|
+
self.password = token[CONF_PASSWORD]
|
|
80
|
+
self._region = token[CONF_REGION]
|
|
81
|
+
self.country_name = token[CONF_COUNTRY]
|
|
82
|
+
|
|
83
|
+
def set_token_fresh_cb(self, callback):
|
|
84
|
+
self._token_fresh_cb = callback
|
|
85
|
+
|
|
86
|
+
def get_identifier(self) -> str:
|
|
87
|
+
return f"{self._region}-{self.username}"
|
|
88
|
+
|
|
89
|
+
def update_password(self, password: str):
|
|
90
|
+
self.password = password
|
|
91
|
+
|
|
92
|
+
async def async_post_login(self):
|
|
93
|
+
"""Login the user input allows us to connect."""
|
|
94
|
+
url = f"{self._base_url}/users/loginWithFreeVerification"
|
|
95
|
+
headers = {CONF_APP_ID: APP_ID, CONF_TERMINAL: "app"}
|
|
96
|
+
data = {
|
|
97
|
+
"countryKey": "region:UnitedStates",
|
|
98
|
+
"username": self.username,
|
|
99
|
+
"password": rsa_password_encrypt(self.password),
|
|
100
|
+
"terminalId": "gvz3gjae10l4zii00t7y0",
|
|
101
|
+
"webVersion": "0.5.0",
|
|
102
|
+
"area": "Asia/Shanghai",
|
|
103
|
+
"UTC": "UTC+8",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
response = await self.session.post(url, headers=headers, json=data)
|
|
108
|
+
response_data = await response.json()
|
|
109
|
+
response.raise_for_status()
|
|
110
|
+
self.login_info = response_data
|
|
111
|
+
self.login_info[CONF_PASSWORD] = self.password
|
|
112
|
+
self.login_info[CONF_REGION] = self._region
|
|
113
|
+
self.login_info[CONF_COUNTRY] = self.country_name
|
|
114
|
+
return self.login_info
|
|
115
|
+
except aiohttp.ClientError as e:
|
|
116
|
+
_LOGGER.info(f"async_post_login ClientError {e}")
|
|
117
|
+
if response_data[CONF_CODE] == ServerErrorCode.USER_PWD_INCORRECT:
|
|
118
|
+
raise AidotUserOrPassIncorrect
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
async def async_refresh_token(self):
|
|
122
|
+
url = f"{self._base_url}/users/refreshToken"
|
|
123
|
+
headers = {CONF_APP_ID: APP_ID, CONF_TERMINAL: "app"}
|
|
124
|
+
data = {
|
|
125
|
+
CONF_REFRESH_TOKEN: self.login_info[CONF_REFRESH_TOKEN],
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
response = await self.session.post(url, headers=headers, json=data)
|
|
130
|
+
response_data = await response.json()
|
|
131
|
+
response.raise_for_status()
|
|
132
|
+
self.login_info[CONF_ACCESS_TOKEN] = response_data[CONF_ACCESS_TOKEN]
|
|
133
|
+
if response_data[CONF_REFRESH_TOKEN] is not None:
|
|
134
|
+
self.login_info[CONF_REFRESH_TOKEN] = response_data[CONF_REFRESH_TOKEN]
|
|
135
|
+
_LOGGER.info(f"refresh token {response_data}")
|
|
136
|
+
if self._token_fresh_cb:
|
|
137
|
+
self._token_fresh_cb()
|
|
138
|
+
return response_data
|
|
139
|
+
except aiohttp.ClientError as e:
|
|
140
|
+
_LOGGER.info(f"async_refresh_token ClientError {e}")
|
|
141
|
+
if response_data[CONF_CODE] == ServerErrorCode.LOGIN_INVALID:
|
|
142
|
+
raise AidotAuthFailed
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
async def async_session_get(self, params: str, headers: str | None = None):
|
|
146
|
+
url = f"{self._base_url}{params}"
|
|
147
|
+
token = self.login_info[CONF_ACCESS_TOKEN]
|
|
148
|
+
if token is None:
|
|
149
|
+
raise AidotAuthFailed()
|
|
150
|
+
if headers is None:
|
|
151
|
+
headers = {
|
|
152
|
+
CONF_TERMINAL: "app",
|
|
153
|
+
CONF_TOKEN: token,
|
|
154
|
+
CONF_APP_ID: APP_ID,
|
|
155
|
+
}
|
|
156
|
+
try:
|
|
157
|
+
response = await self.session.get(url, headers=headers)
|
|
158
|
+
response_data = await response.json()
|
|
159
|
+
response.raise_for_status()
|
|
160
|
+
return response_data
|
|
161
|
+
except aiohttp.ClientError as e:
|
|
162
|
+
_LOGGER.info(f"async_get ClientError {e}")
|
|
163
|
+
code = response_data[CONF_CODE]
|
|
164
|
+
if code == ServerErrorCode.TOKEN_EXPIRED:
|
|
165
|
+
try:
|
|
166
|
+
await self.async_refresh_token()
|
|
167
|
+
return await self.async_session_get(params)
|
|
168
|
+
except AidotAuthFailed:
|
|
169
|
+
raise AidotAuthFailed
|
|
170
|
+
elif code == ServerErrorCode.LOGIN_INVALID or code == 21027 or code == 21041:
|
|
171
|
+
self.login_info[CONF_ACCESS_TOKEN] = None
|
|
172
|
+
raise AidotAuthFailed
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
async def async_get_products(self, product_ids: str):
|
|
176
|
+
"""Get device list."""
|
|
177
|
+
params = f"/products/{product_ids}"
|
|
178
|
+
return await self.async_session_get(params)
|
|
179
|
+
|
|
180
|
+
async def async_get_devices(self, house_id: str):
|
|
181
|
+
"""Get device list."""
|
|
182
|
+
params = f"/devices?houseId={house_id}"
|
|
183
|
+
return await self.async_session_get(params)
|
|
184
|
+
|
|
185
|
+
async def async_get_houses(self):
|
|
186
|
+
"""Get house list."""
|
|
187
|
+
params = "/houses"
|
|
188
|
+
return await self.async_session_get(params)
|
|
189
|
+
|
|
190
|
+
async def async_get_all_device(self):
|
|
191
|
+
final_device_list: list[dict[str, Any]] = []
|
|
192
|
+
try:
|
|
193
|
+
houses = await self.async_get_houses()
|
|
194
|
+
for house in houses:
|
|
195
|
+
# get device_list
|
|
196
|
+
device_list = await self.async_get_devices(house[CONF_ID])
|
|
197
|
+
if device_list:
|
|
198
|
+
final_device_list.extend(device_list)
|
|
199
|
+
|
|
200
|
+
# get product_list
|
|
201
|
+
productIds = ",".join([item[CONF_PRODUCT_ID] for item in final_device_list])
|
|
202
|
+
product_list = await self.async_get_products(productIds)
|
|
203
|
+
|
|
204
|
+
for product in product_list:
|
|
205
|
+
for device in final_device_list:
|
|
206
|
+
if device[CONF_PRODUCT_ID] == product[CONF_ID]:
|
|
207
|
+
device[CONF_PRODUCT] = product
|
|
208
|
+
except Exception as e:
|
|
209
|
+
raise e
|
|
210
|
+
return final_device_list
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from enum import StrEnum
|
|
1
|
+
from enum import StrEnum,IntEnum
|
|
2
2
|
SUPPORTED_COUNTRYS = [
|
|
3
3
|
{"_id": "1-0", "id": "AL", "name": "Albania", "ext": "", "region": "EU"},
|
|
4
4
|
{
|
|
@@ -163,11 +163,20 @@ SUPPORTED_COUNTRYS = [
|
|
|
163
163
|
|
|
164
164
|
SUPPORTED_COUNTRY_NAMES = [item["name"] for item in SUPPORTED_COUNTRYS]
|
|
165
165
|
DEFAULT_COUNTRY_NAME = "United States"
|
|
166
|
+
CONF_APP_ID = "Appid"
|
|
167
|
+
CONF_TERMINAL = "Terminal"
|
|
166
168
|
CONF_LOGIN_RESPONSE = "login_response"
|
|
169
|
+
CONF_LOGIN_INFO = "login_info"
|
|
167
170
|
CONF_SELECTED_HOUSE = "selected_house"
|
|
168
171
|
CONF_DEVICE_LIST = "device_list"
|
|
169
172
|
CONF_PRODUCT_LIST = "product_list"
|
|
170
173
|
CONF_ACCESS_TOKEN = "accessToken"
|
|
174
|
+
CONF_REFRESH_TOKEN = "refreshToken"
|
|
175
|
+
CONF_TOKEN = "Token"
|
|
176
|
+
CONF_USERNAME = "username"
|
|
177
|
+
CONF_PASSWORD = "password"
|
|
178
|
+
CONF_REGION = "region"
|
|
179
|
+
CONF_COUNTRY = "country"
|
|
171
180
|
CONF_ID = "id"
|
|
172
181
|
CONF_NAME = "name"
|
|
173
182
|
CONF_PRODUCT_ID = "productId"
|
|
@@ -185,7 +194,7 @@ CONF_PROPERTIES = "properties"
|
|
|
185
194
|
CONF_MINVALUE = "minValue"
|
|
186
195
|
CONF_MAXVALUE = "maxValue"
|
|
187
196
|
CONF_IPADDRESS = "ipAddress"
|
|
188
|
-
|
|
197
|
+
CONF_CODE = "code"
|
|
189
198
|
class Identity(StrEnum):
|
|
190
199
|
"""Available entity identity."""
|
|
191
200
|
RGBW = "control.light.rgbw"
|
|
@@ -194,4 +203,9 @@ class Identity(StrEnum):
|
|
|
194
203
|
class Attribute(StrEnum):
|
|
195
204
|
"""Available entity attributes."""
|
|
196
205
|
RGBW = "rgbw"
|
|
197
|
-
CCT = "cct"
|
|
206
|
+
CCT = "cct"
|
|
207
|
+
|
|
208
|
+
class ServerErrorCode(IntEnum):
|
|
209
|
+
TOKEN_EXPIRED = 21026
|
|
210
|
+
LOGIN_INVALID = 21025
|
|
211
|
+
USER_PWD_INCORRECT = 560080
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""aidot Exceptions."""
|
|
2
|
+
class AidotError(Exception):
|
|
3
|
+
"""Aidot api exception."""
|
|
4
|
+
|
|
5
|
+
class InvalidURL(AidotError):
|
|
6
|
+
"""Invalid url exception."""
|
|
7
|
+
|
|
8
|
+
class HTTPError(AidotError):
|
|
9
|
+
"""Invalid host exception."""
|
|
10
|
+
|
|
11
|
+
class InvalidHost(AidotError):
|
|
12
|
+
"""Invalid host exception."""
|
|
13
|
+
|
|
14
|
+
class AidotAuthTokenExpired(AidotError):
|
|
15
|
+
"""Authentication failed because token is invalid or expired."""
|
|
16
|
+
|
|
17
|
+
class AidotAuthFailed(AidotError):
|
|
18
|
+
"""Authentication failed because MFA verification code is required."""
|
|
19
|
+
|
|
20
|
+
class AidotUserOrPassIncorrect(AidotError):
|
|
21
|
+
"""The password or email address is incorrect."""
|
|
22
|
+
|
|
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
|
|
File without changes
|
|
File without changes
|