python-aidot 0.3.54b2__tar.gz → 0.3.54b4__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.3.54b2 → python_aidot-0.3.54b4}/PKG-INFO +1 -1
- {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/aidot/aes_utils.py +19 -0
- {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/aidot/client.py +5 -1
- {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/aidot/device_client.py +68 -96
- {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/aidot/discover.py +16 -32
- python_aidot-0.3.54b4/aidot/models/__init__.py +37 -0
- python_aidot-0.3.54b4/aidot/models/device_client_model.py +210 -0
- python_aidot-0.3.54b4/aidot/models/device_model.py +83 -0
- python_aidot-0.3.54b4/aidot/models/discover_model.py +102 -0
- {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/python_aidot.egg-info/PKG-INFO +1 -1
- {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/python_aidot.egg-info/SOURCES.txt +4 -0
- {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/setup.py +1 -1
- {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/LICENSE +0 -0
- {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/README.md +0 -0
- {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/aidot/__init__.py +0 -0
- {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/aidot/const.py +0 -0
- {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/aidot/exceptions.py +0 -0
- {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/aidot/login_const.py +0 -0
- {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/python_aidot.egg-info/dependency_links.txt +0 -0
- {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/python_aidot.egg-info/requires.txt +0 -0
- {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/python_aidot.egg-info/top_level.txt +0 -0
- {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/setup.cfg +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
2
2
|
from cryptography.hazmat.backends import default_backend
|
|
3
3
|
from cryptography.hazmat.primitives import padding
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any, Optional
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
def aes_encrypt(plaintext, key):
|
|
@@ -25,3 +27,20 @@ def aes_decrypt(ciphertext, key):
|
|
|
25
27
|
plaintext = unpadder.update(decrypted_data) + unpadder.finalize()
|
|
26
28
|
|
|
27
29
|
return plaintext.decode()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def aes_decrypt_to_json(ciphertext: bytes, key: Optional[bytes] = None) -> dict[str, Any]:
|
|
33
|
+
"""Decrypt AES encrypted data and parse to JSON.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
ciphertext: AES encrypted data
|
|
37
|
+
key: AES key (optional, if None, assumes data is already decrypted)
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Parsed JSON dict
|
|
41
|
+
"""
|
|
42
|
+
if key:
|
|
43
|
+
decrypted_data = aes_decrypt(ciphertext, key)
|
|
44
|
+
else:
|
|
45
|
+
decrypted_data = ciphertext.decode() if isinstance(ciphertext, bytes) else ciphertext
|
|
46
|
+
return json.loads(decrypted_data)
|
|
@@ -36,6 +36,8 @@ from .const import (
|
|
|
36
36
|
SUPPORTED_COUNTRYS,
|
|
37
37
|
DEFAULT_COUNTRY_CODE,
|
|
38
38
|
CONF_IS_OWNER,
|
|
39
|
+
CONF_TYPE,
|
|
40
|
+
CONF_AES_KEY,
|
|
39
41
|
ServerErrorCode,
|
|
40
42
|
)
|
|
41
43
|
|
|
@@ -77,7 +79,7 @@ class AidotClient:
|
|
|
77
79
|
password: str | None = None,
|
|
78
80
|
token: dict | None = None,
|
|
79
81
|
) -> None:
|
|
80
|
-
_LOGGER.info("Client Version: v0.3.
|
|
82
|
+
_LOGGER.info("Client Version: v0.3.54b3")
|
|
81
83
|
self.session = session
|
|
82
84
|
self.username = username
|
|
83
85
|
self.password = password
|
|
@@ -258,6 +260,7 @@ class AidotClient:
|
|
|
258
260
|
for device in final_device_list:
|
|
259
261
|
if device[CONF_PRODUCT_ID] == product[CONF_ID]:
|
|
260
262
|
device[CONF_PRODUCT] = product
|
|
263
|
+
|
|
261
264
|
except Exception as e:
|
|
262
265
|
raise e
|
|
263
266
|
return {CONF_DEVICE_LIST: final_device_list}
|
|
@@ -286,6 +289,7 @@ class AidotClient:
|
|
|
286
289
|
if self._discover is not None:
|
|
287
290
|
return
|
|
288
291
|
|
|
292
|
+
_LOGGER.warning(f"setup_discover")
|
|
289
293
|
def _discover_callback(dev_id, event: dict[str, str]) -> None:
|
|
290
294
|
device_ip = event[CONF_IPADDRESS]
|
|
291
295
|
device_client: DeviceClient = self._device_clients.get(dev_id)
|
|
@@ -11,7 +11,8 @@ from datetime import datetime
|
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
13
|
from .exceptions import AidotNotLogin
|
|
14
|
-
from .aes_utils import aes_encrypt,
|
|
14
|
+
from .aes_utils import aes_encrypt, aes_decrypt_to_json
|
|
15
|
+
from .models.device_client_model import PingRequest, LoginRequest, LoginPayload, DeviceResponse, DeviceAttr, DeviceActionRequest
|
|
15
16
|
from .const import (
|
|
16
17
|
CONF_AES_KEY,
|
|
17
18
|
CONF_ASCNUMBER,
|
|
@@ -51,15 +52,16 @@ class DeviceStatusData:
|
|
|
51
52
|
cct: int = 2700
|
|
52
53
|
dimming: int = 100
|
|
53
54
|
|
|
54
|
-
def update(self, attr:
|
|
55
|
+
def update(self, attr: DeviceAttr) -> None:
|
|
56
|
+
"""Update status from DeviceAttr model."""
|
|
55
57
|
if attr is None:
|
|
56
58
|
return
|
|
57
|
-
if attr.
|
|
58
|
-
self.on = attr.
|
|
59
|
-
if attr.
|
|
60
|
-
self.dimming = int(attr.
|
|
61
|
-
if attr.
|
|
62
|
-
rgbw_value = attr.
|
|
59
|
+
if attr.OnOff is not None:
|
|
60
|
+
self.on = attr.OnOff
|
|
61
|
+
if attr.Dimming is not None:
|
|
62
|
+
self.dimming = int(attr.Dimming * 255 / 100)
|
|
63
|
+
if attr.RGBW is not None:
|
|
64
|
+
rgbw_value = attr.RGBW
|
|
63
65
|
# If RGBW is 0, set default red color (255, 0, 0, 0)
|
|
64
66
|
if rgbw_value == 0:
|
|
65
67
|
self.rgdb = 0xFF000000 # Red in int: 4278190080
|
|
@@ -72,8 +74,8 @@ class DeviceStatusData:
|
|
|
72
74
|
b = (rgbw >> 8) & 0xFF
|
|
73
75
|
w = rgbw & 0xFF
|
|
74
76
|
self.rgbw = (r, g, b, w)
|
|
75
|
-
if attr.
|
|
76
|
-
self.cct = attr.
|
|
77
|
+
if attr.CCT is not None:
|
|
78
|
+
self.cct = attr.CCT
|
|
77
79
|
|
|
78
80
|
|
|
79
81
|
class DeviceInformation:
|
|
@@ -123,8 +125,8 @@ class DeviceClient(object):
|
|
|
123
125
|
writer: Any = None
|
|
124
126
|
reader: Any = None
|
|
125
127
|
syncProperties = [CONF_ON_OFF, CONF_DIMMING, CONF_RGBW, CONF_CCT]
|
|
126
|
-
heart_time =
|
|
127
|
-
|
|
128
|
+
heart_time = 30
|
|
129
|
+
ping_data = PingRequest().to_dict()
|
|
128
130
|
_TAG: str = "DeviceClient"
|
|
129
131
|
@property
|
|
130
132
|
def connect_and_login(self) -> bool:
|
|
@@ -151,9 +153,10 @@ class DeviceClient(object):
|
|
|
151
153
|
self.device_id = device.get(CONF_ID)
|
|
152
154
|
self._simpleVersion = device.get("simpleVersion")
|
|
153
155
|
self._TAG = f"{self.device_id}"
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
if self.info.model_id == 'lk.WIFI-RGBWLight-D0006':
|
|
157
|
+
self.ping_data = None
|
|
158
|
+
self.heart_time = 10
|
|
159
|
+
|
|
157
160
|
_LOGGER.warning(f"{self._TAG}:{device}")
|
|
158
161
|
|
|
159
162
|
async def connect(self, ip_address) -> None:
|
|
@@ -209,37 +212,34 @@ class DeviceClient(object):
|
|
|
209
212
|
async def login(self) -> None:
|
|
210
213
|
login_seq = str(int(time.time() * 1000) + self._login_uuid)[-9:]
|
|
211
214
|
self._login_uuid += 1
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
"ascNumber": 1,
|
|
224
|
-
},
|
|
225
|
-
}
|
|
215
|
+
|
|
216
|
+
login_request = LoginRequest(
|
|
217
|
+
seq=login_seq,
|
|
218
|
+
srcAddr=self.user_id,
|
|
219
|
+
deviceId=self.device_id,
|
|
220
|
+
payload=LoginPayload(
|
|
221
|
+
userId=self.user_id,
|
|
222
|
+
password=self.password,
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
message = login_request.to_dict()
|
|
226
226
|
try:
|
|
227
227
|
self.writer.write(self.get_send_packet(json.dumps(message).encode(), 1))
|
|
228
228
|
await self.writer.drain()
|
|
229
229
|
header = await self.reader.readexactly(8)
|
|
230
230
|
magic, msgtype, bodysize = struct.unpack(">HHI", header)
|
|
231
231
|
body = await self.reader.readexactly(bodysize)
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if code != 200:
|
|
232
|
+
json_data = aes_decrypt_to_json(body, self.aes_key)
|
|
233
|
+
_LOGGER.warning(f"{self._TAG}:login result: {json_data}")
|
|
234
|
+
|
|
235
|
+
response = DeviceResponse.from_json(json_data)
|
|
236
|
+
if response.ack.code != 200:
|
|
237
237
|
# 登录失败
|
|
238
|
-
_LOGGER.error(f"{self._TAG}:login error, code: {code}")
|
|
238
|
+
_LOGGER.error(f"{self._TAG}:login error, code: {response.ack.code}")
|
|
239
239
|
await self.reset()
|
|
240
240
|
return
|
|
241
241
|
|
|
242
|
-
self.ascNumber =
|
|
242
|
+
self.ascNumber = response.payload.ascNumber + 1
|
|
243
243
|
self.status.online = True
|
|
244
244
|
self._notify_status_update()
|
|
245
245
|
self._receive_task = asyncio.create_task(
|
|
@@ -265,8 +265,7 @@ class DeviceClient(object):
|
|
|
265
265
|
magic, msgtype, bodysize = struct.unpack(">HHI", header)
|
|
266
266
|
self.ping_count = 0 #有读到数据就把ping清零
|
|
267
267
|
body = await self.reader.readexactly(bodysize)
|
|
268
|
-
|
|
269
|
-
json_data = json.loads(decrypted_data)
|
|
268
|
+
json_data = aes_decrypt_to_json(body, self.aes_key)
|
|
270
269
|
_LOGGER.warning(f"{self._TAG}:reveive_data : {json_data}")
|
|
271
270
|
except asyncio.CancelledError:
|
|
272
271
|
_LOGGER.debug(f"{self._TAG}:Receive task cancelled")
|
|
@@ -279,19 +278,18 @@ class DeviceClient(object):
|
|
|
279
278
|
return
|
|
280
279
|
except Exception as e:
|
|
281
280
|
_LOGGER.error(f"{self._TAG}:recv error: {e}")
|
|
282
|
-
self.ping_count = 0
|
|
283
281
|
continue
|
|
284
282
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
283
|
+
response = DeviceResponse.from_json(json_data)
|
|
284
|
+
if response.service == "test":
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
if response.payload:
|
|
288
|
+
if response.payload.ascNumber:
|
|
289
|
+
self.ascNumber = response.payload.ascNumber
|
|
290
|
+
if response.payload.attr:
|
|
291
|
+
self.status.update(response.payload.attr)
|
|
292
|
+
self._notify_status_update()
|
|
295
293
|
|
|
296
294
|
def _schedule_ping(self):
|
|
297
295
|
loop = asyncio.get_running_loop()
|
|
@@ -324,42 +322,19 @@ class DeviceClient(object):
|
|
|
324
322
|
await self.send_dev_attr({CONF_CCT: cct})
|
|
325
323
|
|
|
326
324
|
async def send_action(self, attr, method) -> None:
|
|
327
|
-
current_timestamp_milliseconds = int(time.time() * 1000)
|
|
328
325
|
self.seq_num += 1
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
"userId": self.user_id,
|
|
342
|
-
"password": self.password,
|
|
343
|
-
"attr": attr,
|
|
344
|
-
"channel": "tcp",
|
|
345
|
-
"ascNumber": self.ascNumber,
|
|
346
|
-
},
|
|
347
|
-
"tst": current_timestamp_milliseconds,
|
|
348
|
-
"deviceId": self.device_id,
|
|
349
|
-
}
|
|
350
|
-
else:
|
|
351
|
-
action = {
|
|
352
|
-
"method": method,
|
|
353
|
-
"service": "device",
|
|
354
|
-
"seq": "" + seq,
|
|
355
|
-
"srcAddr": "0." + self.user_id,
|
|
356
|
-
CONF_PAYLOAD: {
|
|
357
|
-
"attr": attr,
|
|
358
|
-
"ascNumber": self.ascNumber,
|
|
359
|
-
},
|
|
360
|
-
"tst": current_timestamp_milliseconds,
|
|
361
|
-
"deviceId": self.device_id,
|
|
362
|
-
}
|
|
326
|
+
action_request = DeviceActionRequest.from_params(
|
|
327
|
+
method=method,
|
|
328
|
+
user_id=self.user_id,
|
|
329
|
+
device_id=self.device_id,
|
|
330
|
+
password=self.password,
|
|
331
|
+
ascNumber=self.ascNumber,
|
|
332
|
+
attr=attr,
|
|
333
|
+
seq="ha93" + str(self.seq_num).zfill(5),
|
|
334
|
+
simpleVersion=self._simpleVersion,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
action = action_request.to_dict()
|
|
363
338
|
_LOGGER.warning(f"{self.device_id} send_action {action}")
|
|
364
339
|
try:
|
|
365
340
|
self.writer.write(self.get_send_packet(json.dumps(action).encode(), 1))
|
|
@@ -373,14 +348,6 @@ class DeviceClient(object):
|
|
|
373
348
|
async def send_ping_action(self) -> int:
|
|
374
349
|
if self._is_close:
|
|
375
350
|
return -1
|
|
376
|
-
ping = {
|
|
377
|
-
"service": "test",
|
|
378
|
-
"method": "pingreq",
|
|
379
|
-
"seq": "123456",
|
|
380
|
-
"srcAddr": "123456",
|
|
381
|
-
CONF_PAYLOAD: {},
|
|
382
|
-
}
|
|
383
|
-
_LOGGER.warning(f"{self.device_id} send_ping_action {ping}")
|
|
384
351
|
try:
|
|
385
352
|
if self.ping_count >= 3:
|
|
386
353
|
_LOGGER.error(
|
|
@@ -390,10 +357,15 @@ class DeviceClient(object):
|
|
|
390
357
|
return -1
|
|
391
358
|
if self._connect_and_login is False:
|
|
392
359
|
return -1
|
|
393
|
-
|
|
394
|
-
# await self.writer.drain()
|
|
360
|
+
|
|
395
361
|
self.ping_count += 1
|
|
396
|
-
|
|
362
|
+
if self.ping_data is not None:
|
|
363
|
+
_LOGGER.warning(f"{self.device_id} send_ping {self.ping_data}")
|
|
364
|
+
self.writer.write(self.get_send_packet(json.dumps(self.ping_data).encode(), 2))
|
|
365
|
+
await self.writer.drain()
|
|
366
|
+
else:
|
|
367
|
+
_LOGGER.warning(f"{self.device_id} send_ping {self.syncProperties}")
|
|
368
|
+
await self.send_action(self.syncProperties, CONF_GET_DEV_ATTR_REQ)
|
|
397
369
|
return 1
|
|
398
370
|
except Exception as e:
|
|
399
371
|
_LOGGER.error(f"{self.device_id} ping error {e}")
|
|
@@ -420,7 +392,7 @@ class DeviceClient(object):
|
|
|
420
392
|
await self.writer.wait_closed()
|
|
421
393
|
except Exception as e:
|
|
422
394
|
_LOGGER.error(f"{self.device_id} writer/reader close error {e}")
|
|
423
|
-
self.writer = self.reader = None
|
|
395
|
+
self.writer = self.reader = None
|
|
424
396
|
|
|
425
397
|
self._connect_and_login = False
|
|
426
398
|
self.status.online = False
|
|
@@ -8,10 +8,9 @@ from typing import Any
|
|
|
8
8
|
from .aes_utils import aes_encrypt, aes_decrypt
|
|
9
9
|
from .const import CONF_ID, CONF_IPADDRESS
|
|
10
10
|
from .exceptions import AidotOSError
|
|
11
|
+
from aidot.models.discover_model import DiscoverResponse, DiscoverRequest
|
|
11
12
|
|
|
12
13
|
_LOGGER = logging.getLogger(__name__)
|
|
13
|
-
# _DISCOVER_TIME = 15
|
|
14
|
-
|
|
15
14
|
_DISCOVER_FAST = 6 # 启动时快速发现
|
|
16
15
|
_DISCOVER_SLOW = 120 # 稳定后慢速维持
|
|
17
16
|
|
|
@@ -19,10 +18,7 @@ class BroadcastProtocol:
|
|
|
19
18
|
_is_closed = False
|
|
20
19
|
|
|
21
20
|
def __init__(self, callback, user_id) -> None:
|
|
22
|
-
self.aes_key = bytearray(32)
|
|
23
|
-
key_string = "T54uednca587"
|
|
24
|
-
key_bytes = key_string.encode()
|
|
25
|
-
self.aes_key[: len(key_bytes)] = key_bytes
|
|
21
|
+
self.aes_key = bytearray(b"T54uednca587".ljust(32, b'\x00'))
|
|
26
22
|
|
|
27
23
|
self._discover_cb = callback
|
|
28
24
|
self.user_id = user_id
|
|
@@ -36,37 +32,25 @@ class BroadcastProtocol:
|
|
|
36
32
|
if self._is_closed is True:
|
|
37
33
|
_LOGGER.error(f"{self.user_id}:Connection is closed")
|
|
38
34
|
return
|
|
39
|
-
current_timestamp_milliseconds = int(time.time() * 1000)
|
|
40
|
-
seq = str(current_timestamp_milliseconds + 1)[-9:]
|
|
41
|
-
message = {
|
|
42
|
-
"protocolVer": "2.0.0",
|
|
43
|
-
"service": "device",
|
|
44
|
-
"method": "devDiscoveryReq",
|
|
45
|
-
"seq": seq,
|
|
46
|
-
"srcAddr": f"0.{self.user_id}]",
|
|
47
|
-
"tst": current_timestamp_milliseconds,
|
|
48
|
-
"payload": {
|
|
49
|
-
"extends": {},
|
|
50
|
-
"localCtrFlag": 1,
|
|
51
|
-
"timestamp": str(current_timestamp_milliseconds),
|
|
52
|
-
},
|
|
53
|
-
}
|
|
54
|
-
_LOGGER.info(f"send_broadcast {message}")
|
|
55
|
-
send_data = aes_encrypt(json.dumps(message).encode(), self.aes_key)
|
|
56
35
|
try:
|
|
36
|
+
request = DiscoverRequest.from_params(userId=self.user_id)
|
|
37
|
+
message = request.to_dict()
|
|
38
|
+
_LOGGER.warning(f"send_broadcast {message}")
|
|
39
|
+
send_data = aes_encrypt(json.dumps(message).encode(), self.aes_key)
|
|
57
40
|
self.transport.sendto(send_data, ("255.255.255.255", 6666))
|
|
58
41
|
except Exception as error:
|
|
59
42
|
_LOGGER.error(f"{self.user_id}:Connection lost due to error: {error}")
|
|
60
43
|
|
|
61
44
|
def datagram_received(self, data, addr) -> None:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
45
|
+
try:
|
|
46
|
+
data_str = aes_decrypt(data, self.aes_key)
|
|
47
|
+
data_json = json.loads(data_str)
|
|
48
|
+
response = DiscoverResponse.from_json(data=data_json)
|
|
49
|
+
_LOGGER.warning(f"datagram_received {data_json}")
|
|
50
|
+
if response.payload and response.payload.devId and self._discover_cb:
|
|
51
|
+
self._discover_cb(response.payload.devId, {CONF_IPADDRESS: addr[0]})
|
|
52
|
+
except Exception as error:
|
|
53
|
+
_LOGGER.error(f"datagram_received error: {error}")
|
|
70
54
|
|
|
71
55
|
def error_received(self, exc) -> None:
|
|
72
56
|
_LOGGER.error(f"{self.user_id}:Error occurred: {exc}")
|
|
@@ -115,7 +99,7 @@ class Discover:
|
|
|
115
99
|
self._schedule_broadcast()
|
|
116
100
|
|
|
117
101
|
def _schedule_broadcast(self) -> None:
|
|
118
|
-
_LOGGER.
|
|
102
|
+
_LOGGER.warning(f"_schedule_broadcast")
|
|
119
103
|
# 前几次快速发现,之后慢速
|
|
120
104
|
if self._fast_discover_count > 0:
|
|
121
105
|
interval = _DISCOVER_FAST
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Models for AiDot."""
|
|
2
|
+
|
|
3
|
+
from .device_client_model import (
|
|
4
|
+
DeviceAck,
|
|
5
|
+
DeviceAttr,
|
|
6
|
+
DeviceAttrPayload,
|
|
7
|
+
DeviceResponse,
|
|
8
|
+
DeviceActionPayload,
|
|
9
|
+
DeviceActionRequest,
|
|
10
|
+
LoginPayload,
|
|
11
|
+
LoginRequest,
|
|
12
|
+
PingRequest,
|
|
13
|
+
PingResponse,
|
|
14
|
+
)
|
|
15
|
+
from .device_model import (
|
|
16
|
+
DeviceFading,
|
|
17
|
+
DeviceProperties,
|
|
18
|
+
DeviceProduct,
|
|
19
|
+
DeviceInformation,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"DeviceAck",
|
|
24
|
+
"DeviceAttr",
|
|
25
|
+
"DeviceAttrPayload",
|
|
26
|
+
"DeviceResponse",
|
|
27
|
+
"DeviceActionPayload",
|
|
28
|
+
"DeviceActionRequest",
|
|
29
|
+
"LoginPayload",
|
|
30
|
+
"LoginRequest",
|
|
31
|
+
"PingRequest",
|
|
32
|
+
"PingResponse",
|
|
33
|
+
"DeviceFading",
|
|
34
|
+
"DeviceProperties",
|
|
35
|
+
"DeviceProduct",
|
|
36
|
+
"DeviceInformation",
|
|
37
|
+
]
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Models for AiDot device client."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, asdict, field
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from dacite import Config, from_dict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class PingRequest:
|
|
11
|
+
"""Ping request (heartbeat)."""
|
|
12
|
+
|
|
13
|
+
service: str = "test"
|
|
14
|
+
method: str = "pingreq"
|
|
15
|
+
seq: str = "123456"
|
|
16
|
+
srcAddr: str = "123456"
|
|
17
|
+
payload: dict[str, Any] = field(default_factory=dict)
|
|
18
|
+
|
|
19
|
+
def to_dict(self) -> dict[str, Any]:
|
|
20
|
+
"""Convert to dictionary."""
|
|
21
|
+
return asdict(self)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class PingResponse:
|
|
26
|
+
"""Ping response."""
|
|
27
|
+
|
|
28
|
+
service: str = None
|
|
29
|
+
method: str = None
|
|
30
|
+
seq: str = None
|
|
31
|
+
srcAddr: str = None
|
|
32
|
+
payload: dict[str, Any] = None
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def from_json(data: dict[str, Any]) -> "PingResponse":
|
|
36
|
+
"""Create PingResponse from JSON dict."""
|
|
37
|
+
return from_dict(
|
|
38
|
+
data_class=PingResponse, data=data, config=Config(check_types=False)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def to_dict(self) -> dict[str, Any]:
|
|
42
|
+
"""Convert to dictionary."""
|
|
43
|
+
return asdict(self)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class LoginPayload:
|
|
48
|
+
"""Login request payload."""
|
|
49
|
+
|
|
50
|
+
userId: str = None
|
|
51
|
+
password: str = None
|
|
52
|
+
timestamp: str = field(default=None)
|
|
53
|
+
ascNumber: int = 1
|
|
54
|
+
|
|
55
|
+
def __post_init__(self):
|
|
56
|
+
"""Auto-generate timestamp if not provided."""
|
|
57
|
+
if self.timestamp is None:
|
|
58
|
+
from datetime import datetime
|
|
59
|
+
self.timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class LoginRequest:
|
|
64
|
+
"""Login request."""
|
|
65
|
+
|
|
66
|
+
service: str = "device"
|
|
67
|
+
method: str = "loginReq"
|
|
68
|
+
seq: str = None
|
|
69
|
+
srcAddr: str = None
|
|
70
|
+
deviceId: str = None
|
|
71
|
+
payload: LoginPayload = None
|
|
72
|
+
|
|
73
|
+
def to_dict(self) -> dict[str, Any]:
|
|
74
|
+
"""Convert to dictionary."""
|
|
75
|
+
return asdict(self)
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class DeviceAck:
|
|
79
|
+
"""Device response ack."""
|
|
80
|
+
|
|
81
|
+
code: int = 0
|
|
82
|
+
tst: str = ""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class DeviceAttr:
|
|
87
|
+
"""Device attribute."""
|
|
88
|
+
|
|
89
|
+
OnOff: int = None
|
|
90
|
+
Dimming: int = None
|
|
91
|
+
RGBW: int = None
|
|
92
|
+
CCT: int = None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class DeviceAttrPayload:
|
|
97
|
+
"""Device payload."""
|
|
98
|
+
|
|
99
|
+
devId: str = ""
|
|
100
|
+
parentId: str = ""
|
|
101
|
+
userId: str = ""
|
|
102
|
+
password: str = ""
|
|
103
|
+
timestamp: str = ""
|
|
104
|
+
channel: str = ""
|
|
105
|
+
ascNumber: int = 0
|
|
106
|
+
attr: Optional[DeviceAttr] = None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class DeviceResponse:
|
|
111
|
+
"""Generic device response."""
|
|
112
|
+
|
|
113
|
+
service: str = ""
|
|
114
|
+
method: str = ""
|
|
115
|
+
seq: str = ""
|
|
116
|
+
srcAddr: str = ""
|
|
117
|
+
deviceId: str = ""
|
|
118
|
+
clientId: str = ""
|
|
119
|
+
payload: DeviceAttrPayload = field(default_factory=DeviceAttrPayload)
|
|
120
|
+
ack: DeviceAck = field(default_factory=DeviceAck)
|
|
121
|
+
tst: int = 0
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def from_json(data: dict[str, Any]) -> "DeviceResponse":
|
|
125
|
+
"""Create DeviceResponse from JSON dict."""
|
|
126
|
+
return from_dict(
|
|
127
|
+
data_class=DeviceResponse, data=data, config=Config(check_types=False)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def to_dict(self) -> dict[str, Any]:
|
|
131
|
+
"""Convert to dictionary."""
|
|
132
|
+
return asdict(self)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class DeviceActionPayload:
|
|
137
|
+
"""Device action payload."""
|
|
138
|
+
|
|
139
|
+
devId: str = ""
|
|
140
|
+
parentId: str = ""
|
|
141
|
+
userId: str = ""
|
|
142
|
+
password: str = ""
|
|
143
|
+
attr: dict[str, Any] = field(default_factory=dict)
|
|
144
|
+
channel: str = "tcp"
|
|
145
|
+
ascNumber: int = 0
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass
|
|
149
|
+
class DeviceActionRequest:
|
|
150
|
+
"""Device action request."""
|
|
151
|
+
|
|
152
|
+
method: str = ""
|
|
153
|
+
service: str = "device"
|
|
154
|
+
clientId: str = ""
|
|
155
|
+
srcAddr: str = ""
|
|
156
|
+
seq: str = ""
|
|
157
|
+
payload: DeviceActionPayload = field(default_factory=DeviceActionPayload)
|
|
158
|
+
tst: int = 0
|
|
159
|
+
deviceId: str = ""
|
|
160
|
+
|
|
161
|
+
def __post_init__(self):
|
|
162
|
+
"""Auto-generate timestamp if not provided."""
|
|
163
|
+
if self.tst == 0:
|
|
164
|
+
import time
|
|
165
|
+
self.tst = int(time.time() * 1000)
|
|
166
|
+
|
|
167
|
+
def to_dict(self) -> dict[str, Any]:
|
|
168
|
+
"""Convert to dictionary."""
|
|
169
|
+
return asdict(self)
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def from_params(
|
|
173
|
+
method: str,
|
|
174
|
+
user_id: str,
|
|
175
|
+
device_id: str,
|
|
176
|
+
password: str,
|
|
177
|
+
ascNumber: int,
|
|
178
|
+
attr: dict[str, Any],
|
|
179
|
+
seq: str,
|
|
180
|
+
simpleVersion: str = None,
|
|
181
|
+
) -> "DeviceActionRequest":
|
|
182
|
+
"""Create DeviceActionRequest from params."""
|
|
183
|
+
if simpleVersion is not None:
|
|
184
|
+
return DeviceActionRequest(
|
|
185
|
+
method=method,
|
|
186
|
+
clientId="ha-" + user_id,
|
|
187
|
+
srcAddr="0." + user_id,
|
|
188
|
+
seq=seq,
|
|
189
|
+
payload=DeviceActionPayload(
|
|
190
|
+
devId=device_id,
|
|
191
|
+
parentId=device_id,
|
|
192
|
+
userId=user_id,
|
|
193
|
+
password=password,
|
|
194
|
+
attr=attr,
|
|
195
|
+
ascNumber=ascNumber,
|
|
196
|
+
),
|
|
197
|
+
deviceId=device_id,
|
|
198
|
+
)
|
|
199
|
+
else:
|
|
200
|
+
return DeviceActionRequest(
|
|
201
|
+
method=method,
|
|
202
|
+
srcAddr="0." + user_id,
|
|
203
|
+
seq=seq,
|
|
204
|
+
payload=DeviceActionPayload(
|
|
205
|
+
attr=attr,
|
|
206
|
+
ascNumber=ascNumber,
|
|
207
|
+
),
|
|
208
|
+
deviceId=device_id,
|
|
209
|
+
)
|
|
210
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Models for AiDot device."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, asdict, field
|
|
4
|
+
from typing import Any, Optional, List
|
|
5
|
+
|
|
6
|
+
from dacite import Config, from_dict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class DeviceFading:
|
|
11
|
+
"""Device fading config."""
|
|
12
|
+
|
|
13
|
+
in_value: Optional[int] = field(default=None, metadata={"alias": "in"})
|
|
14
|
+
out: Optional[int] = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class DeviceProperties:
|
|
19
|
+
"""Device properties."""
|
|
20
|
+
|
|
21
|
+
ssidName: str = None
|
|
22
|
+
ipAddress: str = None
|
|
23
|
+
macAddress: str = None
|
|
24
|
+
networkRssi: str = None
|
|
25
|
+
networkSecurity: str = None
|
|
26
|
+
wifiChannel: str = None
|
|
27
|
+
OnOff: str = None
|
|
28
|
+
Dimming: str = None
|
|
29
|
+
CCT: str = None
|
|
30
|
+
RGBW: str = None
|
|
31
|
+
EffectMode: str = None
|
|
32
|
+
Area: str = None
|
|
33
|
+
city: str = None
|
|
34
|
+
CityTimezone: str = None
|
|
35
|
+
lastNotifyCctRgbw: str = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class DeviceProduct:
|
|
40
|
+
"""Device product info."""
|
|
41
|
+
|
|
42
|
+
id: str = None
|
|
43
|
+
name: str = None
|
|
44
|
+
type: str = None
|
|
45
|
+
modelId: str = None
|
|
46
|
+
picture: str = None
|
|
47
|
+
icon: str = None
|
|
48
|
+
isDirectDevice: int = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class DeviceInformation:
|
|
53
|
+
"""Device information."""
|
|
54
|
+
|
|
55
|
+
id: str = None
|
|
56
|
+
name: str = None
|
|
57
|
+
mac: str = None
|
|
58
|
+
type: str = None
|
|
59
|
+
modelId: str = None
|
|
60
|
+
productId: str = None
|
|
61
|
+
houseId: str = None
|
|
62
|
+
roomId: str = None
|
|
63
|
+
online: bool = None
|
|
64
|
+
firmwareVersion: str = None
|
|
65
|
+
hardwareVersion: str = None
|
|
66
|
+
protocolVersion: str = None
|
|
67
|
+
picture: str = None
|
|
68
|
+
aesKey: List[str] = field(default_factory=list)
|
|
69
|
+
password: str = None
|
|
70
|
+
fading: Optional[DeviceFading] = None
|
|
71
|
+
properties: Optional[DeviceProperties] = None
|
|
72
|
+
product: Optional[DeviceProduct] = None
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def from_json(data: dict[str, Any]) -> "DeviceInformation":
|
|
76
|
+
"""Create DeviceInformation from JSON dict."""
|
|
77
|
+
return from_dict(
|
|
78
|
+
data_class=DeviceInformation, data=data, config=Config(check_types=False)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def to_dict(self) -> dict[str, Any]:
|
|
82
|
+
"""Convert to dictionary."""
|
|
83
|
+
return asdict(self)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Models for AiDot discover."""
|
|
2
|
+
import time
|
|
3
|
+
from dataclasses import dataclass, asdict, field
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from dacite import Config, from_dict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class DiscoverAck:
|
|
11
|
+
"""Discover response ack."""
|
|
12
|
+
|
|
13
|
+
code: int = None
|
|
14
|
+
tst: str = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class DiscoverExtends:
|
|
19
|
+
"""Discover payload extends."""
|
|
20
|
+
|
|
21
|
+
srvHost: str = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class DiscoverPayload:
|
|
26
|
+
"""Discover response payload."""
|
|
27
|
+
|
|
28
|
+
ip: str = None
|
|
29
|
+
mac: str = None
|
|
30
|
+
devId: str = None
|
|
31
|
+
productModel: str = None
|
|
32
|
+
bindFlag: int = None
|
|
33
|
+
conMode: int = None
|
|
34
|
+
lanMode: int = None
|
|
35
|
+
wifiMode: int = None
|
|
36
|
+
wifiBand: int = None
|
|
37
|
+
version: int = None
|
|
38
|
+
extends: Optional[DiscoverExtends] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class DiscoverResponse:
|
|
43
|
+
"""Discover device response."""
|
|
44
|
+
|
|
45
|
+
srcAddr: str = None
|
|
46
|
+
seq: str = None
|
|
47
|
+
service: str = None
|
|
48
|
+
method: str = None
|
|
49
|
+
ack: DiscoverAck = None
|
|
50
|
+
protocolVer: str = None
|
|
51
|
+
payload: DiscoverPayload = None
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def from_json(data: dict[str, Any]) -> "DiscoverResponse":
|
|
55
|
+
"""Create DiscoverResponse from JSON dict."""
|
|
56
|
+
return from_dict(
|
|
57
|
+
data_class=DiscoverResponse, data=data, config=Config(check_types=False)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> dict[str, Any]:
|
|
61
|
+
"""Convert to dictionary."""
|
|
62
|
+
return asdict(self)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class DiscoverRequestPayload:
|
|
67
|
+
"""Discover request payload."""
|
|
68
|
+
|
|
69
|
+
extends: dict[str, Any] = field(default_factory=dict)
|
|
70
|
+
localCtrFlag: int = 1
|
|
71
|
+
timestamp: str = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class DiscoverRequest:
|
|
76
|
+
"""Discover device request."""
|
|
77
|
+
|
|
78
|
+
protocolVer: str = "2.0.0"
|
|
79
|
+
service: str = "device"
|
|
80
|
+
method: str = "devDiscoveryReq"
|
|
81
|
+
seq: str = None
|
|
82
|
+
srcAddr: str = None
|
|
83
|
+
tst: int = None
|
|
84
|
+
payload: DiscoverRequestPayload = field(default_factory=DiscoverRequestPayload)
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def from_params(userId: str) -> "DiscoverResponse":
|
|
88
|
+
"""Create DiscoverResponse from JSON dict."""
|
|
89
|
+
current_timestamp_milliseconds = int(time.time() * 1000)
|
|
90
|
+
seq = str(current_timestamp_milliseconds + 1)[-9:]
|
|
91
|
+
return DiscoverRequest(
|
|
92
|
+
seq=seq,
|
|
93
|
+
srcAddr=f"0.{userId}",
|
|
94
|
+
tst=current_timestamp_milliseconds,
|
|
95
|
+
payload=DiscoverRequestPayload(
|
|
96
|
+
timestamp=str(current_timestamp_milliseconds)
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def to_dict(self) -> dict[str, Any]:
|
|
101
|
+
"""Convert to dictionary."""
|
|
102
|
+
return asdict(self)
|
|
@@ -10,6 +10,10 @@ aidot/device_client.py
|
|
|
10
10
|
aidot/discover.py
|
|
11
11
|
aidot/exceptions.py
|
|
12
12
|
aidot/login_const.py
|
|
13
|
+
aidot/models/__init__.py
|
|
14
|
+
aidot/models/device_client_model.py
|
|
15
|
+
aidot/models/device_model.py
|
|
16
|
+
aidot/models/discover_model.py
|
|
13
17
|
python_aidot.egg-info/PKG-INFO
|
|
14
18
|
python_aidot.egg-info/SOURCES.txt
|
|
15
19
|
python_aidot.egg-info/dependency_links.txt
|
|
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
|