pymammotion 0.4.17__py3-none-any.whl → 0.4.19__py3-none-any.whl

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.
@@ -0,0 +1,295 @@
1
+ import asyncio
2
+ import logging
3
+ import os
4
+ import ssl
5
+ import time
6
+ from typing import Any
7
+ from urllib.parse import urlencode, urlparse
8
+
9
+ import aiohttp
10
+ import certifi
11
+ from requests import PreparedRequest, adapters, status_codes
12
+ from Tea.exceptions import RequiredArgumentException, RetryError
13
+ from Tea.model import TeaModel
14
+ from Tea.request import TeaRequest
15
+ from Tea.response import TeaResponse
16
+ from Tea.stream import BaseStream
17
+
18
+ DEFAULT_CONNECT_TIMEOUT = 5000
19
+ DEFAULT_READ_TIMEOUT = 10000
20
+ DEFAULT_POOL_SIZE = 10
21
+
22
+ logger = logging.getLogger("alibabacloud-tea")
23
+ logger.setLevel(logging.DEBUG)
24
+ ch = logging.StreamHandler()
25
+ logger.addHandler(ch)
26
+
27
+
28
+ class TeaCore:
29
+ http_adapter = adapters.HTTPAdapter(pool_connections=DEFAULT_POOL_SIZE, pool_maxsize=DEFAULT_POOL_SIZE * 4)
30
+ https_adapter = adapters.HTTPAdapter(pool_connections=DEFAULT_POOL_SIZE, pool_maxsize=DEFAULT_POOL_SIZE * 4)
31
+
32
+ @staticmethod
33
+ def get_adapter(prefix):
34
+ if prefix.upper() == "HTTP":
35
+ return TeaCore.http_adapter
36
+ else:
37
+ return TeaCore.https_adapter
38
+
39
+ @staticmethod
40
+ def _prepare_http_debug(request, symbol):
41
+ base = ""
42
+ for key, value in request.headers.items():
43
+ base += f"\n{symbol} {key} : {value}"
44
+ return base
45
+
46
+ @staticmethod
47
+ def _do_http_debug(request, response) -> None:
48
+ # logger the request
49
+ url = urlparse(request.url)
50
+ request_base = f"\n> {request.method.upper()} {url.path + url.query} HTTP/1.1"
51
+ logger.debug(request_base + TeaCore._prepare_http_debug(request, ">"))
52
+
53
+ # logger the response
54
+ response_base = (
55
+ f"\n< HTTP/1.1 {response.status_code}" f" {status_codes._codes.get(response.status_code)[0].upper()}"
56
+ )
57
+ logger.debug(response_base + TeaCore._prepare_http_debug(response, "<"))
58
+
59
+ @staticmethod
60
+ def compose_url(request):
61
+ host = request.headers.get("host")
62
+ if not host:
63
+ raise RequiredArgumentException("endpoint")
64
+ else:
65
+ host = host.rstrip("/")
66
+ protocol = f"{request.protocol.lower()}://"
67
+ pathname = request.pathname
68
+
69
+ if host.startswith(("http://", "https://")):
70
+ protocol = ""
71
+
72
+ if request.port == 80:
73
+ port = ""
74
+ else:
75
+ port = f":{request.port}"
76
+
77
+ url = protocol + host + port + pathname
78
+
79
+ if request.query:
80
+ if "?" in url:
81
+ if not url.endswith("&"):
82
+ url += "&"
83
+ else:
84
+ url += "?"
85
+
86
+ encode_query = {}
87
+ for key in request.query:
88
+ value = request.query[key]
89
+ if value is not None:
90
+ encode_query[key] = str(value)
91
+ url += urlencode(encode_query)
92
+ return url.rstrip("?&")
93
+
94
+ @staticmethod
95
+ async def async_do_action(request: TeaRequest, runtime_option=None) -> TeaResponse:
96
+ runtime_option = runtime_option or {}
97
+
98
+ url = TeaCore.compose_url(request)
99
+ verify = not runtime_option.get("ignoreSSL", False)
100
+
101
+ timeout = runtime_option.get("timeout")
102
+ connect_timeout = runtime_option.get("connectTimeout") or timeout or DEFAULT_CONNECT_TIMEOUT
103
+ read_timeout = runtime_option.get("readTimeout") or timeout or DEFAULT_READ_TIMEOUT
104
+
105
+ connect_timeout, read_timeout = (int(connect_timeout) / 1000, int(read_timeout) / 1000)
106
+
107
+ proxy = None
108
+ if request.protocol.upper() == "HTTP":
109
+ proxy = runtime_option.get("httpProxy")
110
+ if not proxy:
111
+ proxy = os.environ.get("HTTP_PROXY") or os.environ.get("http_proxy")
112
+ elif request.protocol.upper() == "HTTPS":
113
+ proxy = runtime_option.get("httpsProxy")
114
+ if not proxy:
115
+ proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy")
116
+
117
+ connector = None
118
+ ca_cert = certifi.where()
119
+ if ca_cert and request.protocol.upper() == "HTTPS":
120
+ loop = asyncio.get_event_loop()
121
+
122
+ ssl_context = await loop.run_in_executor(None, ssl.create_default_context, ssl.Purpose.SERVER_AUTH)
123
+ await loop.run_in_executor(None, ssl_context.load_verify_locations, ca_cert)
124
+ connector = aiohttp.TCPConnector(
125
+ ssl=ssl_context,
126
+ )
127
+ else:
128
+ verify = False
129
+
130
+ timeout = aiohttp.ClientTimeout(sock_read=read_timeout, sock_connect=connect_timeout)
131
+ async with aiohttp.ClientSession(connector=connector) as s:
132
+ body = b""
133
+ if isinstance(request.body, BaseStream):
134
+ for content in request.body:
135
+ body += content
136
+ elif isinstance(request.body, str):
137
+ body = request.body.encode("utf-8")
138
+ else:
139
+ body = request.body
140
+ try:
141
+ async with s.request(
142
+ request.method, url, data=body, headers=request.headers, ssl=verify, proxy=proxy, timeout=timeout
143
+ ) as response:
144
+ tea_resp = TeaResponse()
145
+ tea_resp.body = await response.read()
146
+ tea_resp.headers = {k.lower(): v for k, v in response.headers.items()}
147
+ tea_resp.status_code = response.status
148
+ tea_resp.status_message = response.reason
149
+ tea_resp.response = response
150
+ except OSError as e:
151
+ raise RetryError(str(e))
152
+ return tea_resp
153
+
154
+ @staticmethod
155
+ def do_action(request: TeaRequest, runtime_option=None) -> TeaResponse:
156
+ url = TeaCore.compose_url(request)
157
+
158
+ runtime_option = runtime_option or {}
159
+
160
+ verify = not runtime_option.get("ignoreSSL", False)
161
+ if verify:
162
+ verify = runtime_option.get("ca", True) if runtime_option.get("ca", True) is not None else True
163
+ cert = runtime_option.get("cert", None)
164
+
165
+ timeout = runtime_option.get("timeout")
166
+ connect_timeout = runtime_option.get("connectTimeout") or timeout or DEFAULT_CONNECT_TIMEOUT
167
+ read_timeout = runtime_option.get("readTimeout") or timeout or DEFAULT_READ_TIMEOUT
168
+
169
+ timeout = (int(connect_timeout) / 1000, int(read_timeout) / 1000)
170
+
171
+ if isinstance(request.body, str):
172
+ request.body = request.body.encode("utf-8")
173
+
174
+ p = PreparedRequest()
175
+ p.prepare(
176
+ method=request.method.upper(),
177
+ url=url,
178
+ data=request.body,
179
+ headers=request.headers,
180
+ )
181
+
182
+ proxies = {}
183
+ http_proxy = runtime_option.get("httpProxy")
184
+ https_proxy = runtime_option.get("httpsProxy")
185
+ no_proxy = runtime_option.get("noProxy")
186
+
187
+ if not http_proxy:
188
+ http_proxy = os.environ.get("HTTP_PROXY") or os.environ.get("http_proxy")
189
+ if not https_proxy:
190
+ https_proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy")
191
+
192
+ if http_proxy:
193
+ proxies["http"] = http_proxy
194
+ if https_proxy:
195
+ proxies["https"] = https_proxy
196
+ if no_proxy:
197
+ proxies["no_proxy"] = no_proxy
198
+
199
+ adapter = TeaCore.get_adapter(request.protocol)
200
+ try:
201
+ resp = adapter.send(
202
+ p,
203
+ proxies=proxies,
204
+ timeout=timeout,
205
+ verify=verify,
206
+ cert=cert,
207
+ )
208
+ except OSError as e:
209
+ raise RetryError(str(e))
210
+
211
+ debug = runtime_option.get("debug") or os.getenv("DEBUG")
212
+ if debug and debug.lower() == "sdk":
213
+ TeaCore._do_http_debug(p, resp)
214
+
215
+ response = TeaResponse()
216
+ response.status_message = resp.reason
217
+ response.status_code = resp.status_code
218
+ response.headers = {k.lower(): v for k, v in resp.headers.items()}
219
+ response.body = resp.content
220
+ response.response = resp
221
+ return response
222
+
223
+ @staticmethod
224
+ def get_response_body(resp) -> str:
225
+ return resp.content.decode("utf-8")
226
+
227
+ @staticmethod
228
+ def allow_retry(dic, retry_times, now=None) -> bool:
229
+ if retry_times == 0:
230
+ return True
231
+ if dic is None or not dic.__contains__("maxAttempts") or dic.get("retryable") is not True and retry_times >= 1:
232
+ return False
233
+ else:
234
+ retry = 0 if dic.get("maxAttempts") is None else int(dic.get("maxAttempts"))
235
+ return retry >= retry_times
236
+
237
+ @staticmethod
238
+ def get_backoff_time(dic, retry_times) -> int:
239
+ default_back_off_time = 0
240
+ if dic is None or not dic.get("policy") or dic.get("policy") == "no":
241
+ return default_back_off_time
242
+
243
+ back_off_time = dic.get("period", default_back_off_time)
244
+ if not isinstance(back_off_time, int) and not (isinstance(back_off_time, str) and back_off_time.isdigit()):
245
+ return default_back_off_time
246
+
247
+ back_off_time = int(back_off_time)
248
+ if back_off_time < 0:
249
+ return retry_times
250
+
251
+ return back_off_time
252
+
253
+ @staticmethod
254
+ async def sleep_async(t) -> None:
255
+ await asyncio.sleep(t)
256
+
257
+ @staticmethod
258
+ def sleep(t) -> None:
259
+ time.sleep(t)
260
+
261
+ @staticmethod
262
+ def is_retryable(ex) -> bool:
263
+ return isinstance(ex, RetryError)
264
+
265
+ @staticmethod
266
+ def bytes_readable(body):
267
+ return body
268
+
269
+ @staticmethod
270
+ def merge(*dic_list) -> dict:
271
+ dic_result = {}
272
+ for item in dic_list:
273
+ if isinstance(item, dict):
274
+ dic_result.update(item)
275
+ elif isinstance(item, TeaModel):
276
+ dic_result.update(item.to_map())
277
+ return dic_result
278
+
279
+ @staticmethod
280
+ def to_map(model: TeaModel | None) -> dict[str, Any]:
281
+ if isinstance(model, TeaModel):
282
+ return model.to_map()
283
+ else:
284
+ return dict()
285
+
286
+ @staticmethod
287
+ def from_map(model: TeaModel, dic: dict[str, Any]) -> TeaModel:
288
+ if isinstance(model, TeaModel):
289
+ try:
290
+ return model.from_map(dic)
291
+ except Exception:
292
+ model._map = dic
293
+ return model
294
+ else:
295
+ return model
@@ -12,6 +12,7 @@ class PathType(IntEnum):
12
12
  AREA = 0
