python-aidot 0.2.9__tar.gz → 0.3.1__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.9/python_aidot.egg-info → python_aidot-0.3.1}/PKG-INFO +1 -1
- {python_aidot-0.2.9 → python_aidot-0.3.1}/aidot/client.py +1 -1
- python_aidot-0.3.1/aidot/discover.py +100 -0
- {python_aidot-0.2.9 → python_aidot-0.3.1}/aidot/exceptions.py +5 -3
- {python_aidot-0.2.9 → python_aidot-0.3.1}/aidot/lan.py +0 -3
- {python_aidot-0.2.9 → python_aidot-0.3.1/python_aidot.egg-info}/PKG-INFO +1 -1
- {python_aidot-0.2.9 → python_aidot-0.3.1}/python_aidot.egg-info/SOURCES.txt +0 -2
- {python_aidot-0.2.9 → python_aidot-0.3.1}/setup.py +1 -1
- python_aidot-0.2.9/aidot/discover.py +0 -74
- python_aidot-0.2.9/aidot/login_control.py +0 -191
- python_aidot-0.2.9/aidot/login_data.py +0 -16
- {python_aidot-0.2.9 → python_aidot-0.3.1}/LICENSE +0 -0
- {python_aidot-0.2.9 → python_aidot-0.3.1}/README.md +0 -0
- {python_aidot-0.2.9 → python_aidot-0.3.1}/aidot/__init__.py +0 -0
- {python_aidot-0.2.9 → python_aidot-0.3.1}/aidot/aes_utils.py +0 -0
- {python_aidot-0.2.9 → python_aidot-0.3.1}/aidot/const.py +0 -0
- {python_aidot-0.2.9 → python_aidot-0.3.1}/aidot/login_const.py +0 -0
- {python_aidot-0.2.9 → python_aidot-0.3.1}/python_aidot.egg-info/dependency_links.txt +0 -0
- {python_aidot-0.2.9 → python_aidot-0.3.1}/python_aidot.egg-info/requires.txt +0 -0
- {python_aidot-0.2.9 → python_aidot-0.3.1}/python_aidot.egg-info/top_level.txt +0 -0
- {python_aidot-0.2.9 → python_aidot-0.3.1}/setup.cfg +0 -0
|
@@ -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,10 @@ class AidotAuthTokenExpired(AidotError):
|
|
|
15
15
|
"""Authentication failed because token is invalid or expired."""
|
|
16
16
|
|
|
17
17
|
class AidotAuthFailed(AidotError):
|
|
18
|
-
"""Authentication failed
|
|
18
|
+
"""Authentication failed"""
|
|
19
19
|
|
|
20
20
|
class AidotUserOrPassIncorrect(AidotError):
|
|
21
|
-
"""
|
|
22
|
-
|
|
21
|
+
"""Authentication failed"""
|
|
22
|
+
|
|
23
|
+
class AidotOSError(Exception):
|
|
24
|
+
"""Aidot exception."""
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|