python-aidot 0.2.8__tar.gz → 0.3.0__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.8
3
+ Version: 0.3.0
4
4
  Summary: aidot control wifi lights
5
5
  Home-page: https://github.com/Aidot-Development-Team/python-aidot
6
6
  Author: aidotdev2024
@@ -2,7 +2,7 @@
2
2
 
3
3
  import logging
4
4
  from typing import Any, Optional
5
- from aiohttp import ClientResponseError, ClientSession
5
+ from aiohttp import ClientSession
6
6
  import base64
7
7
  import aiohttp
8
8
  from cryptography.hazmat.backends import default_backend
@@ -25,6 +25,7 @@ from .const import (
25
25
  CONF_PASSWORD,
26
26
  CONF_CODE,
27
27
  CONF_TOKEN,
28
+ CONF_DEVICE_LIST,
28
29
  ServerErrorCode
29
30
  )
30
31
  from .exceptions import AidotAuthFailed,AidotUserOrPassIncorrect
@@ -207,4 +208,4 @@ class AidotClient:
207
208
  device[CONF_PRODUCT] = product
208
209
  except Exception as e:
209
210
  raise e
210
- return final_device_list
211
+ return {CONF_DEVICE_LIST:final_device_list}
@@ -0,0 +1,100 @@
1
+ import socket
2
+ import json
3
+ import time
4
+ import logging
5
+ from typing import Any
6
+
7
+ from .aes_utils import aes_encrypt,aes_decrypt
8
+ import asyncio
9
+ from .const import CONF_ID,CONF_IPADDRESS
10
+ from .exceptions import AidotOSError
11
+
12
+ _LOGGER = logging.getLogger(__name__)
13
+
14
+ class BroadcastProtocol:
15
+ _is_running: bool = True
16
+ def __init__(self,callback,user_id):
17
+ self.aes_key = bytearray(32)
18
+ key_string = "T54uednca587"
19
+ key_bytes = key_string.encode()
20
+ self.aes_key[:len(key_bytes)] = key_bytes
21
+
22
+ self._discover_cb = callback
23
+ self.user_id = user_id
24
+
25
+ def connection_made(self, transport):
26
+ self.transport = transport
27
+ sock = transport.get_extra_info("socket")
28
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
29
+
30
+ def send_broadcast(self) -> None:
31
+ current_timestamp_milliseconds = int(time.time() * 1000)
32
+ seq = str(current_timestamp_milliseconds + 1)[-9:]
33
+ message = {
34
+ "protocolVer":"2.0.0",
35
+ "service":"device",
36
+ "method":"devDiscoveryReq",
37
+ "seq": seq,
38
+ "srcAddr":f"0.{self.user_id}]",
39
+ "tst":current_timestamp_milliseconds,
40
+ "payload":{
41
+ "extends":{ },
42
+ "localCtrFlag":1,
43
+ "timestamp":str(current_timestamp_milliseconds)
44
+ }
45
+ }
46
+ send_data = aes_encrypt(json.dumps(message).encode(),self.aes_key)
47
+ self.transport.sendto(send_data, ('255.255.255.255', 6666))
48
+
49
+ def datagram_received(self, data, addr):
50
+ data_str = aes_decrypt(data,self.aes_key)
51
+ data_json = json.loads(data_str)
52
+ if("payload" in data_json):
53
+ if("mac" in data_json["payload"]):
54
+ devId = data_json["payload"]["devId"]
55
+ if self._discover_cb:
56
+ self._discover_cb(devId,{CONF_IPADDRESS : addr[0]})
57
+
58
+ def error_received(self, exc):
59
+ _LOGGER.error(f"Error occurred: {exc}")
60
+
61
+ def close(self) -> None:
62
+ self.transport.close()
63
+
64
+ def connection_lost(self, exc):
65
+ if exc:
66
+ _LOGGER.error(f"Connection lost due to error: {exc}")
67
+ else:
68
+ _LOGGER.info("Connection closed.")
69
+
70
+ class Discover:
71
+ _login_info: dict[str: Any] = None
72
+ _broadcast_protocol: BroadcastProtocol = None
73
+ _discovered_device: dict[str: str]
74
+ def __init__(self,login_info):
75
+ self._discovered_device = {}
76
+ self._login_info = login_info
77
+
78
+ async def fetch_devices_info(self) -> dict[str: str]:
79
+ if self._broadcast_protocol is None:
80
+ self._broadcast_protocol = BroadcastProtocol(self._discover_callback,self._login_info[CONF_ID])
81
+ try:
82
+ transport, protocol = await asyncio.get_event_loop().create_datagram_endpoint(
83
+ lambda: self._broadcast_protocol,
84
+ local_addr=("0.0.0.0", 0),
85
+ )
86
+ except OSError:
87
+ raise AidotOSError
88
+
89
+ self._broadcast_protocol.send_broadcast()
90
+ await asyncio.sleep(2)
91
+ return self._discovered_device
92
+
93
+ def _discover_callback(self, dev_id, event: dict[str, str]) -> None:
94
+ self._discovered_device[dev_id] = event[CONF_IPADDRESS]
95
+
96
+ def close(self) -> None:
97
+ if self._broadcast_protocol is not None:
98
+ self._broadcast_protocol.close()
99
+
100
+
@@ -15,8 +15,7 @@ class AidotAuthTokenExpired(AidotError):
15
15
  """Authentication failed because token is invalid or expired."""