13
13
  OBSTACLE = 1
14
14
  PATH = 2
15
+ LINE = 10
15
16
  DUMP = 12
16
17
  SVG = 13
17
18
 
@@ -83,6 +84,32 @@ class FrameList(DataClassORJSONMixin):
83
84
  data: list[NavGetCommData | SvgMessage] = field(default_factory=list)
84
85
 
85
86
 
87
+ @dataclass
88
+ class Plan(DataClassORJSONMixin):
89
+ sub_cmd: int = 2
90
+ version: str = ""
91
+ user_id: str = ""
92
+ device_id: str = ""
93
+ plan_id: str = ""
94
+ task_id: str = ""
95
+ start_time: str = "00:00"
96
+ knife_height: int = 0
97
+ model: int = 0
98
+ edge_mode: int = 0
99
+ route_model: int = 0
100
+ route_spacing: int = 0
101
+ ultrasonic_barrier: int = 0
102
+ total_plan_num: int = 0
103
+ speed: float = 0.0
104
+ task_name: str = ""
105
+ zone_hashs: list[str] = field(default_factory=list)
106
+ reserved: str = ""
107
+ start_date: str = ""
108
+ trigger_type: int = 0
109
+ remained_seconds: str = "-1"
110
+ toward_included_angle: int = 0
111
+
112
+
86
113
  @dataclass(eq=False, repr=False)
