python-aidot 0.3.45__tar.gz → 0.3.47__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
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: python-aidot
3
- Version: 0.3.45
3
+ Version: 0.3.47
4
4
  Summary: aidot control wifi lights
5
5
  Home-page: https://github.com/Aidot-Development-Team/python-aidot
6
6
  Author: aidotdev2024
@@ -16,6 +16,7 @@ Dynamic: classifier
16
16
  Dynamic: description
17
17
  Dynamic: description-content-type
18
18
  Dynamic: home-page
19
+ Dynamic: license-file
19
20
  Dynamic: requires-dist
20
21
  Dynamic: summary
21
22
 
@@ -9,7 +9,9 @@ from typing import Any, Optional
9
9
  from cryptography.hazmat.backends import default_backend
10
10
  from cryptography.hazmat.primitives import serialization
11
11
  from cryptography.hazmat.primitives.asymmetric import padding
12
-
12
+ import uuid
13
+ from pathlib import Path
14
+ import hashlib
13
15
  from .exceptions import AidotAuthFailed, AidotUserOrPassIncorrect
14
16
  from .device_client import DeviceClient
15
17
  from .discover import Discover
@@ -75,6 +77,7 @@ class AidotClient:
75
77
  password: str | None = None,
76
78
  token: dict | None = None,
77
79
  ) -> None:
80
+ _LOGGER.info(f"Aidot Client Version: v0.3.46")
78
81
  self.session = session
79
82
  self.username = username
80
83
  self.password = password
@@ -93,7 +96,7 @@ class AidotClient:
93
96
  self._region = token[CONF_REGION]
94
97
  self.country_name = token[CONF_COUNTRY]
95
98
  self._base_url = f"https://prod-{self._region}-api.arnoo.com/v17"
96
-
99
+ self.setup_discover()
97
100
  def set_token_fresh_cb(self, callback) -> None:
98
101
  self._token_fresh_cb = callback
99
102
 
@@ -102,17 +105,35 @@ class AidotClient:
102
105
 
103
106
  def update_password(self, password: str) -> None:
104
107
  self.password = password
108
+
109
+ def get_terminal_id(self) -> str:
110
+ file_path = Path.home() / ".aidot_terminal_id"
111
+ if file_path.exists():
112
+ raw_id = file_path.read_text().strip()
113
+ else:
114
+ node = uuid.getnode()
115
+ is_random = (node >> 40) & 1
116
+
117
+ if is_random:
118
+ raw_id = str(uuid.uuid4())
119
+ else:
120
+ raw_id = format(node, 'x')
121
+ file_path.write_text(raw_id)
122
+ return hashlib.md5(raw_id.encode()).hexdigest()
105
123
 
106
124
  async def async_post_login(self) -> dict[str, Any]:
107
125
  """Login the user input allows us to connect."""
108
126
  url = f"{self._base_url}/users/loginWithFreeVerification"
109
127
  headers = {CONF_APP_ID: APP_ID, CONF_TERMINAL: "app"}
110
128
  # f"{region}:{self.country_name.strip()}",