16
16
 
17
17
  class AidotAuthFailed(AidotError):
18
- """Authentication failed because MFA verification code is required."""
18
+ """Authentication failed """
19
19
 
20
- class AidotUserOrPassIncorrect(AidotError):
21
- """The password or email address is incorrect."""
22
-
20
+ class AidotOSError(Exception):
21
+ """Aidot exception."""
@@ -4,9 +4,6 @@ import binascii
4
4
  import time
5
5
  from datetime import datetime
6
6
  import json
7
- import threading
8
- import colorsys
9
- from time import sleep
10
7
  import asyncio
11
8
  import logging
12
9
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: python-aidot
3
- Version: 0.2.8
3
+ Version: 0.3.0
4
4
  Summary: aidot control wifi lights
5
5
  Home-page: https://github.com/Aidot-Development-Team/python-aidot
6
6
  Author: aidotdev2024
@@ -10,8 +10,6 @@ aidot/discover.py
10
10
  aidot/exceptions.py
11
11
  aidot/lan.py
12
12
  aidot/login_const.py
13
- aidot/login_control.py
14
- aidot/login_data.py
15
13
  python_aidot.egg-info/PKG-INFO
16
14
  python_aidot.egg-info/SOURCES.txt
17
15
  python_aidot.egg-info/dependency_links.txt
@@ -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.8",
8
+ version="0.3.0",
9
9
  author="aidotdev2024",
10
10
  url='https://github.com/Aidot-Development-Team/python-aidot',
11
11
  description="aidot control wifi lights",