87
114
  class NavGetHashListData(DataClassORJSONMixin):
88
115
  """Dataclass for NavGetHashListData."""
@@ -124,8 +151,10 @@ class HashList(DataClassORJSONMixin):
124
151
  area: dict[int, FrameList] = field(default_factory=dict) # type 0
125
152
  path: dict[int, FrameList] = field(default_factory=dict) # type 2
126
153
  obstacle: dict[int, FrameList] = field(default_factory=dict) # type 1
127
- dump: dict[int, FrameList] = field(default_factory=dict) # type 12?
154
+ dump: dict[int, FrameList] = field(default_factory=dict) # type 12? / sub cmd 4
128
155
  svg: dict[int, FrameList] = field(default_factory=dict) # type 13
156
+ line: dict[int, FrameList] = field(default_factory=dict) # type 10 possibly breakpoint? / sub cmd 3
157
+ plan: dict[int, Plan] = field(default_factory=dict)
129
158
  area_name: list[AreaHashNameList] = field(default_factory=list)
130
159
 
131
160
  def update_hash_lists(self, hashlist: list[int]) -> None:
@@ -152,6 +181,8 @@ class HashList(DataClassORJSONMixin):
152
181
  all_hash_ids = set(self.area.keys()).union(
153
182
  self.path.keys(), self.obstacle.keys(), self.dump.keys(), self.svg.keys()
154
183
  )
