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.
Files changed (22) hide show
  1. {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/PKG-INFO +1 -1
  2. {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/aidot/aes_utils.py +19 -0
  3. {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/aidot/client.py +5 -1
  4. {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/aidot/device_client.py +68 -96
  5. {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/aidot/discover.py +16 -32
  6. python_aidot-0.3.54b4/aidot/models/__init__.py +37 -0
  7. python_aidot-0.3.54b4/aidot/models/device_client_model.py +210 -0
  8. python_aidot-0.3.54b4/aidot/models/device_model.py +83 -0
  9. python_aidot-0.3.54b4/aidot/models/discover_model.py +102 -0
  10. {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/python_aidot.egg-info/PKG-INFO +1 -1
  11. {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/python_aidot.egg-info/SOURCES.txt +4 -0
  12. {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/setup.py +1 -1
  13. {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/LICENSE +0 -0
  14. {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/README.md +0 -0
  15. {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/aidot/__init__.py +0 -0
  16. {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/aidot/const.py +0 -0
  17. {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/aidot/exceptions.py +0 -0
  18. {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/aidot/login_const.py +0 -0
  19. {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/python_aidot.egg-info/dependency_links.txt +0 -0
  20. {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/python_aidot.egg-info/requires.txt +0 -0
  21. {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/python_aidot.egg-info/top_level.txt +0 -0
  22. {python_aidot-0.3.54b2 → python_aidot-0.3.54b4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot
3
- Version: 0.3.54b2
3
+ Version: 0.3.54b4
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,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 = 10
127
- # syncProperties = []
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
- # if self.info.model_id == 'lk.WIFI-RGBWLight-D0006':
155
- # self.syncProperties = [CONF_ON_OFF, CONF_DIMMING, CONF_RGBW, CONF_CCT]
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
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
213
- message = {
214
- "service": "device",
215
- "method": "loginReq",
216
- "seq": login_seq,
217
- "srcAddr": self.user_id,
218
- "deviceId": self.device_id,
219
- "payload": {
220
- "userId": self.user_id,
221
- "password": self.password,
222
- "timestamp": timestamp,
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
- decrypted_data = aes_decrypt(body, self.aes_key) if self.aes_key else body
233
- json_data = json.loads(decrypted_data)
234
- code = json_data[CONF_ACK][CONF_CODE]
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 = json_data[CONF_PAYLOAD][CONF_ASCNUMBER] + 1
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
- decrypted_data = aes_decrypt(body, self.aes_key)
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
- if "service" in json_data:
286
- if "test" == json_data["service"]:
287
- self.ping_count = 0
288
- continue
289
-
290
- payload = json_data.get(CONF_PAYLOAD)
291
- if payload is not None:
292
- self.ascNumber = payload.get(CONF_ASCNUMBER)
293
- self.status.update(payload.get(CONF_ATTR))
294
- 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()
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
- seq = "ha93" + str(self.seq_num).zfill(5)
330
-
331
- if self._simpleVersion is not None:
332
- action = {
333
- "method": method,
334
- "service": "device",
335
- "clientId": "ha-" + self.user_id,
336
- "srcAddr": "0." + self.user_id,
337
- "seq": "" + seq,
338
- CONF_PAYLOAD: {
339
- "devId": self.device_id,
340
- "parentId": self.device_id,
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
- # self.writer.write(self.get_send_packet(json.dumps(ping).encode(), 2))
394
- # await self.writer.drain()
360
+
395
361
  self.ping_count += 1
396
- 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)
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
- 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.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.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
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot
3
- Version: 0.3.54b2
3
+ Version: 0.3.54b4
4
4
  Summary: aidot control wifi lights
5
5
  Home-page: https://github.com/Aidot-Development-Team/python-aidot
6
6
  Author: aidotdev2024
@@ -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
@@ -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.54b2",
8
+ version="0.3.54b4",
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