python-aidot 0.1.0__py3-none-any.whl
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.
- aidot/__init__.py +0 -0
- aidot/aes_utils.py +25 -0
- aidot/discover.py +74 -0
- aidot/lan.py +292 -0
- aidot/login_const.py +14 -0
- aidot/login_control.py +164 -0
- aidot/login_data.py +16 -0
- python_aidot-0.1.0.dist-info/LICENSE +21 -0
- python_aidot-0.1.0.dist-info/METADATA +16 -0
- python_aidot-0.1.0.dist-info/RECORD +12 -0
- python_aidot-0.1.0.dist-info/WHEEL +5 -0
- python_aidot-0.1.0.dist-info/top_level.txt +1 -0
aidot/__init__.py
ADDED
|
File without changes
|
aidot/aes_utils.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
2
|
+
from cryptography.hazmat.backends import default_backend
|
|
3
|
+
from cryptography.hazmat.primitives import padding
|
|
4
|
+
|
|
5
|
+
def aes_encrypt(plaintext, key):
|
|
6
|
+
padder = padding.PKCS7(algorithms.AES.block_size).padder()
|
|
7
|
+
padded_data = padder.update(plaintext) + padder.finalize()
|
|
8
|
+
|
|
9
|
+
cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend())
|
|
10
|
+
encryptor = cipher.encryptor()
|
|
11
|
+
|
|
12
|
+
ciphertext = encryptor.update(padded_data) + encryptor.finalize()
|
|
13
|
+
|
|
14
|
+
return ciphertext
|
|
15
|
+
|
|
16
|
+
def aes_decrypt(ciphertext, key):
|
|
17
|
+
cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend())
|
|
18
|
+
decryptor = cipher.decryptor()
|
|
19
|
+
|
|
20
|
+
decrypted_data = decryptor.update(ciphertext) + decryptor.finalize()
|
|
21
|
+
|
|
22
|
+
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
|
|
23
|
+
plaintext = unpadder.update(decrypted_data) + unpadder.finalize()
|
|
24
|
+
|
|
25
|
+
return plaintext.decode()
|
aidot/discover.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
|
aidot/lan.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
import struct
|
|
3
|
+
import binascii
|
|
4
|
+
import time
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
import json
|
|
7
|
+
import threading
|
|
8
|
+
import colorsys
|
|
9
|
+
from time import sleep
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
from .aes_utils import aes_encrypt,aes_decrypt
|
|
14
|
+
_LOGGER = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
class Lan(object):
|
|
17
|
+
|
|
18
|
+
_is_on : bool = False
|
|
19
|
+
_dimming = 0
|
|
20
|
+
_rgdb : int
|
|
21
|
+
_cct : int
|
|
22
|
+
_login_uuid = 0
|
|
23
|
+
_available : bool = False
|
|
24
|
+
|
|
25
|
+
_connectAndLogin : bool = False
|
|
26
|
+
_connecting = False
|
|
27
|
+
_simpleVersion = ""
|
|
28
|
+
_colorMode = ""
|
|
29
|
+
|
|
30
|
+
def __init__(self,device:dict,user_info:dict) -> None:
|
|
31
|
+
self.ping_count = 0
|
|
32
|
+
|
|
33
|
+
if "id" in user_info:
|
|
34
|
+
self.user_id = user_info["id"]
|
|
35
|
+
|
|
36
|
+
if "aesKey" in device :
|
|
37
|
+
key_string = device["aesKey"][0]
|
|
38
|
+
if key_string is not None:
|
|
39
|
+
self.aes_key = bytearray(16)
|
|
40
|
+
key_bytes = key_string.encode()
|
|
41
|
+
self.aes_key[:len(key_bytes)] = key_bytes
|
|
42
|
+
|
|
43
|
+
if "password" in device:
|
|
44
|
+
self.password = device["password"]
|
|
45
|
+
|
|
46
|
+
if "id" in device:
|
|
47
|
+
self.device_id = device["id"]
|
|
48
|
+
|
|
49
|
+
if "simpleVersion" in device:
|
|
50
|
+
self._simpleVersion = device["simpleVersion"]
|
|
51
|
+
|
|
52
|
+
async def connect(self,ipAddress):
|
|
53
|
+
self.reader = self.writer = None
|
|
54
|
+
self._connecting = True
|
|
55
|
+
try:
|
|
56
|
+
self.reader, self.writer = await asyncio.open_connection(ipAddress, 10000)
|
|
57
|
+
sock: socket.socket = self.writer.get_extra_info("socket")
|
|
58
|
+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
59
|
+
self.seq_num = 1
|
|
60
|
+
await self.login()
|
|
61
|
+
self._connectAndLogin = True
|
|
62
|
+
except Exception as e:
|
|
63
|
+
self._connectAndLogin = False
|
|
64
|
+
finally:
|
|
65
|
+
self._connecting = False
|
|
66
|
+
|
|
67
|
+
def setUpdateDeviceCb(self,callback):
|
|
68
|
+
self._updateDeviceCb = callback
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def brightness(self) -> int:
|
|
72
|
+
return self._dimming * 255 / 100
|
|
73
|
+
|
|
74
|
+
def printfHex(self,packet):
|
|
75
|
+
hex_representation = binascii.hexlify(packet).decode()
|
|
76
|
+
|
|
77
|
+
def getSendPacket(self,message,msgtype):
|
|
78
|
+
magic = struct.pack('>H', 0x1eed)
|
|
79
|
+
_msgtype = struct.pack('>h', msgtype)
|
|
80
|
+
|
|
81
|
+
if self.aes_key is not None:
|
|
82
|
+
send_data = aes_encrypt(message,self.aes_key)
|
|
83
|
+
else :
|
|
84
|
+
send_data = message
|
|
85
|
+
|
|
86
|
+
bodysize = struct.pack('>i', len(send_data))
|
|
87
|
+
packet = magic + _msgtype + bodysize + send_data
|
|
88
|
+
|
|
89
|
+
return packet
|
|
90
|
+
|
|
91
|
+
async def login(self):
|
|
92
|
+
login_seq = str(int(time.time() * 1000) + self._login_uuid)[-9:]
|
|
93
|
+
self._login_uuid += 1
|
|
94
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
|
95
|
+
message = {
|
|
96
|
+
"service":"device",
|
|
97
|
+
"method":"loginReq",
|
|
98
|
+
"seq":login_seq,
|
|
99
|
+
"srcAddr":self.user_id,
|
|
100
|
+
"deviceId":self.device_id,
|
|
101
|
+
"payload":{
|
|
102
|
+
"userId":self.user_id,
|
|
103
|
+
"password":self.password,
|
|
104
|
+
"timestamp":timestamp,
|
|
105
|
+
"ascNumber":1
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
self.writer.write(self.getSendPacket(json.dumps(message).encode(),1))
|
|
109
|
+
await self.writer.drain()
|
|
110
|
+
|
|
111
|
+
data = await self.reader.read(1024)
|
|
112
|
+
data_len = len(data)
|
|
113
|
+
if(data_len <= 0):
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
magic, msgtype, bodysize = struct.unpack('>HHI', data[:8])
|
|
117
|
+
encrypted_data = data[8:]
|
|
118
|
+
if self.aes_key is not None:
|
|
119
|
+
decrypted_data = aes_decrypt(encrypted_data, self.aes_key)
|
|
120
|
+
else :
|
|
121
|
+
decrypted_data = encrypted_data
|
|
122
|
+
|
|
123
|
+
json_data = json.loads(decrypted_data)
|
|
124
|
+
|
|
125
|
+
self.ascNumber = json_data["payload"]["ascNumber"]
|
|
126
|
+
self.ascNumber += 1
|
|
127
|
+
|
|
128
|
+
self._available = True
|
|
129
|
+
|
|
130
|
+
await self.sendAction({},"getDevAttrReq")
|
|
131
|
+
|
|
132
|
+
async def recvData(self):
|
|
133
|
+
while True:
|
|
134
|
+
try :
|
|
135
|
+
data = await self.reader.read(1024)
|
|
136
|
+
except Exception as e:
|
|
137
|
+
_LOGGER.error(f"recv data error {e}")
|
|
138
|
+
await asyncio.sleep(3)
|
|
139
|
+
continue
|
|
140
|
+
data_len = len(data)
|
|
141
|
+
if(data_len <= 0):
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
magic, msgtype, bodysize = struct.unpack('>HHI', data[:8])
|
|
146
|
+
encrypted_data = data[8:]
|
|
147
|
+
decrypted_data = aes_decrypt(encrypted_data, self.aes_key)
|
|
148
|
+
|
|
149
|
+
json_data = json.loads(decrypted_data)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
_LOGGER.error(f"recv json error : {e}")
|
|
152
|
+
await asyncio.sleep(3)
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
if "service" in json_data:
|
|
156
|
+
if "test" == json_data["service"]:
|
|
157
|
+
self.ping_count = 0
|
|
158
|
+
|
|
159
|
+
if "payload" in json_data:
|
|
160
|
+
if "ascNumber" in json_data["payload"]:
|
|
161
|
+
self.ascNumber = json_data["payload"]["ascNumber"]
|
|
162
|
+
if "attr" in json_data["payload"]:
|
|
163
|
+
if "OnOff" in json_data["payload"]["attr"]:
|
|
164
|
+
self._is_on = json_data["payload"]["attr"]["OnOff"]
|
|
165
|
+
if "Dimming" in json_data["payload"]["attr"]:
|
|
166
|
+
self._dimming = json_data["payload"]["attr"]["Dimming"]
|
|
167
|
+
if "RGBW" in json_data["payload"]["attr"]:
|
|
168
|
+
self._rgdb = json_data["payload"]["attr"]["RGBW"]
|
|
169
|
+
self._colorMode = "rgbw"
|
|
170
|
+
if "CCT" in json_data["payload"]["attr"]:
|
|
171
|
+
self._cct = json_data["payload"]["attr"]["CCT"]
|
|
172
|
+
self._colorMode = "cct"
|
|
173
|
+
if self._updateDeviceCb:
|
|
174
|
+
await self._updateDeviceCb()
|
|
175
|
+
|
|
176
|
+
async def ping_task(self):
|
|
177
|
+
while True:
|
|
178
|
+
if await self.sendPingAction() == -1 :
|
|
179
|
+
return
|
|
180
|
+
await asyncio.sleep(10)
|
|
181
|
+
|
|
182
|
+
def getOnOffAction(self,OnOff):
|
|
183
|
+
self._is_on = OnOff
|
|
184
|
+
return {"OnOff": self._is_on}
|
|
185
|
+
|
|
186
|
+
def getDimingAction(self,brightness):
|
|
187
|
+
self._dimming = int(brightness * 100 / 255)
|
|
188
|
+
return {"Dimming": self._dimming}
|
|
189
|
+
|
|
190
|
+
def getCCTAction(self,cct):
|
|
191
|
+
self._cct = cct
|
|
192
|
+
self._colorMode = "cct"
|
|
193
|
+
return {"CCT": self._cct}
|
|
194
|
+
|
|
195
|
+
def getRGBWAction(self,rgbw):
|
|
196
|
+
self._rgdb = rgbw
|
|
197
|
+
self._colorMode = "rgbw"
|
|
198
|
+
return {"RGBW": rgbw}
|
|
199
|
+
|
|
200
|
+
async def sendDevAttr(self,devAttr):
|
|
201
|
+
await self.sendAction(devAttr,"setDevAttrReq")
|
|
202
|
+
|
|
203
|
+
async def sendAction(self,attr,method):
|
|
204
|
+
|
|
205
|
+
current_timestamp_milliseconds = int(time.time() * 1000)
|
|
206
|
+
|
|
207
|
+
self.seq_num += 1
|
|
208
|
+
|
|
209
|
+
seq = "ha93" + str(self.seq_num).zfill(5)
|
|
210
|
+
|
|
211
|
+
if not self._is_on and not "OnOff" in attr:
|
|
212
|
+
attr["OnOff"] = 1
|
|
213
|
+
self._is_on = 1
|
|
214
|
+
|
|
215
|
+
if self._simpleVersion is not None:
|
|
216
|
+
action = {
|
|
217
|
+
"method": method,
|
|
218
|
+
"service": "device",
|
|
219
|
+
"clientId": "ha-" + self.user_id,
|
|
220
|
+
"srcAddr": "0." + self.user_id,
|
|
221
|
+
"seq": "" + seq,
|
|
222
|
+
"payload": {
|
|
223
|
+
"devId": self.device_id,
|
|
224
|
+
"parentId": self.device_id,
|
|
225
|
+
"userId": self.user_id,
|
|
226
|
+
"password": self.password,
|
|
227
|
+
"attr": attr,
|
|
228
|
+
"channel":"tcp",
|
|
229
|
+
"ascNumber":self.ascNumber,
|
|
230
|
+
},
|
|
231
|
+
"tst": current_timestamp_milliseconds,
|
|
232
|
+
# "tid": "homeassistant",
|
|
233
|
+
"deviceId": self.device_id,
|
|
234
|
+
}
|
|
235
|
+
else :
|
|
236
|
+
action = {
|
|
237
|
+
"method": method,
|
|
238
|
+
"service": "device",
|
|
239
|
+
"seq": "" + seq,
|
|
240
|
+
"srcAddr": "0." + self.user_id,
|
|
241
|
+
"payload": {
|
|
242
|
+
"attr": attr,
|
|
243
|
+
"ascNumber":self.ascNumber,
|
|
244
|
+
},
|
|
245
|
+
"tst": current_timestamp_milliseconds,
|
|
246
|
+
# "tid": "homeassistant",
|
|
247
|
+
"deviceId": self.device_id,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
self.writer.write(self.getSendPacket(json.dumps(action).encode(),1))
|
|
252
|
+
await self.writer.drain()
|
|
253
|
+
except BrokenPipeError as e :
|
|
254
|
+
_LOGGER.error(f"{self.device_id} send action error {e}")
|
|
255
|
+
except Exception as e:
|
|
256
|
+
_LOGGER.error(f"{self.device_id} send action error {e}")
|
|
257
|
+
|
|
258
|
+
async def sendPingAction(self):
|
|
259
|
+
ping = {
|
|
260
|
+
"service": "test",
|
|
261
|
+
"method": "pingreq",
|
|
262
|
+
"seq": "123456",
|
|
263
|
+
"srcAddr": "x.xxxxxxx",
|
|
264
|
+
"payload": {}
|
|
265
|
+
}
|
|
266
|
+
try:
|
|
267
|
+
if self.ping_count >= 2 :
|
|
268
|
+
_LOGGER.error(f"Last ping did not return within 20 seconds. device id:{self.device_id}")
|
|
269
|
+
await self.reset()
|
|
270
|
+
return -1
|
|
271
|
+
self.writer.write(self.getSendPacket(json.dumps(ping).encode(),2))
|
|
272
|
+
await self.writer.drain()
|
|
273
|
+
self.ping_count += 1
|
|
274
|
+
return 1
|
|
275
|
+
except Exception as e:
|
|
276
|
+
_LOGGER.error(f"{self.device_id} ping error {e}")
|
|
277
|
+
await self.reset()
|
|
278
|
+
return -1
|
|
279
|
+
|
|
280
|
+
async def reset(self):
|
|
281
|
+
try:
|
|
282
|
+
if self.writer:
|
|
283
|
+
self.writer.close()
|
|
284
|
+
await self.writer.wait_closed()
|
|
285
|
+
except Exception as e:
|
|
286
|
+
_LOGGER.error(f"{self.device_id} writer close error {e}")
|
|
287
|
+
self._connectAndLogin = False
|
|
288
|
+
self._available = False
|
|
289
|
+
self.ping_count = 0
|
|
290
|
+
if self._updateDeviceCb:
|
|
291
|
+
await self._updateDeviceCb()
|
|
292
|
+
|
aidot/login_const.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Constants for the aidot integration."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
APP_ID = "1383974540041977857"
|
|
5
|
+
BASE_URL = "https://prod-us-api.arnoo.com/v17"
|
|
6
|
+
|
|
7
|
+
PUBLIC_KEY_PEM = b"""
|
|
8
|
+
-----BEGIN PUBLIC KEY-----
|
|
9
|
+
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCtQAnPCi8ksPnS1Du6z96PsKfN
|
|
10
|
+
p2Gp/f/bHwlrAdplbX3p7/TnGpnbJGkLq8uRxf6cw+vOthTsZjkPCF7CatRvRnTj
|
|
11
|
+
c9fcy7yE0oXa5TloYyXD6GkxgftBbN/movkJJGQCc7gFavuYoAdTRBOyQoXBtm0m
|
|
12
|
+
kXMSjXOldI/290b9BQIDAQAB
|
|
13
|
+
-----END PUBLIC KEY-----
|
|
14
|
+
"""
|
aidot/login_control.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
_LOGGER = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
def rsa_password_encrypt(message: str):
|
|
19
|
+
"""Get password rsa encrypt."""
|
|
20
|
+
public_key = serialization.load_pem_public_key(
|
|
21
|
+
PUBLIC_KEY_PEM, backend=default_backend()
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
encrypted = public_key.encrypt(
|
|
25
|
+
message.encode("utf-8"),
|
|
26
|
+
padding.PKCS1v15(),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
encrypted_base64 = base64.b64encode(encrypted).decode("utf-8")
|
|
30
|
+
|
|
31
|
+
return encrypted_base64
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LoginControl:
|
|
35
|
+
_instance = None # singleton
|
|
36
|
+
|
|
37
|
+
def __new__(cls, *args, **kwargs):
|
|
38
|
+
if cls._instance is None:
|
|
39
|
+
cls._instance = super().__new__(cls)
|
|
40
|
+
return cls._instance
|
|
41
|
+
|
|
42
|
+
def __init__(self) -> None:
|
|
43
|
+
self.LoginData = LoginData()
|
|
44
|
+
|
|
45
|
+
def change_country_code(self, selected_contry_obj: str):
|
|
46
|
+
"""Do change_country_code."""
|
|
47
|
+
self.LoginData.baseUrl = (
|
|
48
|
+
f"https://prod-{selected_contry_obj['region'].lower()}-api.arnoo.com/v17"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
async def async_get_products(
|
|
52
|
+
self, hass: HomeAssistant, token: str, product_ids: str
|
|
53
|
+
):
|
|
54
|
+
"""Get device list."""
|
|
55
|
+
url = f"{self.LoginData.baseUrl}/products/{product_ids}"
|
|
56
|
+
headers = {
|
|
57
|
+
"Terminal": "app",
|
|
58
|
+
"Token": token,
|
|
59
|
+
"Appid": APP_ID,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
session = hass.helpers.aiohttp_client.async_get_clientsession()
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
async with session.get(url, headers=headers) as response:
|
|
66
|
+
response.raise_for_status()
|
|
67
|
+
response_data = await response.json()
|
|
68
|
+
return response_data
|
|
69
|
+
except aiohttp.ClientError as e:
|
|
70
|
+
_LOGGER.info("async_get_products ClientError {e}")
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
async def async_get_devices(self, hass: HomeAssistant, token: str, house_id: str):
|
|
74
|
+
"""Get device list."""
|
|
75
|
+
|
|
76
|
+
url = f"{self.LoginData.baseUrl}/devices?houseId={house_id}"
|
|
77
|
+
headers = {
|
|
78
|
+
"Terminal": "app",
|
|
79
|
+
"Token": token,
|
|
80
|
+
"Appid": APP_ID,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
session = hass.helpers.aiohttp_client.async_get_clientsession()
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
async with session.get(url, headers=headers) as response:
|
|
87
|
+
response.raise_for_status()
|
|
88
|
+
response_data = await response.json()
|
|
89
|
+
return response_data
|
|
90
|
+
except aiohttp.ClientError as e:
|
|
91
|
+
_LOGGER.info("async_get_devices ClientError {e}")
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
async def async_get_houses(self, hass: HomeAssistant, token: str):
|
|
95
|
+
"""Get house list."""
|
|
96
|
+
|
|
97
|
+
url = f"{self.LoginData.baseUrl}/houses"
|
|
98
|
+
headers = {
|
|
99
|
+
"Terminal": "app",
|
|
100
|
+
"Token": token,
|
|
101
|
+
"Appid": APP_ID,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
session = hass.helpers.aiohttp_client.async_get_clientsession()
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
async with session.get(url, headers=headers) as response:
|
|
108
|
+
response.raise_for_status()
|
|
109
|
+
response_data = await response.json()
|
|
110
|
+
return response_data
|
|
111
|
+
except aiohttp.ClientError as e:
|
|
112
|
+
_LOGGER.info("async_get_houses ClientError {e}")
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
async def async_post_login(self, hass: HomeAssistant, username: str, password: str):
|
|
116
|
+
"""Login the user input allows us to connect."""
|
|
117
|
+
|
|
118
|
+
url = f"{self.LoginData.baseUrl}/users/loginWithFreeVerification"
|
|
119
|
+
headers = {"Appid": APP_ID, "Terminal": "app"}
|
|
120
|
+
data = {
|
|
121
|
+
"countryKey": "region:UnitedStates",
|
|
122
|
+
"username": username,
|
|
123
|
+
"password": rsa_password_encrypt(password),
|
|
124
|
+
"terminalId": "gvz3gjae10l4zii00t7y0",
|
|
125
|
+
"webVersion": "0.5.0",
|
|
126
|
+
"area": "Asia/Shanghai",
|
|
127
|
+
"UTC": "UTC+8",
|
|
128
|
+
}
|
|
129
|
+
session = hass.helpers.aiohttp_client.async_get_clientsession()
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
async with session.post(url, headers=headers, json=data) as response:
|
|
133
|
+
response.raise_for_status()
|
|
134
|
+
login_response = await response.json()
|
|
135
|
+
return login_response
|
|
136
|
+
except aiohttp.ClientError as e:
|
|
137
|
+
_LOGGER.info("async_post_login ClientError {e}")
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
async def async_get_all_login_info(
|
|
141
|
+
self, hass: HomeAssistant, username: str, password: str
|
|
142
|
+
):
|
|
143
|
+
"""Get get all login info."""
|
|
144
|
+
# login in
|
|
145
|
+
login_response = await self.async_post_login(
|
|
146
|
+
hass,
|
|
147
|
+
username,
|
|
148
|
+
password,
|
|
149
|
+
)
|
|
150
|
+
accessToken = login_response["accessToken"]
|
|
151
|
+
|
|
152
|
+
# get houses
|
|
153
|
+
default_house = await self.async_get_houses(hass, accessToken)
|
|
154
|
+
|
|
155
|
+
# get device_list
|
|
156
|
+
device_list = await self.async_get_devices(
|
|
157
|
+
hass, accessToken, default_house["id"]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# get product_list
|
|
161
|
+
productIds = ",".join([item["productId"] for item in device_list])
|
|
162
|
+
product_list = await self.async_get_products(hass, accessToken, productIds)
|
|
163
|
+
|
|
164
|
+
return (login_response, default_house, device_list, product_list)
|
aidot/login_data.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015-2024 Fabian Affolter <fabian@affolter-engineering.ch>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: python-aidot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: aidot control wifi lights
|
|
5
|
+
Author: aidotdev2024
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: requests
|
|
12
|
+
Requires-Dist: aiohttp
|
|
13
|
+
Requires-Dist: setuptools
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
aidot is a Python library to control the aidot Wi-Fi Lights with home assistant.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
aidot/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
aidot/aes_utils.py,sha256=lCOJ0VXx9REn6ot1jCFK8o-oY-fQ0amcDAbOFwqauu0,950
|
|
3
|
+
aidot/discover.py,sha256=lict_jTNiSRg57Lo_6uixqklUQy0H34G5z46Xgf_hoE,2359
|
|
4
|
+
aidot/lan.py,sha256=g2vjwZViS9EeM7AYc3-mN0ZJ8iXx0TDaNm_IKEzkiVo,9527
|
|
5
|
+
aidot/login_const.py,sha256=-bQBeq_cqGM5IT53hSlvr1SJ2WC8G7NHE7EzI63OXrY,436
|
|
6
|
+
aidot/login_control.py,sha256=WkvdTKJjwuyGSSorTwkLD-rRRy5glwADRVcFo2FsuiA,5610
|
|
7
|
+
aidot/login_data.py,sha256=bw417da8bkd71KnAvlPEgmSgwvjiI3b23mqmyOq3Tfc,351
|
|
8
|
+
python_aidot-0.1.0.dist-info/LICENSE,sha256=EZzvMisn9C0Z2kwzKMqGE_xljuPxjA9bwUetwX_1nwM,1141
|
|
9
|
+
python_aidot-0.1.0.dist-info/METADATA,sha256=nh28oHlMcW_ohQU4JbpXs0Jom7ScT-_rTldBEzm4y8U,495
|
|
10
|
+
python_aidot-0.1.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
11
|
+
python_aidot-0.1.0.dist-info/top_level.txt,sha256=_doNL2OOnXeinm1X72eH2wz26wAkZwbM47KhNB6_QzI,6
|
|
12
|
+
python_aidot-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aidot
|