129
+ terminalId = self.get_terminal_id()
130
+ if terminalId is None:
131
+ terminalId = "gvz3gjae10l4zii00t7y0"
111
132
  data = {
112
133
  "countryKey": f"region:{self.country_name.strip()}",
113
134
  "username": self.username,
114
135
  "password": rsa_password_encrypt(self.password),
115
- "terminalId": "gvz3gjae10l4zii00t7y0",
136
+ "terminalId": terminalId,
116
137
  "webVersion": "0.5.0",
117
138
  "area": "Asia/Shanghai",
118
139
  "UTC": "UTC+8",
@@ -126,6 +147,7 @@ class AidotClient:
126
147
  self.login_info[CONF_PASSWORD] = self.password
127
148
  self.login_info[CONF_REGION] = self._region
128
149
  self.login_info[CONF_COUNTRY] = self.country_name
150
+ self.setup_discover()
129
151
  return self.login_info
130
152
  except aiohttp.ClientError as e:
131
153
  _LOGGER.info(f"async_post_login ClientError {e}")
@@ -237,7 +259,6 @@ class AidotClient:
237
259
  if device_client is None:
238
260
  device_client = DeviceClient(device, self.login_info)
239
261
  self._device_clients[device_id] = device_client
240
- asyncio.get_running_loop().create_task(device_client.ping_task())
241
262
  if self._discover is not None:
242
263
  ip = self._discover.discovered_device.get(device_id)
243
264
  device_client.update_ip_address(ip)
@@ -249,7 +270,10 @@ class AidotClient:
249
270
  await device_client.close()
250
271
  del self._device_clients[dev_id]
251
272
 
252
- def start_discover(self) -> None:
273
+ def setup_discover(self) -> None:
274
+ """初始化完成后调用,启动设备发现"""
275
+ if self.login_info.get(CONF_ID) is None:
276
+ return
253
277
  if self._discover is not None:
254
278
  return
255
279
 
@@ -260,14 +284,18 @@ class AidotClient:
260
284
  device_client.update_ip_address(device_ip)
261
285
 
262
286
  self._discover = Discover(self.login_info, _discover_callback)
263
- asyncio.get_running_loop().create_task(self._discover.repeat_broadcast())
287
+ self._discover.start_repeat_broadcast()
264
288
 
265
- def stop_discover(self) -> None:
266
- self._discover.close()
267
- self._discover = None
268
-
269
- def cleanup(self) -> None:
270
- self.stop_discover()
289
+ async def async_close(self) -> None:
290
+ """关闭客户端,清理资源"""
291
+ if self._discover is not None:
292
+ self._discover.close()
293
+ self._discover = None
271
294
  for client in self._device_clients.values():
272
- asyncio.get_running_loop().create_task(client.close())
295
+ await client.close()
273
296
  self._device_clients.clear()
297
+
298
+ async def async_cleanup(self) -> None:
299
+ """清理所有资源"""
300
+ _LOGGER.info(f"async_cleanup")
301
+ await self.async_close()
@@ -107,7 +107,14 @@ class DeviceClient(object):
107
107
  _ip_address: str = None
108
108
  device_id: str
109
109
  _is_close: bool = False
110
- _status_fresh_cb: Any = None
110
+ on_status_update: Any = None
111
+ _receive_task: Any = None
112
+ _login_task: Any = None
113
+ _reconnect_handle: Any = None
114
+ _ping_timer: Any = None
115
+ writer: Any = None
116
+ reader: Any = None
117
+ _TAG: str = "DeviceClient"
111
118
  @property
112
119
  def connect_and_login(self) -> bool:
113
120
  return self._connect_and_login
@@ -132,9 +139,11 @@ class DeviceClient(object):
132
139
  self.password = device.get(CONF_PASSWORD)
133
140
  self.device_id = device.get(CONF_ID)
134
141
  self._simpleVersion = device.get("simpleVersion")
142
+ self._TAG = f"{self.device_id}";
143
+ _LOGGER.warning(f"{self._TAG}:{device}")
135
144
 
136
145
  async def connect(self, ip_address) -> None:
137
- _LOGGER.info(f"connect device : {ip_address}")
146
+ _LOGGER.warning(f"{self._TAG}:connect device: {ip_address}")
138
147
  self.reader = self.writer = None
139
148
  self._connecting = True
140
149
  try:
@@ -154,7 +163,8 @@ class DeviceClient(object):
154
163
  return
155
164
  self._ip_address = ip
156
165
  if self._connecting is not True and self._connect_and_login is not True:
157
- asyncio.get_running_loop().create_task(self.async_login())
166
+ self._login_task = asyncio.create_task(self.async_login())
167
+
158
168
 
159
169
  async def async_login(self) -> None:
160
170
  if self._ip_address is None:
@@ -175,6 +185,11 @@ class DeviceClient(object):
175
185
  packet = magic + _msgtype + bodysize + send_data
176
186
 
177
187
  return packet
188
+
189
+ def _notify_status_update(self) -> None:
190
+ if self.on_status_update:
191
+ self.on_status_update(self.status)
192
+
178
193
 
179
194
  async def login(self) -> None:
180
195
  login_seq = str(int(time.time() * 1000) + self._login_uuid)[-9:]
@@ -196,68 +211,60 @@ class DeviceClient(object):
196
211
  try:
197
212
  self.writer.write(self.get_send_packet(json.dumps(message).encode(), 1))
198
213
  await self.writer.drain()
199
- data = await self.reader.read(1024)
200
- except (BrokenPipeError, ConnectionResetError) as e:
201
- _LOGGER.error(f"{self.device_id} login read status error {e}")
202
- except Exception as e:
203
- _LOGGER.error(f"recv data error {e}")
204
-
205
- data_len = len(data)
206
- if data_len <= 0:
207
- return
208
-
209
- try:
210
- magic, msgtype, bodysize = struct.unpack(">HHI", data[:8])
211
- encrypted_data = data[8:]
212
- if self.aes_key is not None:
213
- decrypted_data = aes_decrypt(encrypted_data, self.aes_key)
214
- else:
215
- decrypted_data = encrypted_data
216
-
214
+ header = await self.reader.readexactly(8)
215
+ magic, msgtype, bodysize = struct.unpack(">HHI", header)
216
+ body = await self.reader.readexactly(bodysize)
217
+ decrypted_data = aes_decrypt(body, self.aes_key) if self.aes_key else body
217
218
  json_data = json.loads(decrypted_data)
218
219
  code = json_data[CONF_ACK][CONF_CODE]
220
+
219
221
  if code != 200:
220
222
  # 登录失败
221
- _LOGGER.error(f"{self.device_id} login error, code: {code}")
223
+ _LOGGER.error(f"{self._TAG}:login error, code: {code}")
222
224
  await self.reset()
223
225
  return
224
226
 
225
- self.ascNumber = json_data[CONF_PAYLOAD][CONF_ASCNUMBER]
226
- self.ascNumber += 1
227
+ self.ascNumber = json_data[CONF_PAYLOAD][CONF_ASCNUMBER] + 1
227
228
  self.status.online = True
228
- asyncio.get_running_loop().create_task(self.reveive_data())
229
- _LOGGER.info(f"connect device success: {self._ip_address}")
229
+ self._receive_task = asyncio.create_task(
230
+ self.receive_data(),
231
+ name=f"aidot_receive_{self.device_id}"
232
+ )
233
+ if self._ping_timer:
234
+ self._ping_timer.cancel()
235
+ if self._reconnect_handle:
236
+ self._reconnect_handle.cancel()
237
+ self._reconnect_handle = None
238
+ self._schedule_ping()
239
+ _LOGGER.warning(f"{self._TAG}:connect success: {self._ip_address}")
230
240
  await self.send_action({}, "getDevAttrReq")
231
- except Exception as e:
232
- _LOGGER.error(f"connect device error : {e}")
233
- return
234
-
235
-
241
+ except (BrokenPipeError, ConnectionResetError, Exception) as e:
242
+ _LOGGER.error(f"{self.device_id} login read status error {e}")
236
243
 
237
- async def reveive_data(self) -> None:
244
+ # TCP容易拼包,需要谨慎处理
245
+ async def receive_data(self) -> None:
238
246
  while True:
239
247
  try:
240
- data = await self.reader.read(1024)
241
- except (BrokenPipeError, ConnectionResetError) as e:
242
- _LOGGER.error(f"{self.device_id} read status error {e}")
243
- await self.reset()
244
- self.status.online = False
245
- return
246
- except Exception as e:
247
- _LOGGER.error(f"recv data error {e}")
248
- return
249
- data_len = len(data)
250
- if data_len <= 0:
251
- _LOGGER.error("recv data error len, exit socket")
252
- await self.reset()
253
- self.status.online = False
254
- return
255
- try:
256
- magic, msgtype, bodysize = struct.unpack(">HHI", data[:8])
257
- decrypted_data = aes_decrypt(data[8:], self.aes_key)
248
+ header = await self.reader.readexactly(8)
249
+ magic, msgtype, bodysize = struct.unpack(">HHI", header)
250
+ self.ping_count = 0 #有读到数据就把ping清零
251
+ body = await self.reader.readexactly(bodysize)
252
+ decrypted_data = aes_decrypt(body, self.aes_key)
258
253
  json_data = json.loads(decrypted_data)
254
+ _LOGGER.warning(f"{self._TAG}:reveive_data : {json_data}")
255
+ except asyncio.CancelledError:
256
+ _LOGGER.debug(f"{self._TAG}:Receive task cancelled")
257
+ raise
258
+ except (BrokenPipeError, ConnectionResetError, asyncio.IncompleteReadError) as e:
259
+ _LOGGER.error(f"{self._TAG}:read status error {e}")
260
+ # await self.reset()
261
+ syncio.get_running_loop().call_soon(
262
+ lambda: asyncio.create_task(self.reset())
263
+ )
264
+ return
259
265
  except Exception as e:
260
- _LOGGER.error(f"recv json error : {e}")
266
+ _LOGGER.error(f"{self._TAG}:recv error: {e}")
267
+ self.ping_count = 0
261
268
  continue
262
269
 
263
270
  if "service" in json_data:
@@ -269,56 +276,12 @@ class DeviceClient(object):
269
276
  if payload is not None:
270
277
  self.ascNumber = payload.get(CONF_ASCNUMBER)
271
278
  self.status.update(payload.get(CONF_ATTR))
272
- # _LOGGER.info(f"recv status : {payload}")
273
- if self._status_fresh_cb:
274
- self._status_fresh_cb(self.status)
275
- def set_status_fresh_cb(self, callback) -> None:
276
- self._status_fresh_cb = callback
277
- async def read_status(self) -> DeviceStatusData:
278
- # if self._connect_and_login is False:
279
- # await asyncio.sleep(2)
280
- # raise AidotNotLogin
281
- # try:
282
- # data = await self.reader.read(1024)
283
- # except (BrokenPipeError, ConnectionResetError) as e:
284
- # _LOGGER.error(f"{self.device_id} read status error {e}")
285
- # await self.reset()
286
- # self.status.online = False
287
- # return self.status
288
- # except Exception as e:
289
- # _LOGGER.error(f"recv data error {e}")
290
- # return self.status
291
- # data_len = len(data)
292
- # if data_len <= 0:
293
- # _LOGGER.error("recv data error len")
294
- # await self.reset()
295
- # self.status.online = False
296
- # return self.status
297
- # try:
298
- # magic, msgtype, bodysize = struct.unpack(">HHI", data[:8])
299
- # decrypted_data = aes_decrypt(data[8:], self.aes_key)
300
- # json_data = json.loads(decrypted_data)
301
- # except Exception as e:
302
- # _LOGGER.error(f"recv json error : {e}")
303
- # return await self.read_status()
304
-
305
- # if "service" in json_data:
306
- # if "test" == json_data["service"]:
307
- # self.ping_count = 0
308
- # return await self.read_status()
309
- # payload = json_data.get(CONF_PAYLOAD)
310
- # if payload is not None:
311
- # self.ascNumber = payload.get(CONF_ASCNUMBER)
312
- # self.status.update(payload.get(CONF_ATTR))
313
- return self.status
314
-
315
- async def ping_task(self) -> None:
316
- while True:
317
- if self._is_close:
318
- return
319
- await asyncio.sleep(5)
320
- await self.send_ping_action()
321
- await asyncio.sleep(5)
279
+ self._notify_status_update()
280
+
281
+ def _schedule_ping(self):
282
+ loop = asyncio.get_running_loop()
283
+ loop.create_task(self.send_ping_action())
284
+ self._ping_timer = loop.call_later(30, self._schedule_ping)
322
285
 
323
286
  async def send_dev_attr(self, dev_attr) -> None:
324
287
  if not self._connect_and_login:
@@ -387,12 +350,14 @@ class DeviceClient(object):
387
350
  self.writer.write(self.get_send_packet(json.dumps(action).encode(), 1))
388
351
  await self.writer.drain()
389
352
  except (BrokenPipeError, ConnectionResetError) as e:
390
- _LOGGER.error(f"{self.device_id} send action error {e}")
353
+ _LOGGER.error(f"{self._TAG}:send action error {e}")
391
354
  await self.reset()
392
355
  except Exception as e:
393
- _LOGGER.error(f"{self.device_id} send action error {e}")
356
+ _LOGGER.error(f"{self._TAG}:send action error {e}")
394
357
 
395
358
  async def send_ping_action(self) -> int:
359
+ if self._is_close:
360
+ return -1
396
361
  ping = {
397
362
  "service": "test",
398
363
  "method": "pingreq",
@@ -400,10 +365,11 @@ class DeviceClient(object):
400
365
  "srcAddr": "x.xxxxxxx",
401
366
  CONF_PAYLOAD: {},
402
367
  }
368
+ # _LOGGER.info(f"{self.device_id} send_ping_action {ping}")
403
369
  try:
404
- if self.ping_count >= 2:
370
+ if self.ping_count >= 3:
405
371
  _LOGGER.error(
406
- f"Last ping did not return within 20 seconds. device id:{self.device_id}"
372
+ f"{self._TAG}:Device unresponsive within 90 seconds, disconnecting."
407
373
  )
408
374
  await self.reset()
409
375
  return -1
@@ -419,17 +385,47 @@ class DeviceClient(object):
419
385
  return -1
420
386
 
421
387
  async def reset(self) -> None:
388
+ if self._ping_timer:
389
+ self._ping_timer.cancel()
390
+ if self._reconnect_handle:
391
+ self._reconnect_handle.cancel()
392
+ self._reconnect_handle = None
393
+
394
+ if self._receive_task and not self._receive_task.done():
395
+ self._receive_task.cancel()
396
+ try:
397
+ await self._receive_task
398
+ except asyncio.CancelledError as e:
399
+ _LOGGER.error(f"{self.device_id} writer close error {e}")
400
+ pass
422
401
  try:
423
402
  if self.writer:
424
403
  self.writer.close()
425
404
  await self.writer.wait_closed()
426
405
  except Exception as e:
427
- _LOGGER.error(f"{self.device_id} writer close error {e}")
406
+ _LOGGER.error(f"{self.device_id} writer/reader close error {e}")
407
+ self.writer = self.reader = None;
408
+
428
409
  self._connect_and_login = False
429
410
  self.status.online = False
430
411
  self.ping_count = 0
412
+ self._notify_status_update()
413
+ # 自动重连(如果没有主动关闭)
414
+ if not self._is_close and self._ip_address:
415
+ self._schedule_reconnect()
431
416
 
432
417
  async def close(self) -> None:
433
418
  self._is_close = True
434
419
  await self.reset()
435
420
  _LOGGER.info(f"{self.device_id} connect close by user")
421
+
422
+ def _schedule_reconnect(self) -> None:
423
+ """延迟重连"""
424
+ _LOGGER.info(f"{self.device_id} _schedule_reconnect")
425
+ loop = asyncio.get_running_loop()
426
+ # self._reconnect_handle = loop.call_later(
427
+ # 10, # 10秒后重连
428
+ # lambda: asyncio.create_task(self.async_login())
429
+ # )
430
+ self._reconnect_handle = loop.call_later(15, self._schedule_reconnect)
431
+ self._login_task = asyncio.create_task(self.async_login())
@@ -10,8 +10,10 @@ from .const import CONF_ID, CONF_IPADDRESS
10
10
  from .exceptions import AidotOSError
11
11
 
12
12
  _LOGGER = logging.getLogger(__name__)
13
- _DISCOVER_TIME = 5
13
+ # _DISCOVER_TIME = 15
14
14
 
15
+ _DISCOVER_FAST = 10 # 启动时快速发现
16
+ _DISCOVER_SLOW = 120 # 稳定后慢速维持
15
17
 
16
18
  class BroadcastProtocol:
17
19
  _is_closed = False
@@ -49,6 +51,7 @@ class BroadcastProtocol:
49
51
  "timestamp": str(current_timestamp_milliseconds),
50
52
  },
