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.
- {python_aidot-0.3.45 → python_aidot-0.3.47}/PKG-INFO +3 -2
- {python_aidot-0.3.45 → python_aidot-0.3.47}/aidot/client.py +41 -13
- {python_aidot-0.3.45 → python_aidot-0.3.47}/aidot/device_client.py +102 -106
- {python_aidot-0.3.45 → python_aidot-0.3.47}/aidot/discover.py +45 -35
- {python_aidot-0.3.45 → python_aidot-0.3.47}/python_aidot.egg-info/PKG-INFO +3 -2
- {python_aidot-0.3.45 → python_aidot-0.3.47}/setup.py +1 -1
- {python_aidot-0.3.45 → python_aidot-0.3.47}/LICENSE +0 -0
- {python_aidot-0.3.45 → python_aidot-0.3.47}/README.md +0 -0
- {python_aidot-0.3.45 → python_aidot-0.3.47}/aidot/__init__.py +0 -0
- {python_aidot-0.3.45 → python_aidot-0.3.47}/aidot/aes_utils.py +0 -0
- {python_aidot-0.3.45 → python_aidot-0.3.47}/aidot/const.py +0 -0
- {python_aidot-0.3.45 → python_aidot-0.3.47}/aidot/exceptions.py +0 -0
- {python_aidot-0.3.45 → python_aidot-0.3.47}/aidot/login_const.py +0 -0
- {python_aidot-0.3.45 → python_aidot-0.3.47}/python_aidot.egg-info/SOURCES.txt +0 -0
- {python_aidot-0.3.45 → python_aidot-0.3.47}/python_aidot.egg-info/dependency_links.txt +0 -0
- {python_aidot-0.3.45 → python_aidot-0.3.47}/python_aidot.egg-info/requires.txt +0 -0
- {python_aidot-0.3.45 → python_aidot-0.3.47}/python_aidot.egg-info/top_level.txt +0 -0
- {python_aidot-0.3.45 → python_aidot-0.3.47}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot
|
|
3
|
-
Version: 0.3.
|
|
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":
|
|
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
|
|
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
|
-
|
|
287
|
+
self._discover.start_repeat_broadcast()
|
|
264
288
|
|
|
265
|
-
def
|
|
266
|
-
|
|
267
|
-
self._discover
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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.
|
|
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.
|
|
229
|
-
|
|
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"
|
|
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
|
-
|
|
244
|
+
# TCP容易拼包,需要谨慎处理
|
|
245
|
+
async def receive_data(self) -> None:
|
|
238
246
|
while True:
|
|
239
247
|
try:
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
await self.
|
|
244
|
-
|
|
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
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
self.
|
|
277
|
-
|
|
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.
|
|
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.
|
|
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 >=
|
|
370
|
+
if self.ping_count >= 3:
|
|
405
371
|
_LOGGER.error(
|
|
406
|
-
f"
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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.
|
|
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.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot
|
|
3
|
-
Version: 0.3.
|
|
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
|
|
|
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
|
|
File without changes
|
|
File without changes
|