184
+ if sub_cmd == 3:
185
+ all_hash_ids = set(self.line.keys())
155
186
  return [
156
187
  i
157
188
  for root_list in self.root_hash_lists
@@ -174,7 +205,7 @@ class HashList(DataClassORJSONMixin):
174
205
  if target_root_list is None:
175
206
  return []
176
207
 
177
- return self._find_missing_frames(target_root_list)
208
+ return self.find_missing_frames(target_root_list)
178
209
 
179
210
  def update_root_hash_list(self, hash_list: NavGetHashListData) -> None:
180
211
  target_root_list = next(
@@ -206,26 +237,29 @@ class HashList(DataClassORJSONMixin):
206
237
  missing_frames = []
207
238
  filtered_lists = [rl for rl in self.root_hash_lists if rl.sub_cmd == hash_ack.sub_cmd]
208
239
  for root_list in filtered_lists:
209
- missing = self._find_missing_frames(root_list)
240
+ missing = self.find_missing_frames(root_list)
210
241
  if missing:
211
242
  missing_frames.extend(missing)
212
243
  return missing_frames
213
244
 
214
245
  def missing_frame(self, hash_data: NavGetCommDataAck | SvgMessageAckT) -> list[int]:
215
246
  if hash_data.type == PathType.AREA:
216
- return self._find_missing_frames(self.area.get(hash_data.hash))
247
+ return self.find_missing_frames(self.area.get(hash_data.hash))
217
248
 
218
249
  if hash_data.type == PathType.OBSTACLE:
219
- return self._find_missing_frames(self.obstacle.get(hash_data.hash))
250
+ return self.find_missing_frames(self.obstacle.get(hash_data.hash))
220
251
 
221
252
  if hash_data.type == PathType.PATH:
222
- return self._find_missing_frames(self.path.get(hash_data.hash))
253
+ return self.find_missing_frames(self.path.get(hash_data.hash))
254
+
255
+ if hash_data.type == PathType.LINE:
256
+ return self.find_missing_frames(self.line.get(hash_data.hash))
223
257
 
224
258
  if hash_data.type == PathType.DUMP:
225
- return self._find_missing_frames(self.dump.get(hash_data.hash))
259
+ return self.find_missing_frames(self.dump.get(hash_data.hash))
226
260
 
227
261
  if hash_data.type == PathType.SVG:
228
- return self._find_missing_frames(self.svg.get(hash_data.data_hash))
262
+ return self.find_missing_frames(self.svg.get(hash_data.data_hash))
229
263
 
230
264
  return []
231
265
 
@@ -247,6 +281,9 @@ class HashList(DataClassORJSONMixin):
247
281
  if hash_data.type == PathType.PATH:
248
282
  return self._add_hash_data(self.path, hash_data)
249
283
 
284
+ if hash_data.type == PathType.LINE:
285
+ return self._add_hash_data(self.line, hash_data)
286
+
250
287
  if hash_data.type == PathType.DUMP:
251
288
  return self._add_hash_data(self.dump, hash_data)
252
289
 
@@ -256,7 +293,7 @@ class HashList(DataClassORJSONMixin):
256
293
  return False
257
294
 
258
295
  @staticmethod
259
- def _find_missing_frames(frame_list: FrameList | RootHashList) -> list[int]:
296
+ def find_missing_frames(frame_list: FrameList | RootHashList) -> list[int]:
260
297
  if frame_list is None:
261
298
  return []
262
299
 
pymammotion/http/http.py CHANGED
@@ -13,6 +13,8 @@ class MammotionHTTP:
13
13
  def __init__(self) -> None:
14
14
  self.code = None
15
15
  self.msg = None
16
+ self.account = None
17
+ self._password = None
16
18
  self.response: Response | None = None
17
19
  self.login_info: LoginResponseData | None = None
18
20
  self._headers = {"User-Agent": "okhttp/3.14.9", "App-Version": "google Pixel 2 XL taimen-Android 11,1.11.332"}
@@ -106,7 +108,12 @@ class MammotionHTTP:
106
108
  # Assuming the data format matches the expected structure
107
109
  return Response[StreamSubscriptionResponse].from_dict(data)
108
110
 
109
- async def login(self, username: str, password: str) -> Response[LoginResponseData]:
111
+ async def refresh_login(self) -> Response[LoginResponseData]:
112
+ return await self.login(self.account, self._password)
113
+
114
+ async def login(self, account: str, password: str) -> Response[LoginResponseData]:
115
+ self.account = account
116
+ self._password = password
110
117
  async with ClientSession(MAMMOTION_DOMAIN) as session:
111
118
  async with session.post(
112
119
  "/oauth/token",
@@ -118,7 +125,7 @@ class MammotionHTTP:
118
125
  "Ec-Version": "v1",
119
126
  },
120
127
  params=dict(
121
- username=self.encryption_utils.encryption_by_aes(username),
128
+ username=self.encryption_utils.encryption_by_aes(account),
122
129
  password=self.encryption_utils.encryption_by_aes(password),
123
130
  client_id=self.encryption_utils.encryption_by_aes(MAMMOTION_CLIENT_ID),
124
131
  client_secret=self.encryption_utils.encryption_by_aes(MAMMOTION_CLIENT_SECRET),
@@ -297,7 +297,7 @@ class MessageNavigation(AbstractMessage, ABC):
297
297
 
298
298
  def synchronize_hash_data(self, hash_num: int) -> bytes:
299
299
  build = MctlNav(todev_get_commondata=NavGetCommData(pver=1, action=8, hash=hash_num, sub_cmd=1))
300
- logger.debug(f"Send command--209,hash synchronize area data hash:{hash}")
300
+ logger.debug(f"Send command--209,hash synchronize area data hash:{hash_num}")
301
301
  return self.send_order_msg_nav(build)
302
302
 
303
303
  def get_area_to_be_transferred(self) -> bytes:
@@ -44,19 +44,19 @@ class MammotionBaseDevice:
44
44
  self._cloud_device = cloud_device
45
45
 
46
46
  async def datahash_response(self, hash_ack: NavGetHashListAck) -> None:
47
- """Handle datahash responses."""
47
+ """Handle datahash responses for root level hashs."""
48
+ current_frame = hash_ack.current_frame
48
49
 
49
50
  missing_frames = self.mower.map.missing_root_hash_frame(hash_ack)
50
51
  if len(missing_frames) == 0:
51
- for sub_cmd in [0, 3]:
52
- if len(self.mower.map.missing_hashlist(sub_cmd)) > 0:
53
- # data_hash = self.mower.map.missing_hashlist(hash_ack.sub_cmd).pop()
54
- for data_hash in self.mower.map.missing_hashlist(hash_ack.sub_cmd):
55
- await self.queue_command("synchronize_hash_data", hash_num=data_hash)
52
+ if len(self.mower.map.missing_hashlist(0)) > 0:
53
+ data_hash = self.mower.map.missing_hashlist(hash_ack.sub_cmd).pop()
54
+ await self.queue_command("synchronize_hash_data", hash_num=data_hash)
56
55
  return
57
56
 
58
- for frame in missing_frames:
59
- await self.queue_command("get_hash_response", total_frame=hash_ack.total_frame, current_frame=frame - 1)
57
+ if current_frame != missing_frames[0] - 1:
58
+ current_frame = missing_frames[0] - 1
59
+ await self.queue_command("get_hash_response", total_frame=hash_ack.total_frame, current_frame=current_frame)
60
60
 
61
61
  async def commdata_response(self, common_data: NavGetCommDataAck | SvgMessageAckT) -> None:
62
62
  """Handle common data responses."""
@@ -211,25 +211,49 @@ class MammotionBaseDevice:
211
211
 
212
212
  self.mower.map.update_hash_lists(self.mower.map.hashlist)
213
213
 
214
+ await self.queue_command("send_todev_ble_sync", sync_type=3)
215
+
214
216
  if self._cloud_device and len(self.mower.map.area_name) == 0 and not DeviceType.is_luba1(self.mower.name):
215
217
  await self.queue_command("get_area_name_list", device_id=self._cloud_device.iotId)
216
218
 
217
219
  await self.queue_command("read_plan", sub_cmd=2, plan_index=0)
218
220
 
219
- await self.queue_command("get_all_boundary_hash_list", sub_cmd=0)
221
+ if len(self.mower.map.root_hash_lists) == 0:
222
+ await self.queue_command("get_all_boundary_hash_list", sub_cmd=0)
223
+
224
+ for hash, frame in self.mower.map.area.items():
225
+ missing_frames = self.mower.map.find_missing_frames(frame)
226
+ if len(missing_frames) > 0:
227
+ del self.mower.map.area[hash]
228
+
229
+ for hash, frame in self.mower.map.path.items():
230
+ missing_frames = self.mower.map.find_missing_frames(frame)
231
+ if len(missing_frames) > 0:
232
+ del self.mower.map.path[hash]
233
+
234
+ for hash, frame in self.mower.map.obstacle.items():
235
+ missing_frames = self.mower.map.find_missing_frames(frame)
236
+ if len(missing_frames) > 0:
237
+ del self.mower.map.obstacle[hash]
238
+
239
+ # for hash, frame in self.mower.map.svg.items():
240
+ # missing_frames = self.mower.map.find_missing_frames(frame)
241
+ # if len(missing_frames) > 0:
242
+ # del self.mower.map.svg[hash]
243
+
220
244
  if len(self.mower.map.missing_hashlist()) > 0:
221
245
  data_hash = self.mower.map.missing_hashlist().pop()
222
246
  await self.queue_command("synchronize_hash_data", hash_num=data_hash)
223
247
 
224
- if len(self.mower.map.missing_hashlist(3)) > 0:
225
- data_hash = self.mower.map.missing_hashlist().pop()
226
- await self.queue_command("synchronize_hash_data", hash_num=data_hash)
248
+ # if len(self.mower.map.missing_hashlist(3)) > 0:
249
+ # data_hash = self.mower.map.missing_hashlist(3).pop()
250
+ # await self.queue_command("synchronize_hash_data", hash_num=data_hash)
227
251
 
228
252
  # sub_cmd 3 is job hashes??
229
253
  # sub_cmd 4 is dump location (yuka)
230
254
  # jobs list
231
255
  #
232
- # hash_list_result = await self._send_command_with_args("get_all_boundary_hash_list", sub_cmd=3)
256
+ # await self.queue_command("get_all_boundary_hash_list", sub_cmd=3)
233
257
 
234
258
  async def async_read_settings(self) -> None:
235
259
  """Read settings from device."""
@@ -187,6 +187,18 @@ class Mammotion:
187
187
  cloud_client = await self.login(account, password)
188
188
  await self.initiate_cloud_connection(account, cloud_client)
189
189
 
190
+ async def refresh_login(self, account: str) -> None:
191
+ async with self._login_lock:
192
+ exists: MammotionCloud | None = self.mqtt_list.get(account)
193
+ if not exists:
194
+ return
195
+ mammotion_http = exists.cloud_client.mammotion_http
196
+ await mammotion_http.refresh_login()
197
+ await self.connect_iot(mammotion_http, exists.cloud_client)
198
+ if not exists.is_connected():
199
+ loop = asyncio.get_running_loop()
200
+ await loop.run_in_executor(None, exists.connect_async)
201
+
190
202
  async def initiate_cloud_connection(self, account: str, cloud_client: CloudIOTGateway) -> None:
191
203
  loop = asyncio.get_running_loop()
192
204
  if mqtt := self.mqtt_list.get(account):
@@ -242,21 +254,19 @@ class Mammotion:
242
254
  cloud_client = CloudIOTGateway()
243
255
  mammotion_http = MammotionHTTP()
244
256
  await mammotion_http.login(account, password)
257
+ await self.connect_iot(mammotion_http, cloud_client)
258
+ await cloud_client.list_binding_by_account()
259
+ return cloud_client
260
+
261
+ @staticmethod
262
+ async def connect_iot(mammotion_http: MammotionHTTP, cloud_client: CloudIOTGateway) -> None:
245
263
  country_code = mammotion_http.login_info.userInformation.domainAbbreviation
246
- _LOGGER.debug("CountryCode: " + country_code)
247
- _LOGGER.debug("AuthCode: " + mammotion_http.login_info.authorization_code)
248
264
  cloud_client.set_http(mammotion_http)
249
- loop = asyncio.get_running_loop()
250
- await loop.run_in_executor(
251
- None, cloud_client.get_region, country_code, mammotion_http.login_info.authorization_code
252
- )
265
+ await cloud_client.get_region(country_code, mammotion_http.login_info.authorization_code)
253
266
  await cloud_client.connect()
254
267
  await cloud_client.login_by_oauth(country_code, mammotion_http.login_info.authorization_code)
255
- await loop.run_in_executor(None, cloud_client.aep_handle)
256
- await loop.run_in_executor(None, cloud_client.session_by_auth_code)
257
-
258
- await loop.run_in_executor(None, cloud_client.list_binding_by_account)
259
- return cloud_client
268
+ await cloud_client.aep_handle()
269
+ await cloud_client.session_by_auth_code()
260
270
 
261
271
  async def remove_device(self, name: str) -> None:
262
272
  await self.device_manager.remove_device(name)
@@ -106,7 +106,6 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
106
106
 
107
107
  async def _ble_sync(self) -> None:
108
108
  if self._client is not None and self._client.is_connected:
109
- _LOGGER.debug("BLE SYNC")
110
109
  command_bytes = self._commands.send_todev_ble_sync(2)
111
110
  await self._message.post_custom_data_bytes(command_bytes)
112
111
 
@@ -369,7 +368,7 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
369
368
  _LOGGER.debug("%s: Sending command: %s", self.name, key)
370
369
  await self._message.post_custom_data_bytes(command)
371
370
 
372
- timeout = 2
371
+ timeout = 1
373
372
  timeout_handle = self.loop.call_at(self.loop.time() + timeout, _handle_timeout, self._notify_future)
374
373
  timeout_expired = False
375
374
  try: