python-aidot 0.3.54b1__tar.gz → 0.3.54b3__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.4
2
2
  Name: python-aidot
3
- Version: 0.3.54b1
3
+ Version: 0.3.54b3
4
4
  Summary: aidot control wifi lights
5
5
  Home-page: https://github.com/Aidot-Development-Team/python-aidot
6
6
  Author: aidotdev2024
@@ -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.52")
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, aes_decrypt
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: dict[str, Any]) -> None:
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.get(CONF_ON_OFF) is not None:
58
- self.on = attr.get(CONF_ON_OFF)
59
- if attr.get(CONF_DIMMING) is not None:
60
- self.dimming = int(attr.get(CONF_DIMMING) * 255 / 100)
61
- if attr.get(CONF_RGBW) is not None:
62
- rgbw_value = attr.get(CONF_RGBW)
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.get(CONF_CCT) is not None:
76
- self.cct = attr.get(CONF_CCT)
77
+ if attr.CCT is not None:
78
+ self.cct = attr.CCT
77
79
 
78
80
 
79
81
  class DeviceInformation:
@@ -123,6 +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]
128
+ heart_time = 30
129
+ ping_data = PingRequest().to_dict()
126
130
  _TAG: str = "DeviceClient"
127
131
  @property
128
132
  def connect_and_login(self) -> bool:
@@ -148,7 +152,11 @@ class DeviceClient(object):
148
152
  self.password = device.get(CONF_PASSWORD)
149
153
  self.device_id = device.get(CONF_ID)
150
154
  self._simpleVersion = device.get("simpleVersion")
151
- self._TAG = f"{self.device_id}";
155
+ self._TAG = f"{self.device_id}"
156
+ if self.info.model_id == 'lk.WIFI-RGBWLight-D0006':
157
+ self.ping_data = None
158
+ self.heart_time = 10
159
+
152
160
  _LOGGER.warning(f"{self._TAG}:{device}")
153
161
 
154
162
  async def connect(self, ip_address) -> None:
@@ -204,38 +212,36 @@ class DeviceClient(object):
204
212
  async def login(self) -> None:
205
213
  login_seq = str(int(time.time() * 1000) + self._login_uuid)[-9:]
206
214
  self._login_uuid += 1
207
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
208
- message = {
209
- "service": "device",
210
- "method": "loginReq",
211
- "seq": login_seq,
212
- "srcAddr": self.user_id,
213
- "deviceId": self.device_id,
214
- "payload": {
215
- "userId": self.user_id,
216
- "password": self.password,
217
- "timestamp": timestamp,
218
- "ascNumber": 1,
219
- },
220
- }
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()
221
226
  try:
222
227
  self.writer.write(self.get_send_packet(json.dumps(message).encode(), 1))
223
228
  await self.writer.drain()
224
229
  header = await self.reader.readexactly(8)
225
230
  magic, msgtype, bodysize = struct.unpack(">HHI", header)
226
231
  body = await self.reader.readexactly(bodysize)
227
- decrypted_data = aes_decrypt(body, self.aes_key) if self.aes_key else body
228
- json_data = json.loads(decrypted_data)
229
- code = json_data[CONF_ACK][CONF_CODE]
230
-
231
- 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:
232
237
  # 登录失败
233
- _LOGGER.error(f"{self._TAG}:login error, code: {code}")
238
+ _LOGGER.error(f"{self._TAG}:login error, code: {response.ack.code}")
234
239
  await self.reset()
235
240
  return
236
241
 
237
- self.ascNumber = json_data[CONF_PAYLOAD][CONF_ASCNUMBER] + 1
242
+ self.ascNumber = response.payload.ascNumber + 1
238
243
  self.status.online = True
244
+ self._notify_status_update()
239
245
  self._receive_task = asyncio.create_task(
240
246
  self.receive_data(),
241
247
  name=f"aidot_receive_{self.device_id}"
@@ -259,8 +265,7 @@ class DeviceClient(object):
259
265
  magic, msgtype, bodysize = struct.unpack(">HHI", header)
260
266
  self.ping_count = 0 #有读到数据就把ping清零
261
267
  body = await self.reader.readexactly(bodysize)
262
- decrypted_data = aes_decrypt(body, self.aes_key)
263
- json_data = json.loads(decrypted_data)
268
+ json_data = aes_decrypt_to_json(body, self.aes_key)
264
269
  _LOGGER.warning(f"{self._TAG}:reveive_data : {json_data}")
265
270
  except asyncio.CancelledError:
266
271
  _LOGGER.debug(f"{self._TAG}:Receive task cancelled")
@@ -273,24 +278,23 @@ class DeviceClient(object):
273
278
  return
274
279
  except Exception as e:
275
280
  _LOGGER.error(f"{self._TAG}:recv error: {e}")
276
- self.ping_count = 0
277
281
  continue
278
282
 
279
- if "service" in json_data:
280
- if "test" == json_data["service"]:
281
- self.ping_count = 0
282
- continue
283
-
284
- payload = json_data.get(CONF_PAYLOAD)
285
- if payload is not None:
286
- self.ascNumber = payload.get(CONF_ASCNUMBER)
287
- self.status.update(payload.get(CONF_ATTR))
288
- self._notify_status_update()
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()
289
293
 
290
294
  def _schedule_ping(self):
291
295
  loop = asyncio.get_running_loop()
292
296
  loop.create_task(self.send_ping_action())
293
- self._ping_timer = loop.call_later(30, self._schedule_ping)
297
+ self._ping_timer = loop.call_later(self.heart_time, self._schedule_ping)
294
298
 
295
299
  async def send_dev_attr(self, dev_attr) -> None:
296
300
  if not self._connect_and_login:
@@ -318,42 +322,19 @@ class DeviceClient(object):
318
322
  await self.send_dev_attr({CONF_CCT: cct})
319
323
 
320
324
  async def send_action(self, attr, method) -> None:
321
- current_timestamp_milliseconds = int(time.time() * 1000)
322
325
  self.seq_num += 1
323
- seq = "ha93" + str(self.seq_num).zfill(5)
324
-
325
- if self._simpleVersion is not None:
326
- action = {
327
- "method": method,
328
- "service": "device",
329
- "clientId": "ha-" + self.user_id,
330
- "srcAddr": "0." + self.user_id,
331
- "seq": "" + seq,
332
- CONF_PAYLOAD: {
333
- "devId": self.device_id,
334
- "parentId": self.device_id,
335
- "userId": self.user_id,
336
- "password": self.password,
337
- "attr": attr,
338
- "channel": "tcp",
339
- "ascNumber": self.ascNumber,
340
- },
341
- "tst": current_timestamp_milliseconds,
342
- "deviceId": self.device_id,
343
- }
344
- else:
345
- action = {
346
- "method": method,
347
- "service": "device",
348
- "seq": "" + seq,
349
- "srcAddr": "0." + self.user_id,
350
- CONF_PAYLOAD: {
351
- "attr": attr,
352
- "ascNumber": self.ascNumber,
353
- },
354
- "tst": current_timestamp_milliseconds,
355
- "deviceId": self.device_id,
356
- }
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()
357
338
  _LOGGER.warning(f"{self.device_id} send_action {action}")
358
339
  try:
359
340
  self.writer.write(self.get_send_packet(json.dumps(action).encode(), 1))
@@ -367,14 +348,6 @@ class DeviceClient(object):
367
348
  async def send_ping_action(self) -> int:
368
349
  if self._is_close:
369
350
  return -1
370
- ping = {
371
- "service": "test",
372
- "method": "pingreq",
373
- "seq": "123456",
374
- "srcAddr": "123456",
375
- CONF_PAYLOAD: {},
376
- }
377
- _LOGGER.warning(f"{self.device_id} send_ping_action {ping}")
378
351
  try:
379
352
  if self.ping_count >= 3:
380
353
  _LOGGER.error(
@@ -384,10 +357,15 @@ class DeviceClient(object):
384
357
  return -1
385
358
  if self._connect_and_login is False:
386
359
  return -1
387
- self.writer.write(self.get_send_packet(json.dumps(ping).encode(), 2))
388
- await self.writer.drain()
360
+
389
361
  self.ping_count += 1
390
- await self.send_action(self.syncProperties, CONF_GET_DEV_ATTR_REQ)
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)
391
369
  return 1
392
370
  except Exception as e:
393
371
  _LOGGER.error(f"{self.device_id} ping error {e}")
@@ -414,7 +392,7 @@ class DeviceClient(object):
414
392
  await self.writer.wait_closed()
415
393
  except Exception as e:
416
394
  _LOGGER.error(f"{self.device_id} writer/reader close error {e}")
417
- self.writer = self.reader = None;
395
+ self.writer = self.reader = None
418
396
 
419
397
  self._connect_and_login = False
420
398
  self.status.online = False
@@ -433,9 +411,5 @@ class DeviceClient(object):
433
411
  """延迟重连"""
434
412
  _LOGGER.info(f"{self.device_id} _schedule_reconnect")
435
413
  loop = asyncio.get_running_loop()
436
- # self._reconnect_handle = loop.call_later(
437
- # 10, # 10秒后重连
438
- # lambda: asyncio.create_task(self.async_login())
439
- # )
440
414
  self._reconnect_handle = loop.call_later(60, self._schedule_reconnect)
441
415
  self._login_task = asyncio.create_task(self.async_login())
@@ -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.info(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
- data_str = aes_decrypt(data, self.aes_key)
63
- data_json = json.loads(data_str)
64
- _LOGGER.info(f"datagram_received {data_json}")
65
- if "payload" in data_json:
66
- if "mac" in data_json["payload"]:
67
- devId = data_json["payload"]["devId"]
68
- if self._discover_cb:
69
- self._discover_cb(devId, {CONF_IPADDRESS: addr[0]})
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.info(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.debug(f"_schedule_broadcast")
102
+ _LOGGER.warning(f"_schedule_broadcast")
119
103
  # 前几次快速发现,之后慢速
120
104
  if self._fast_discover_count > 0:
121
105
  interval = _DISCOVER_FAST
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot
3
- Version: 0.3.54b1
3
+ Version: 0.3.54b3
4
4
  Summary: aidot control wifi lights
5
5
  Home-page: https://github.com/Aidot-Development-Team/python-aidot
6
6
  Author: aidotdev2024
@@ -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.3.54b1",
8
+ version="0.3.54b3",
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