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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: python-aidot
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: aidot control wifi lights
5
5
  Home-page: https://github.com/Aidot-Development-Team/python-aidot
6
6
  Author: aidotdev2024
@@ -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
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: python-aidot
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: aidot control wifi lights
5
5
  Home-page: https://github.com/Aidot-Development-Team/python-aidot
6
6
  Author: aidotdev2024
@@ -4,8 +4,10 @@ setup.cfg
4
4
  setup.py
5
5
  aidot/__init__.py
6
6
  aidot/aes_utils.py
7
+ aidot/client.py
7
8
  aidot/const.py
8
9
  aidot/discover.py
10
+ aidot/exceptions.py
9
11
  aidot/lan.py
10
12
  aidot/login_const.py
11
13
  aidot/login_control.py
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="python-aidot",
8
- version="0.2.4",
8
+ version="0.2.6",
9
9
  author="aidotdev2024",
10
10
  url='https://github.com/Aidot-Development-Team/python-aidot',
11
11
  description="aidot control wifi lights",
File without changes
File without changes
File without changes
File without changes