51
53
  }
54
+ _LOGGER.info(f"send_broadcast {message}")
52
55
  send_data = aes_encrypt(json.dumps(message).encode(), self.aes_key)
53
56
  try:
54
57
  self.transport.sendto(send_data, ("255.255.255.255", 6666))
@@ -58,6 +61,7 @@ class BroadcastProtocol:
58
61
  def datagram_received(self, data, addr) -> None:
59
62
  data_str = aes_decrypt(data, self.aes_key)
60
63
  data_json = json.loads(data_str)
64
+ _LOGGER.info(f"datagram_received {data_json}")
61
65
  if "payload" in data_json:
62
66
  if "mac" in data_json["payload"]:
63
67
  devId = data_json["payload"]["devId"]
@@ -85,47 +89,51 @@ class Discover:
85
89
  _login_info: dict[str, Any] = None
86
90
  _broadcast_protocol: BroadcastProtocol = None
87
91
  discovered_device: dict[str, str]
88
- _is_close: bool = False
92
+ _timer_handle: asyncio.TimerHandle | None = None
89
93
 
90
94
  def __init__(self, login_info, callback):
91
95
  self.discovered_device = {}
92
96
  self._login_info = login_info
93
97
  self._callback = callback
94
-
98
+
95
99
  async def try_create_broadcast(self) -> None:
96
- if self._broadcast_protocol is None:
97
- self._broadcast_protocol = BroadcastProtocol(
98
- self._discover_callback, self._login_info[CONF_ID]
100
+ if self._broadcast_protocol is not None:
101
+ return
102
+ try:
103
+ protocol = BroadcastProtocol(self._discover_callback, self._login_info[CONF_ID])
104
+ self._transport, _ = await asyncio.get_running_loop().create_datagram_endpoint(
105
+ lambda: protocol,
106
+ local_addr=("0.0.0.0", 0),
99
107
  )
100
- try:
101
- (
102
- transport,
103
- protocol,
104
- ) = await asyncio.get_event_loop().create_datagram_endpoint(
105
- lambda: self._broadcast_protocol,
106
- local_addr=("0.0.0.0", 0),
107
- )
108
- except OSError:
109
- raise AidotOSError
110
-
111
- async def send_broadcast(self) -> None:
112
- await self.try_create_broadcast()
113
- self._broadcast_protocol.send_broadcast()
114
-
115
- async def repeat_broadcast(self) -> None:
108
+ self._broadcast_protocol = protocol # 成功后再赋值
109
+ except OSError:
110
+ raise AidotOSError
111
+
112
+ def start_repeat_broadcast(self) -> None:
116
113
  self._is_close = False
117
- while True:
118
- await self.send_broadcast()
119
- for i in range(_DISCOVER_TIME):
120
- await asyncio.sleep(1) # 每秒检查一次是否需要取消任务
121
- if self._is_close is True:
122
- return
123
-
124
- async def fetch_devices_info(self) -> dict[str, str]:
125
- self.try_create_broadcast()
126
- self._broadcast_protocol.send_broadcast()
127
- await asyncio.sleep(2)
128
- return self.discovered_device
114
+ self._fast_discover_count = 3 # 前三次快速
115
+ self._schedule_broadcast()
116
+
117
+ def _schedule_broadcast(self) -> None:
118
+ _LOGGER.debug(f"_schedule_broadcast")
119
+ # 前几次快速发现,之后慢速
120
+ if self._fast_discover_count > 0:
121
+ interval = _DISCOVER_FAST
122
+ self._fast_discover_count -= 1
123
+ else:
124
+ interval = _DISCOVER_SLOW
125
+
126
+ loop = asyncio.get_running_loop()
127
+ asyncio.create_task(self._do_broadcast())
128
+ self._timer_handle = loop.call_later(interval, self._schedule_broadcast)
129
+
130
+ async def _do_broadcast(self) -> None:
131
+ """执行广播"""
132
+ try:
133
+ await self.try_create_broadcast()
134
+ self._broadcast_protocol.send_broadcast()
135
+ except Exception as e:
136
+ _LOGGER.error(f"Broadcast failed: {e}")
129
137
 
130
138
  def _discover_callback(self, dev_id, event: dict[str, str]) -> None:
131
139
  self.discovered_device[dev_id] = event[CONF_IPADDRESS]
@@ -133,7 +141,9 @@ class Discover:
133
141
  self._callback(dev_id, event)
134
142
 
135
143
  def close(self) -> None:
136
- self._is_close = True
144
+ if self._timer_handle is not None:
145
+ self._timer_handle.cancel()
146
+ self._timer_handle = None
137
147
  if self._broadcast_protocol is not None:
138
148
  self._broadcast_protocol.close()
139
149
  self._broadcast_protocol = None
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: python-aidot
3
- Version: 0.3.45
3
+ Version: 0.3.47
4
4
  Summary: aidot control wifi lights
5
5
  Home-page: https://github.com/Aidot-Development-Team/python-aidot
6
6
  Author: aidotdev2024
@@ -16,6 +16,7 @@ Dynamic: classifier
16
16
  Dynamic: description
17
17
  Dynamic: description-content-type
18
18
  Dynamic: home-page
19
+ Dynamic: license-file
19
20
  Dynamic: requires-dist
20
21
  Dynamic: summary
21
22
 
@@ -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.45",
8
+ version="0.3.47",
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
File without changes
File without changes