@@ -1,74 +0,0 @@
1
- import socket
2
- import binascii
3
- import threading
4
- import json
5
- import time
6
- import logging
7
-
8
- from .aes_utils import aes_encrypt,aes_decrypt
9
- import asyncio
10
-
11
- _LOGGER = logging.getLogger(__name__)
12
-
13
- class BroadcastProtocol:
14
-
15
- def __init__(self,callback,user_id):
16
- self.aes_key = bytearray(32)
17
- key_string = "T54uednca587"
18
- key_bytes = key_string.encode()
19
- self.aes_key[:len(key_bytes)] = key_bytes
20
-
21
- self._discoverCb = callback
22
- self.user_id = user_id
23
-
24
- def connection_made(self, transport):
25
- self.transport = transport
26
- sock = transport.get_extra_info("socket")
27
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
28
- self.discover_task = asyncio.create_task(self.do_discover())
29
-
30
- async def do_discover(self):
31
- while True:
32
- current_timestamp_milliseconds = int(time.time() * 1000)
33
- seq = str(current_timestamp_milliseconds + 1)[-9:]
34
- message = {
35
- "protocolVer":"2.0.0",
36
- "service":"device",
37
- "method":"devDiscoveryReq",
38
- "seq": seq,
39
- "srcAddr":f"0.{self.user_id}]",
40
- "tst":current_timestamp_milliseconds,
41
- "payload":{
42
- "extends":{ },
43
- "localCtrFlag":1,
44
- "timestamp":str(current_timestamp_milliseconds)
45
- }
46
- }
47
- send_data = aes_encrypt(json.dumps(message).encode(),self.aes_key)
48
- self.transport.sendto(send_data, ('255.255.255.255', 6666))
49
- await asyncio.sleep(3)
50
-
51
- def datagram_received(self, data, addr):
52
- data_str = aes_decrypt(data,self.aes_key)
53
- data_json = json.loads(data_str)
54
- if("payload" in data_json):
55
- if("mac" in data_json["payload"]):
56
- devId = data_json["payload"]["devId"]
57
- if self._discoverCb:
58
- self._discoverCb(devId,{"ipAddress" : addr[0]})
59
-
60
- def error_received(self, exc):
61
- _LOGGER.error(f"Error occurred: {exc}")
62
-
63
- class Discover:
64
-
65
- async def broadcast_message(self,callback,user_id):
66
- transport, protocol = await asyncio.get_event_loop().create_datagram_endpoint(
67
- lambda: BroadcastProtocol(callback,user_id),
68
- local_addr=("0.0.0.0", 0),
69
- )
70
-
71
-
72
-
73
-
74
-
@@ -1,191 +0,0 @@
1
- """The aidot integration."""
2
-
3
- from homeassistant.core import HomeAssistant
4
- import aiohttp
5
- import logging
6
- from .login_data import LoginData
7
-
8
- import base64
9
- from cryptography.hazmat.backends import default_backend
10
- from cryptography.hazmat.primitives import serialization
11
- from cryptography.hazmat.primitives.asymmetric import rsa
12
- from cryptography.hazmat.primitives.asymmetric import padding
13
- from cryptography.hazmat.primitives import hashes
14
-
15
- from .login_const import APP_ID, PUBLIC_KEY_PEM
16
- from .const import SUPPORTED_COUNTRYS
17
- _LOGGER = logging.getLogger(__name__)
18
-
19
- def rsa_password_encrypt(message: str):
20
- """Get password rsa encrypt."""
21
- public_key = serialization.load_pem_public_key(
22
- PUBLIC_KEY_PEM, backend=default_backend()
23
- )
24
-
25
- encrypted = public_key.encrypt(
26
- message.encode("utf-8"),
27
- padding.PKCS1v15(),
28
- )
29
-
30
- encrypted_base64 = base64.b64encode(encrypted).decode("utf-8")
31
-
32
- return encrypted_base64
33
-
34
-
35
- class LoginControl:
36
- _instance = None # singleton
37
-
38
- def __new__(cls, *args, **kwargs):
39
- if cls._instance is None:
40
- cls._instance = super().__new__(cls)
41
- return cls._instance
42
-
43
- def __init__(self) -> None:
44
- self.LoginData = LoginData()
45
- self.region = "us"
46
- self.username = ""
47
-
48
- def get_identifier(self,house_id: str) -> str:
49
- identifier = (
50
- "username:"
51
- + self.username
52
- + ","
53
- + "house_id:"
54
- + house_id
55
- + ","
56
- + "region:"
57
- + self.region
58
- )
59
- return identifier
60
-
61
- def change_country_code(self, selected_contry_obj: str):
62
- """Do change_country_code."""
63
- self.LoginData.baseUrl = (
64
- f"https://prod-{selected_contry_obj['region'].lower()}-api.arnoo.com/v17"
65
- )
66
-
67
- def change_country_name(self, selected_contry_name: str):
68
- selected_contry_obj = {}
69
- for item in SUPPORTED_COUNTRYS:
70
- if item["name"] == selected_contry_name:
71
- selected_contry_obj = item
72
- break
73
- selected_region = selected_contry_obj['region']
74
- if selected_region is not None:
75
- self.region = selected_region.lower()
76
- self.LoginData.baseUrl = (f"https://prod-{self.region}-api.arnoo.com/v17")
77
-
78
- async def async_get_products(
79
- self, hass: HomeAssistant, token: str, product_ids: str
80
- ):
81
- """Get device list."""
82
- url = f"{self.LoginData.baseUrl}/products/{product_ids}"
83
- headers = {
84
- "Terminal": "app",
85
- "Token": token,
86
- "Appid": APP_ID,
87
- }
88
-
89
- session = hass.helpers.aiohttp_client.async_get_clientsession()
90
-
91
- try:
92
- async with session.get(url, headers=headers) as response:
93
- response.raise_for_status()
94
- response_data = await response.json()
95
- return response_data
96
- except aiohttp.ClientError as e:
97
- _LOGGER.info("async_get_products ClientError {e}")
98
- return None
99
-
100
- async def async_get_devices(self, hass: HomeAssistant, token: str, house_id: str):
101
- """Get device list."""
102
-
103
- url = f"{self.LoginData.baseUrl}/devices?houseId={house_id}"
104
- headers = {
105
- "Terminal": "app",
106
- "Token": token,
107
- "Appid": APP_ID,
108
- }
109
-
110
- session = hass.helpers.aiohttp_client.async_get_clientsession()
111
-
112
- try:
113
- async with session.get(url, headers=headers) as response:
114
- response.raise_for_status()
115
- response_data = await response.json()
116
- return response_data
117
- except aiohttp.ClientError as e:
118
- _LOGGER.info("async_get_devices ClientError {e}")
119
- return None
120
-
121
- async def async_get_houses(self, hass: HomeAssistant, token: str):
122
- """Get house list."""
123
-
124
- url = f"{self.LoginData.baseUrl}/houses"
125
- headers = {
126
- "Terminal": "app",
127
- "Token": token,
128
- "Appid": APP_ID,
129
- }
130
-
131
- session = hass.helpers.aiohttp_client.async_get_clientsession()
132
-
133
- try:
134
- async with session.get(url, headers=headers) as response:
135
- response.raise_for_status()
136
- response_data = await response.json()
137
- return response_data
138
- except aiohttp.ClientError as e:
139
- _LOGGER.info("async_get_houses ClientError {e}")
140
- return None
141
-
142
- async def async_post_login(self, hass: HomeAssistant, username: str, password: str):
143
- """Login the user input allows us to connect."""
144
- self.username = username
145
- url = f"{self.LoginData.baseUrl}/users/loginWithFreeVerification"
146
- headers = {"Appid": APP_ID, "Terminal": "app"}
147
- data = {
148
- "countryKey": "region:UnitedStates",
149
- "username": username,
150
- "password": rsa_password_encrypt(password),
151
- "terminalId": "gvz3gjae10l4zii00t7y0",
152
- "webVersion": "0.5.0",
153
- "area": "Asia/Shanghai",
154
- "UTC": "UTC+8",
155
- }
156
- session = hass.helpers.aiohttp_client.async_get_clientsession()
157
-
158
- try:
159
- async with session.post(url, headers=headers, json=data) as response:
160
- response.raise_for_status()
161
- login_response = await response.json()
162
- return login_response
163
- except aiohttp.ClientError as e:
164
- _LOGGER.info("async_post_login ClientError {e}")
165
- return None
166
-
167
- async def async_get_all_login_info(
168
- self, hass: HomeAssistant, username: str, password: str
169
- ):
170
- """Get get all login info."""
171
- # login in
172
- login_response = await self.async_post_login(
173
- hass,
174
- username,
175
- password,
176
- )
177
- accessToken = login_response["accessToken"]
178
-
179
- # get houses
180
- default_house = await self.async_get_houses(hass, accessToken)
181
-
182
- # get device_list
183
- device_list = await self.async_get_devices(
184
- hass, accessToken, default_house["id"]
185
- )
186
-
187
- # get product_list
188
- productIds = ",".join([item["productId"] for item in device_list])
189
- product_list = await self.async_get_products(hass, accessToken, productIds)
190
-
191
- return (login_response, default_house, device_list, product_list)
@@ -1,16 +0,0 @@
1
- """The aidot integration."""
2
-
3
- from .login_const import BASE_URL
4
-
5
-
6
- class LoginData:
7
-
8
- _instance = None # singleton
9
-
10
- def __new__(cls, *args, **kwargs):
11
- if cls._instance is None:
12
- cls._instance = super().__new__(cls)
13
- return cls._instance
14
-
15
- def __init__(self) -> None:
16
- self.baseUrl = BASE_URL
File without changes
File without changes
File without changes