pymammotion 0.2.27__py3-none-any.whl → 0.2.29__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.
pymammotion/__init__.py CHANGED
@@ -9,14 +9,14 @@ import logging
9
9
  import os
10
10
 
11
11
  # works outside HA on its own
12
- from pymammotion.bluetooth.ble import LubaBLE
12
+ from pymammotion.bluetooth.ble import MammotionBLE
13
13
  from pymammotion.http.http import MammotionHTTP, connect_http
14
14
 
15
15
  # TODO make a working device that will work outside HA too.
16
16
  from pymammotion.mammotion.devices import MammotionBaseBLEDevice
17
17
  from pymammotion.mqtt import MammotionMQTT
18
18
 
19
- __all__ = ["LubaBLE", "MammotionHTTP", "connect_http", "MammotionBaseBLEDevice", "MammotionMQTT"]
19
+ __all__ = ["MammotionBLE", "MammotionHTTP", "connect_http", "MammotionBaseBLEDevice", "MammotionMQTT"]
20
20
 
21
21
  logger = logging.getLogger(__name__)
22
22
 
@@ -49,15 +49,19 @@ MOVE_HEADERS = (
49
49
  class SetupException(Exception):
50
50
  pass
51
51
 
52
+
52
53
  class AuthRefreshException(Exception):
53
54
  """Raise exception when library cannot refresh token."""
54
55
 
56
+
55
57
  class DeviceOfflineException(Exception):
56
58
  """Raise exception when device is offline."""
57
59
 
60
+
58
61
  class LoginException(Exception):
59
62
  """Raise exception when library cannot log in."""
60
63
 
64
+
61
65
  class CloudIOTGateway:
62
66
  """Class for interacting with Aliyun Cloud IoT Gateway."""
63
67
 
@@ -72,11 +76,19 @@ class CloudIOTGateway:
72
76
  _devices_by_account_response: ListingDevByAccountResponse | None = None
73
77
  _region_response = None
74
78
 
75
- _iot_token_issued_at : int = None
79
+ _iot_token_issued_at: int = None
76
80
 
77
81
  converter = DatatypeConverter()
78
82
 
79
- def __init__(self, connect_response: ConnectResponse | None = None, login_by_oauth_response: LoginByOAuthResponse | None = None, aep_response: AepResponse | None = None, session_by_authcode_response: SessionByAuthCodeResponse | None = None, region_response: RegionResponse | None = None, dev_by_account: ListingDevByAccountResponse | None = None):
83
+ def __init__(
84
+ self,
85
+ connect_response: ConnectResponse | None = None,
86
+ login_by_oauth_response: LoginByOAuthResponse | None = None,
87
+ aep_response: AepResponse | None = None,
88
+ session_by_authcode_response: SessionByAuthCodeResponse | None = None,
89
+ region_response: RegionResponse | None = None,
90
+ dev_by_account: ListingDevByAccountResponse | None = None,
91
+ ):
80
92
  """Initialize the CloudIOTGateway."""
81
93
  self.mammotion_http: MammotionHTTP | None = None
82
94
  self._app_key = APP_KEY
@@ -376,11 +388,8 @@ class CloudIOTGateway:
376
388
  async with session.post(
377
389
  f"https://{region_url}/api/prd/loginbyoauth.json",
378
390
  headers=headers,
379
- data={
380
- 'loginByOauthRequest': json.dumps(_bodyParam, separators=(",", ":"))
381
- }
391
+ data={"loginByOauthRequest": json.dumps(_bodyParam, separators=(",", ":"))},
382
392
  ) as resp:
383
-
384
393
  data = await resp.json()
385
394
  logger.debug(data)
386
395
  if resp.status == 200:
@@ -441,7 +450,7 @@ class CloudIOTGateway:
441
450
  raise Exception("Error in creating session: " + response_body_str)
442
451
 
443
452
  self._session_by_authcode_response = session_by_auth
444
- self._iot_token_issued_at = int(time.time())
453
+ self._iot_token_issued_at = int(time.time())
445
454
 
446
455
  return response.body
447
456
 
@@ -491,18 +500,23 @@ class CloudIOTGateway:
491
500
  response_body_dict = json.loads(response_body_str)
492
501
 
493
502
  if int(response_body_dict.get("code")) != 200:
494
- raise Exception("Error check or refresh token: " + response_body_dict.get('msg', ''))
503
+ logger.error(response_body_dict)
504
+ raise Exception("Error check or refresh token: " + response_body_dict.__str__())
495
505
 
496
506
  session = SessionByAuthCodeResponse.from_dict(response_body_dict)
497
507
  session_data = session.data
498
508
 
499
- if session_data.identityId is None or session_data.refreshTokenExpire is None or session_data.iotToken is None or session_data.iotTokenExpire is None or session_data.refreshToken is None:
509
+ if (
510
+ session_data.identityId is None
511
+ or session_data.refreshTokenExpire is None
512
+ or session_data.iotToken is None
513
+ or session_data.iotTokenExpire is None
514
+ or session_data.refreshToken is None
515
+ ):
500
516
  raise Exception("Error check or refresh token: Parameters not correct")
501
517
 
502
518
  self._session_by_authcode_response = session
503
- self._iot_token_issued_at = int(time.time())
504
-
505
-
519
+ self._iot_token_issued_at = int(time.time())
506
520
 
507
521
  def list_binding_by_account(self) -> ListingDevByAccountResponse:
508
522
  """List bindings by account."""
@@ -550,14 +564,16 @@ class CloudIOTGateway:
550
564
  """Send a cloud command to the specified IoT device."""
551
565
 
552
566
  """Check if iotToken is expired"""
553
- if self._iot_token_issued_at + self._session_by_authcode_response.data.iotTokenExpire <= (int(time.time()) + (5 * 3600)):
567
+ if self._iot_token_issued_at + self._session_by_authcode_response.data.iotTokenExpire <= (
568
+ int(time.time()) + (5 * 3600)
569
+ ):
554
570
  """Token expired - Try to refresh - Check if refreshToken is not expired"""
555
- if self._iot_token_issued_at + self._session_by_authcode_response.data.refreshTokenExpire > (int(time.time())):
571
+ if self._iot_token_issued_at + self._session_by_authcode_response.data.refreshTokenExpire > (
572
+ int(time.time())
573
+ ):
556
574
  self.check_or_refresh_session()
557
575
  else:
558
576
  raise AuthRefreshException("Refresh token expired. Please re-login")
559
-
560
-
561
577
 
562
578
  config = Config(
563
579
  app_key=self._app_key,
@@ -29,7 +29,6 @@ class Device(DataClassORJSONMixin):
29
29
  categoryImage: Optional[str] = None
30
30
  productModel: Optional[str] = None
31
31
 
32
-
33
32
  class Config(BaseConfig):
34
33
  omit_default = True
35
34
 
@@ -12,6 +12,7 @@ class SessionOauthToken(DataClassORJSONMixin):
12
12
  iotTokenExpire: int
13
13
  refreshToken: str
14
14
 
15
+
15
16
  @dataclass
16
17
  class SessionByAuthCodeResponse(DataClassORJSONMixin):
17
18
  code: int
@@ -11,7 +11,7 @@ from pymammotion.event.event import BleNotificationEvent
11
11
  address = "90:38:0C:6E:EE:9E"
12
12
 
13
13
 
14
- class LubaBLE:
14
+ class MammotionBLE:
15
15
  client: BleakClient
16
16
 
17
17
  def __init__(self, bleEvt: BleNotificationEvent):
@@ -5,4 +5,4 @@ from dataclasses import dataclass
5
5
  class Credentials:
6
6
  email: str = None
7
7
  password: str = None
8
- account_id: str = None
8
+ account_id: str = None
@@ -16,8 +16,13 @@ from pymammotion.proto.mctrl_driver import MctlDriver
16
16
  from pymammotion.proto.mctrl_nav import MctlNav
17
17
  from pymammotion.proto.mctrl_ota import MctlOta
18
18
  from pymammotion.proto.mctrl_pept import MctlPept
19
- from pymammotion.proto.mctrl_sys import MctlSys, MowToAppInfoT, ReportInfoData, SystemUpdateBufMsg, \
20
- SystemRapidStateTunnelMsg
19
+ from pymammotion.proto.mctrl_sys import (
20
+ MctlSys,
21
+ MowToAppInfoT,
22
+ ReportInfoData,
23
+ SystemUpdateBufMsg,
24
+ SystemRapidStateTunnelMsg,
25
+ )
21
26
  from pymammotion.utility.constant import WorkMode
22
27
  from pymammotion.utility.conversions import parse_double
23
28
  from pymammotion.utility.map import CoordinateConverter
@@ -105,9 +110,9 @@ class MowingDevice:
105
110
  parse_double(location.real_pos_y, 4.0), parse_double(location.real_pos_x, 4.0)
106
111
  )
107
112
  if location.zone_hash:
108
- self.location.work_zone = location.zone_hash if self.report_data.dev.sys_status == WorkMode.MODE_WORKING else 0
109
-
110
-
113
+ self.location.work_zone = (
114
+ location.zone_hash if self.report_data.dev.sys_status == WorkMode.MODE_WORKING else 0
115
+ )
111
116
 
112
117
  self.report_data = self.report_data.from_dict(toapp_report_data.to_dict(casing=betterproto.Casing.SNAKE))
113
118
 
@@ -1,5 +1,7 @@
1
1
  from dataclasses import dataclass
2
2
 
3
+ from pymammotion.utility.device_type import DeviceType
4
+
3
5
 
4
6
  @dataclass
5
7
  class DeviceLimits:
@@ -7,3 +9,72 @@ class DeviceLimits:
7
9
  blade_height_max: int
8
10
  working_speed_min: float
9
11
  working_speed_max: float
12
+
13
+
14
+ @dataclass
15
+ class OperationSettings:
16
+ """Operation settings for a device."""
17
+
18
+ is_mow: bool = True
19
+ is_dump: bool = True
20
+ is_edge: bool = False
21
+ collect_grass_frequency: int = 10
22
+ job_mode: int = 0 # taskMode
23
+ job_version: int = 0
24
+ job_id: int = 0
25
+ speed: float = 0.3
26
+ ultra_wave: int = 2 # touch no touch etc
27
+ channel_mode: int = 0 # line mode is grid single double or single2
28
+ channel_width: int = 25
29
+ rain_tactics: int = 0
30
+ blade_height: int = 0
31
+ path_order: str = ""
32
+ toward: int = 0 # is just angle
33
+ toward_included_angle: int = 0
34
+ toward_mode: int = 0 # angle type relative etc
35
+ border_mode: int = 1 # border laps
36
+ obstacle_laps: int = 1
37
+ start_progress: int = 0
38
+
39
+
40
+ def create_path_order(operation_mode: OperationSettings, device_name: str) -> str:
41
+ i = 8
42
+ bArr = bytearray(8)
43
+ bArr[0] = operation_mode.border_mode
44
+ bArr[1] = operation_mode.obstacle_laps
45
+ bArr[3] = operation_mode.start_progress
46
+ bArr[2] = 0
47
+
48
+ if not DeviceType.is_luba1(device_name):
49
+ bArr[4] = 0
50
+ if DeviceType.is_yuka(device_name):
51
+ i = calculate_yuka_mode(operation_mode)
52
+ elif not DeviceType.is_luba_2(device_name):
53
+ i = 0
54
+ bArr[5] = i
55
+ if operation_mode.is_dump:
56
+ b = operation_mode.collect_grass_frequency
57
+ else:
58
+ b = 10
59
+ bArr[6] = b
60
+ if DeviceType.is_luba1(device_name):
61
+ bArr[4] = operation_mode.toward_mode
62
+ return str(bArr, "UTF-8")
63
+
64
+
65
+ def calculate_yuka_mode(operation_mode: OperationSettings):
66
+ if operation_mode.is_mow and operation_mode.is_dump and operation_mode.is_edge:
67
+ return 14
68
+ if operation_mode.is_mow and operation_mode.is_dump and not operation_mode.is_edge:
69
+ return 12
70
+ if operation_mode.is_mow and not operation_mode.is_dump and operation_mode.is_edge:
71
+ return 10
72
+ if operation_mode.is_mow and not operation_mode.is_dump and not operation_mode.is_edge:
73
+ return 8
74
+ if not operation_mode.is_mow and operation_mode.is_dump and operation_mode.is_edge:
75
+ return 6
76
+ if not operation_mode.is_mow and not operation_mode.is_dump and operation_mode.is_edge:
77
+ return 2
78
+ if not operation_mode.is_mow and operation_mode.is_dump and not operation_mode.is_edge:
79
+ return 4
80
+ return 0
@@ -1,133 +1,27 @@
1
1
  import logging
2
+ from dataclasses import dataclass
2
3
  from typing import List
3
4
 
4
5
  logger = logging.getLogger(__name__)
5
6
 
6
7
 
8
+ @dataclass
7
9
  class GenerateRouteInformation:
8
10
  """Creates a model for generating route information and mowing plan before starting a job."""
9
11
 
10
- def __init__(
11
- self,
12
- one_hashs: List[int],
13
- job_mode: int,
14
- channel_width: int,
15
- speed: float,
16
- ultra_wave: int,
17
- channel_mode: int,
18
- rain_tactics: int,
19
- toward: int,
20
- knife_height: int,
21
- path_order: str,
22
- toward_included_angle: int,
23
- ):
24
- self.path_order = ""
25
- self.toward_mode = 0
26
- self.one_hashs = one_hashs
27
- self.rain_tactics = rain_tactics
28
- self.job_mode = job_mode
29
- self.knife_height = knife_height
30
- self.speed = speed
31
- self.ultra_wave = ultra_wave
32
- self.channel_width = channel_width
33
- self.channel_mode = channel_mode
34
- self.toward = toward
35
- self.edge_mode = rain_tactics
36
- self.path_order = path_order
37
- self.toward_included_angle = toward_included_angle
38
- logger.debug(
39
- f"Mode route command parameters jobMode={job_mode}//channelWidth={channel_width}//speed={speed}//UltraWave={ultra_wave}//channelMode={channel_mode}//edgeMode={rain_tactics}//knifeHeight={knife_height} pathOrder:{path_order.encode('utf-8')}"
40
- )
41
-
42
- def get_job_id(self) -> int:
43
- return self.job_id
44
-
45
- def set_job_id(self, job_id: int) -> None:
46
- self.job_id = job_id
47
-
48
- def get_job_ver(self) -> int:
49
- return self.job_ver
50
-
51
- def set_job_ver(self, job_ver: int) -> None:
52
- self.job_ver = job_ver
53
-
54
- def get_rain_tactics(self) -> int:
55
- return self.rain_tactics
56
-
57
- def set_rain_tactics(self, rain_tactics: int) -> None:
58
- self.rain_tactics = rain_tactics
59
-
60
- def get_job_mode(self) -> int:
61
- return self.job_mode
62
-
63
- def set_job_mode(self, job_mode: int) -> None:
64
- self.job_mode = job_mode
65
-
66
- def get_knife_height(self) -> int:
67
- return self.knife_height
68
-
69
- def set_knife_height(self, knife_height: int) -> None:
70
- self.knife_height = knife_height
71
-
72
- def get_speed(self) -> float:
73
- return self.speed
74
-
75
- def set_speed(self, speed: float) -> None:
76
- self.speed = speed
77
-
78
- def get_ultra_wave(self) -> int:
79
- return self.ultra_wave
80
-
81
- def set_ultra_wave(self, ultra_wave: int) -> None:
82
- self.ultra_wave = ultra_wave
83
-
84
- def get_channel_width(self) -> int:
85
- return self.channel_width
86
-
87
- def set_channel_width(self, channel_width: int) -> None:
88
- self.channel_width = channel_width
89
-
90
- def get_channel_mode(self) -> int:
91
- return self.channel_mode
92
-
93
- def set_channel_mode(self, channel_mode: int) -> None:
94
- self.channel_mode = channel_mode
95
-
96
- def get_toward(self) -> int:
97
- return self.toward
98
-
99
- def set_toward(self, toward: int) -> None:
100
- self.toward = toward
101
-
102
- def get_one_hashs(self) -> List[int]:
103
- return self.one_hashs if self.one_hashs else []
104
-
105
- def set_one_hashs(self, one_hashs: List[int]) -> None:
106
- self.one_hashs = one_hashs
107
-
108
- def get_path_order(self) -> str:
109
- return self.path_order
110
-
111
- def set_path_order(self, path_order: str) -> None:
112
- self.path_order = path_order
113
-
114
- def get_toward_included_angle(self) -> int:
115
- return self.toward_included_angle
116
-
117
- def set_toward_included_angle(self, toward_included_angle: int) -> None:
118
- self.toward_included_angle = toward_included_angle
119
-
120
- def get_toward_mode(self) -> int:
121
- return self.toward_mode
122
-
123
- def get_edge_mode(self) -> int:
124
- return self.edge_mode
125
-
126
- def set_edge_mode(self, edge_mode: int) -> None:
127
- self.edge_mode = edge_mode
128
-
129
- def __str__(self) -> str:
130
- try:
131
- return f"GenerateRouteInformation{{oneHashs={self.one_hashs}, jobId={self.job_id}, jobVer={self.job_ver}, rainTactics={self.rain_tactics}, jobMode={self.job_mode}, knifeHeight={self.knife_height}, speed={self.speed}, UltraWave={self.ultra_wave}, channelWidth={self.channel_width}, channelMode={self.channel_mode}, toward={self.toward}, pathOrder='{self.path_order.encode('utf-8')}', edgeMode={self.edge_mode}, towardIncludedAngle={self.toward_included_angle}}}"
132
- except Exception as e:
133
- return str(e)
12
+ one_hashs: list[int] = list
13
+ job_mode: int = 0 # taskMode
14
+ job_version: int = 0
15
+ job_id: int = 0
16
+ speed: float = 0.3
17
+ ultra_wave: int = 2 # touch no touch etc
18
+ channel_mode: int = 0 # line mode is grid single double or single2
19
+ channel_width: int = 25
20
+ rain_tactics: int = 0
21
+ blade_height: int = 0
22
+ path_order: str = ""
23
+ toward: int = 0 # is just angle
24
+ toward_included_angle: int = 0
25
+ toward_mode: int = 0 # angle type relative etc
26
+ edge_mode: int = 1 # border laps
27
+ obstacle_laps: int = 1
@@ -6,10 +6,12 @@ from pymammotion.proto.mctrl_nav import NavGetCommDataAck
6
6
 
7
7
  class PathType(IntEnum):
8
8
  """Path types for common data."""
9
+
9
10
  AREA = 0
10
11
  OBSTACLE = 1
11
12
  PATH = 2
12
13
 
14
+
13
15
  @dataclass
14
16
  class FrameList:
15
17
  total_frame: int
@@ -34,7 +36,6 @@ class HashList:
34
36
  self.path = {hash_id: frames for hash_id, frames in self.path.items() if hash_id in hashlist}
35
37
  self.obstacle = {hash_id: frames for hash_id, frames in self.obstacle.items() if hash_id in hashlist}
36
38
 
37
-
38
39
  def missing_frame(self, hash_data: NavGetCommDataAck) -> list[int]:
39
40
  if hash_data.type == PathType.AREA:
40
41
  return self._find_missing_frames(self.area.get(hash_data.hash))
@@ -45,8 +46,6 @@ class HashList:
45
46
  if hash_data.type == PathType.PATH:
46
47
  return self._find_missing_frames(self.path.get(hash_data.hash))
47
48
 
48
-
49
-
50
49
  def update(self, hash_data: NavGetCommDataAck) -> bool:
51
50
  """Update the map data."""
52
51
  if hash_data.type == PathType.AREA:
@@ -58,18 +57,16 @@ class HashList:
58
57
  if hash_data.type == PathType.PATH:
59
58
  return self._add_hash_data(self.path, hash_data)
60
59
 
61
-
62
60
  @staticmethod
63
61
  def _find_missing_frames(frame_list: FrameList) -> list[int]:
64
62
  if frame_list.total_frame == len(frame_list.data):
65
63
  return []
66
- number_list = list(range(1, frame_list.total_frame+1))
64
+ number_list = list(range(1, frame_list.total_frame + 1))
67
65
 
68
66
  current_frames = {frame.current_frame for frame in frame_list.data}
69
67
  missing_numbers = [num for num in number_list if num not in current_frames]
70
68
  return missing_numbers
71
69
 
72
-
73
70
  @staticmethod
74
71
  def _add_hash_data(hash_dict: dict, hash_data: NavGetCommDataAck) -> bool:
75
72
  if hash_dict.get(hash_data.hash) is None:
@@ -30,7 +30,7 @@ class Location:
30
30
  RTK: Point
31
31
  dock: Dock
32
32
  position_type: int = 0
33
- orientation: int = 0 # 360 degree rotation +-
33
+ orientation: int = 0 # 360 degree rotation +-
34
34
  work_zone: int = 0
35
35
 
36
36
  def __init__(self):
@@ -44,3 +44,11 @@ class BypassStrategy(IntEnum):
44
44
  slow_touch = 1
45
45
  less_touch = 2
46
46
  no_touch = 3 # luba 2 yuka only or possibly value of 10
47
+
48
+
49
+ class PathAngleSetting(IntEnum):
50
+ """Path Angle type."""
51
+
52
+ relative_angle = 0
53
+ absolute_angle = 1
54
+ random_angle = 2 # Luba Pro / Luba 2 Yuka only
@@ -45,23 +45,25 @@ class DeviceWarningEventValue(DataClassORJSONMixin):
45
45
  # (see resources/res/values-en-rUS/strings.xml in APK)
46
46
  code: int
47
47
 
48
+
48
49
  @dataclass
49
50
  class DeviceConfigurationRequestValue(DataClassORJSONMixin):
50
51
  code: int
51
52
  bizId: str
52
53
  params: str
53
54
 
55
+
54
56
  @dataclass
55
57
  class DeviceNotificationEventCode(DataClassORJSONMixin):
56
58
  localTime: int
57
59
  code: str
58
60
 
61
+
59
62
  @dataclass
60
63
  class DeviceNotificationEventValue(DataClassORJSONMixin):
61
64
  data: DeviceNotificationEventCode
62
65
 
63
66
 
64
-
65
67
  @dataclass
66
68
  class GeneralParams(DataClassORJSONMixin):
67
69
  groupIdList: list[str]
@@ -100,6 +102,7 @@ class DeviceProtobufMsgEventParams(GeneralParams):
100
102
  type: Literal["info"]
101
103
  value: DeviceProtobufMsgEventValue
102
104
 
105
+
103
106
  @dataclass
104
107
  class DeviceNotificationEventParams(GeneralParams):
105
108
  """Device notification event.
@@ -111,12 +114,14 @@ class DeviceNotificationEventParams(GeneralParams):
111
114
  type: Literal["info"]
112
115
  value: DeviceNotificationEventValue
113
116
 
117
+
114
118
  @dataclass
115
119
  class DeviceWarningEventParams(GeneralParams):
116
120
  identifier: Literal["device_warning_event"]
117
121
  type: Literal["alert"]
118
122
  value: DeviceWarningEventValue
119
123
 
124
+
120
125
  @dataclass
121
126
  class DeviceConfigurationRequestEvent(GeneralParams):
122
127
  type: Literal["info"]
@@ -127,7 +132,7 @@ class DeviceConfigurationRequestEvent(GeneralParams):
127
132
  class ThingEventMessage(DataClassORJSONMixin):
128
133
  method: Literal["thing.events", "thing.properties"]
129
134
  id: str
130
- params: Union[DeviceProtobufMsgEventParams, DeviceWarningEventParams, str]
135
+ params: Union[DeviceProtobufMsgEventParams, DeviceWarningEventParams, dict]
131
136
  version: Literal["1.0"]
132
137
 
133
138
  @classmethod
@@ -1,4 +1,5 @@
1
1
  """Manage state from notifications into MowingDevice."""
2
+
2
3
  from typing import Optional, Callable, Awaitable
3
4
 
4
5
  import betterproto
@@ -15,9 +16,9 @@ class StateManager:
15
16
 
16
17
  def __init__(self, device: MowingDevice):
17
18
  self._device = device
18
- self.gethash_ack_callback: Optional[Callable[[NavGetHashListAck],Awaitable[None]]] = None
19
- self.get_commondata_ack_callback: Optional[Callable[[NavGetCommDataAck],Awaitable[None]]] = None
20
- self.on_notification_callback: Optional[Callable[[],Awaitable[None]]] = None
19
+ self.gethash_ack_callback: Optional[Callable[[NavGetHashListAck], Awaitable[None]]] = None
20
+ self.get_commondata_ack_callback: Optional[Callable[[NavGetCommDataAck], Awaitable[None]]] = None
21
+ self.on_notification_callback: Optional[Callable[[], Awaitable[None]]] = None
21
22
 
22
23
  def get_device(self) -> MowingDevice:
23
24
  """Get device."""
pymammotion/http/http.py CHANGED
@@ -54,6 +54,16 @@ class MammotionHTTP:
54
54
  self.msg = response.msg
55
55
  self.code = response.code
56
56
 
57
+ async def get_all_error_codes(self):
58
+ async with ClientSession() as session:
59
+ async with session.post(
60
+ "code/record/export-data",
61
+ headers=self._headers,
62
+ ) as resp:
63
+ if resp.status == 200:
64
+ data = await resp.json()
65
+ print(resp)
66
+
57
67
  @classmethod
58
68
  async def login(cls, session: ClientSession, username: str, password: str) -> Response[LoginResponseData]:
59
69
  async with session.post(
@@ -14,6 +14,7 @@ bleNotificationEvt = BleNotificationEvent()
14
14
 
15
15
  nest_asyncio.apply()
16
16
 
17
+
17
18
  class JoystickControl:
18
19
  """Joystick class for controlling Luba with a joystick"""
19
20
 
@@ -133,13 +133,19 @@ class ConnectionPreference(Enum):
133
133
  WIFI = 1
134
134
  BLUETOOTH = 2
135
135
 
136
+
136
137
  class MammotionMixedDeviceManager:
137
138
  _ble_device: MammotionBaseBLEDevice | None = None
138
139
  _cloud_device: MammotionBaseCloudDevice | None = None
139
140
  _mowing_state: MowingDevice = MowingDevice()
140
141
 
141
- def __init__(self, name: str, cloud_device: Device | None = None,
142
- ble_device: BLEDevice | None = None, mqtt: MammotionMQTT | None = None) -> None:
142
+ def __init__(
143
+ self,
144
+ name: str,
145
+ cloud_device: Device | None = None,
146
+ ble_device: BLEDevice | None = None,
147
+ mqtt: MammotionMQTT | None = None,
148
+ ) -> None:
143
149
  self.name = name
144
150
  self.add_ble(ble_device)
145
151
  self.add_cloud(cloud_device, mqtt)
@@ -160,14 +166,13 @@ class MammotionMixedDeviceManager:
160
166
  def add_cloud(self, cloud_device: Device | None = None, mqtt: MammotionMQTT | None = None) -> None:
161
167
  if cloud_device is not None:
162
168
  self._cloud_device = MammotionBaseCloudDevice(
163
- mqtt_client=mqtt,
164
- cloud_device=cloud_device,
165
- mowing_state=self._mowing_state)
169
+ mqtt_client=mqtt, cloud_device=cloud_device, mowing_state=self._mowing_state
170
+ )
166
171
 
167
- def replace_cloud(self, cloud_device:MammotionBaseCloudDevice) -> None:
172
+ def replace_cloud(self, cloud_device: MammotionBaseCloudDevice) -> None:
168
173
  self._cloud_device = cloud_device
169
174
 
170
- def replace_ble(self, ble_device:MammotionBaseBLEDevice) -> None:
175
+ def replace_ble(self, ble_device: MammotionBaseBLEDevice) -> None:
171
176
  self._ble_device = ble_device
172
177
 
173
178
  def has_cloud(self) -> bool:
@@ -178,7 +183,6 @@ class MammotionMixedDeviceManager:
178
183
 
179
184
 
180
185
  class MammotionDevices:
181
-
182
186
  devices: dict[str, MammotionMixedDeviceManager] = {}
183
187
 
184
188
  def add_device(self, mammotion_device: MammotionMixedDeviceManager) -> None:
@@ -194,14 +198,18 @@ class MammotionDevices:
194
198
  def get_device(self, mammotion_device_name: str) -> MammotionMixedDeviceManager:
195
199
  return self.devices.get(mammotion_device_name)
196
200
 
197
- async def create_devices(ble_device: BLEDevice,
201
+
202
+ async def create_devices(
203
+ ble_device: BLEDevice,
198
204
  cloud_credentials: Credentials | None = None,
199
- preference: ConnectionPreference = ConnectionPreference.BLUETOOTH):
205
+ preference: ConnectionPreference = ConnectionPreference.BLUETOOTH,
206
+ ):
200
207
  mammotion = Mammotion(ble_device, preference)
201
208
 
202
209
  if cloud_credentials and preference == ConnectionPreference.EITHER or preference == ConnectionPreference.WIFI:
203
- cloud_client = await Mammotion.login(cloud_credentials.account_id or cloud_credentials.email,
204
- cloud_credentials.password)
210
+ cloud_client = await Mammotion.login(
211
+ cloud_credentials.account_id or cloud_credentials.email, cloud_credentials.password
212
+ )
205
213
  await mammotion.initiate_cloud_connection(cloud_client)
206
214
 
207
215
  return mammotion
@@ -215,12 +223,8 @@ class Mammotion(object):
215
223
  cloud_client: CloudIOTGateway | None = None
216
224
  mqtt: MammotionMQTT | None = None
217
225
 
218
-
219
-
220
226
  def __init__(
221
- self,
222
- ble_device: BLEDevice,
223
- preference: ConnectionPreference = ConnectionPreference.BLUETOOTH
227
+ self, ble_device: BLEDevice, preference: ConnectionPreference = ConnectionPreference.BLUETOOTH
224
228
  ) -> None:
225
229
  """Initialize MammotionDevice."""
226
230
  if ble_device:
@@ -235,20 +239,24 @@ class Mammotion(object):
235
239
  return
236
240
 
237
241
  self.cloud_client = cloud_client
238
- self.mqtt = MammotionMQTT(region_id=cloud_client._region_response.data.regionId,
239
- product_key=cloud_client._aep_response.data.productKey,
240
- device_name=cloud_client._aep_response.data.deviceName,
241
- device_secret=cloud_client._aep_response.data.deviceSecret,
242
- iot_token=cloud_client._session_by_authcode_response.data.iotToken,
243
- client_id=cloud_client._client_id)
242
+ self.mqtt = MammotionMQTT(
243
+ region_id=cloud_client._region_response.data.regionId,
244
+ product_key=cloud_client._aep_response.data.productKey,
245
+ device_name=cloud_client._aep_response.data.deviceName,
246
+ device_secret=cloud_client._aep_response.data.deviceSecret,
247
+ iot_token=cloud_client._session_by_authcode_response.data.iotToken,
248
+ client_id=cloud_client._client_id,
249
+ )
244
250
 
245
251
  self.mqtt._cloud_client = cloud_client
246
252
  loop = asyncio.get_running_loop()
247
253
  await loop.run_in_executor(None, self.mqtt.connect_async)
248
254
 
249
255
  for device in cloud_client.listing_dev_by_account_response.data.data:
250
- if device.deviceName.startswith(("Luba-", "Yuka-")):
251
- self.devices.add_device(MammotionMixedDeviceManager(name=device.deviceName, cloud_device=device, mqtt=self.mqtt))
256
+ if device.deviceName.startswith(("Luba-", "Yuka-")) and self.devices.get_device(device.deviceName) is None:
257
+ self.devices.add_device(
258
+ MammotionMixedDeviceManager(name=device.deviceName, cloud_device=device, mqtt=self.mqtt)
259
+ )
252
260
 
253
261
  def set_disconnect_strategy(self, disconnect: bool):
254
262
  for device_name, device in self.devices.devices:
@@ -267,7 +275,9 @@ class Mammotion(object):
267
275
  _LOGGER.debug("AuthCode: " + mammotion_http.login_info.authorization_code)
268
276
  loop = asyncio.get_running_loop()
269
277
  cloud_client.set_http(mammotion_http)
270
- await loop.run_in_executor(None, cloud_client.get_region, country_code, mammotion_http.login_info.authorization_code)
278
+ await loop.run_in_executor(
279
+ None, cloud_client.get_region, country_code, mammotion_http.login_info.authorization_code
280
+ )
271
281
  await cloud_client.connect()
272
282
  await cloud_client.login_by_oauth(country_code, mammotion_http.login_info.authorization_code)
273
283
  await loop.run_in_executor(None, cloud_client.aep_handle)
@@ -276,7 +286,6 @@ class Mammotion(object):
276
286
  await loop.run_in_executor(None, cloud_client.list_binding_by_account)
277
287
  return cloud_client
278
288
 
279
-
280
289
  def get_device_by_name(self, name: str) -> MammotionMixedDeviceManager:
281
290
  return self.devices.get_device(name)
282
291
 
@@ -290,7 +299,7 @@ class Mammotion(object):
290
299
  return await device.cloud().command(key)
291
300
  # TODO work with both with EITHER
292
301
 
293
- async def send_command_with_args(self,name: str, key: str, **kwargs: any):
302
+ async def send_command_with_args(self, name: str, key: str, **kwargs: any):
294
303
  """Send a command with args to the device."""
295
304
  device = self.get_device_by_name(name)
296
305
  if device:
@@ -300,7 +309,7 @@ class Mammotion(object):
300
309
  return await device.cloud().command(key, **kwargs)
301
310
  # TODO work with both with EITHER
302
311
 
303
- async def start_sync(self, name:str, retry: int):
312
+ async def start_sync(self, name: str, retry: int):
304
313
  device = self.get_device_by_name(name)
305
314
  if device:
306
315
  if self._preference is ConnectionPreference.BLUETOOTH:
@@ -309,7 +318,7 @@ class Mammotion(object):
309
318
  return await device.cloud().start_sync(retry)
310
319
  # TODO work with both with EITHER
311
320
 
312
- async def start_map_sync(self, name:str):
321
+ async def start_map_sync(self, name: str):
313
322
  device = self.get_device_by_name(name)
314
323
  if device:
315
324
  if self._preference is ConnectionPreference.BLUETOOTH:
@@ -323,6 +332,7 @@ class Mammotion(object):
323
332
  if device:
324
333
  return device.mower_state()
325
334
 
335
+
326
336
  def has_field(message: betterproto.Message) -> bool:
327
337
  """Check if the message has any fields serialized on wire."""
328
338
  return betterproto.serialized_on_wire(message)
@@ -346,7 +356,7 @@ class MammotionBaseDevice:
346
356
  self._notify_future: asyncio.Future[bytes] | None = None
347
357
  self._cloud_device = cloud_device
348
358
 
349
- def set_notification_callback(self, func: Callable[[],Awaitable[None]]):
359
+ def set_notification_callback(self, func: Callable[[], Awaitable[None]]):
350
360
  self._state_manager.on_notification_callback = func
351
361
 
352
362
  async def datahash_response(self, hash_ack: NavGetHashListAck):
@@ -368,8 +378,8 @@ class MammotionBaseDevice:
368
378
 
369
379
  await self.queue_command("synchronize_hash_data", hash_num=data_hash)
370
380
  else:
371
- if current_frame != missing_frames[0]-1:
372
- current_frame = missing_frames[0]-1
381
+ if current_frame != missing_frames[0] - 1:
382
+ current_frame = missing_frames[0] - 1
373
383
 
374
384
  region_data = RegionData()
375
385
  region_data.hash = common_data.hash
@@ -476,7 +486,7 @@ class MammotionBaseDevice:
476
486
  return self._mower
477
487
 
478
488
  @abstractmethod
479
- async def queue_command(self, key: str, **kwargs: any) -> bytes:
489
+ async def queue_command(self, key: str, **kwargs: any) -> bytes:
480
490
  """Queue commands to mower."""
481
491
 
482
492
  @abstractmethod
@@ -507,28 +517,22 @@ class MammotionBaseDevice:
507
517
 
508
518
  await self.queue_command("get_hash_response", total_frame=1, current_frame=1)
509
519
 
510
-
511
520
  # work out why this crashes sometimes for better proto
512
521
  if self._cloud_device:
513
- await self.queue_command(
514
- "get_area_name_list", device_id=self._cloud_device.deviceName
515
- )
522
+ await self.queue_command("get_area_name_list", device_id=self._cloud_device.deviceName)
516
523
  if has_field(self._mower.net.toapp_wifi_iot_status):
517
- await self.queue_command(
518
- "get_area_name_list", device_id=self._mower.net.toapp_wifi_iot_status.devicename
519
- )
520
-
524
+ await self.queue_command("get_area_name_list", device_id=self._mower.net.toapp_wifi_iot_status.devicename)
521
525
 
522
526
  # sub_cmd 3 is job hashes??
523
527
  # sub_cmd 4 is dump location (yuka)
524
528
  # jobs list
525
529
  # hash_list_result = await self._send_command_with_args("get_all_boundary_hash_list", sub_cmd=3)
530
+
526
531
  async def async_get_errors(self):
527
532
  """Error codes."""
528
533
  await self.queue_command("allpowerfull_rw", id=5, rw=1, context=2)
529
534
  await self.queue_command("allpowerfull_rw", id=5, rw=1, context=3)
530
535
 
531
-
532
536
  async def move_forward(self, linear: float):
533
537
  """Move forward. values 0.0 1.0."""
534
538
  linear_percent = get_percent(abs(linear * 100))
@@ -553,7 +557,6 @@ class MammotionBaseDevice:
553
557
  (linear_speed, angular_speed) = transform_both_speeds(0.0, 180.0, 0.0, angular_percent)
554
558
  await self.queue_command("send_movement", linear_speed=linear_speed, angular_speed=angular_speed)
555
559
 
556
-
557
560
  async def command(self, key: str, **kwargs):
558
561
  """Send a command to the device."""
559
562
  return await self.queue_command(key, **kwargs)
@@ -815,7 +818,7 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
815
818
  notify_msg = await self._notify_future
816
819
  except asyncio.TimeoutError:
817
820
  timeout_expired = True
818
- notify_msg = b''
821
+ notify_msg = b""
819
822
  finally:
820
823
  if not timeout_expired:
821
824
  timeout_handle.cancel()
@@ -978,12 +981,7 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
978
981
  class MammotionBaseCloudDevice(MammotionBaseDevice):
979
982
  """Base class for Mammotion Cloud devices."""
980
983
 
981
- def __init__(
982
- self,
983
- mqtt_client: MammotionMQTT,
984
- cloud_device: Device,
985
- mowing_state: MowingDevice
986
- ) -> None:
984
+ def __init__(self, mqtt_client: MammotionMQTT, cloud_device: Device, mowing_state: MowingDevice) -> None:
987
985
  """Initialize MammotionBaseCloudDevice."""
988
986
  super().__init__(mowing_state, cloud_device)
989
987
  self._ble_sync_task = None
@@ -1015,7 +1013,6 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
1015
1013
  """Callback for when MQTT is subscribed to events."""
1016
1014
  loop = asyncio.get_event_loop()
1017
1015
 
1018
-
1019
1016
  await self._ble_sync()
1020
1017
  await self.run_periodic_sync_task()
1021
1018
  loop.create_task(self._process_queue())
@@ -1025,15 +1022,15 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
1025
1022
  async def on_connected(self):
1026
1023
  """Callback for when MQTT connects."""
1027
1024
 
1028
-
1029
1025
  async def on_disconnected(self):
1030
1026
  """Callback for when MQTT disconnects."""
1031
1027
 
1032
1028
  async def _ble_sync(self):
1033
1029
  command_bytes = self._commands.send_todev_ble_sync(3)
1034
1030
  loop = asyncio.get_running_loop()
1035
- await loop.run_in_executor(None, self._mqtt_client.get_cloud_client().send_cloud_command, self.iot_id, command_bytes)
1036
-
1031
+ await loop.run_in_executor(
1032
+ None, self._mqtt_client.get_cloud_client().send_cloud_command, self.iot_id, command_bytes
1033
+ )
1037
1034
 
1038
1035
  async def run_periodic_sync_task(self) -> None:
1039
1036
  """Send ble sync to robot."""
@@ -1088,10 +1085,7 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
1088
1085
  async def _send_command(self, key: str, retry: int | None = None) -> bytes | None:
1089
1086
  """Send command to device via MQTT and read response."""
1090
1087
  if self._operation_lock.locked():
1091
- _LOGGER.debug(
1092
- "%s: Operation already in progress, waiting for it to complete;",
1093
- self.device.nickName
1094
- )
1088
+ _LOGGER.debug("%s: Operation already in progress, waiting for it to complete;", self.device.nickName)
1095
1089
  with self._operation_lock:
1096
1090
  try:
1097
1091
  command_bytes = getattr(self._commands, key)()
@@ -1140,14 +1134,16 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
1140
1134
  self._key = key
1141
1135
  _LOGGER.debug("%s: Sending command: %s", self.device.nickName, key)
1142
1136
 
1143
- await self.loop.run_in_executor(None, self._mqtt_client.get_cloud_client().send_cloud_command, self.iot_id, command)
1137
+ await self.loop.run_in_executor(
1138
+ None, self._mqtt_client.get_cloud_client().send_cloud_command, self.iot_id, command
1139
+ )
1144
1140
  future = MammotionFuture()
1145
1141
  self._waiting_queue.append(future)
1146
1142
  timeout = 5
1147
1143
  try:
1148
1144
  notify_msg = await future.async_get(timeout)
1149
1145
  except asyncio.TimeoutError:
1150
- notify_msg = b''
1146
+ notify_msg = b""
1151
1147
 
1152
1148
  _LOGGER.debug("%s: Message received", self.device.nickName)
1153
1149
 
@@ -1156,10 +1152,7 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
1156
1152
  async def _send_command_with_args(self, key: str, **kwargs: any) -> bytes | None:
1157
1153
  """Send command with arguments to device via MQTT and read response."""
1158
1154
  if self._operation_lock.locked():
1159
- _LOGGER.debug(
1160
- "%s: Operation already in progress, waiting for it to complete;",
1161
- self.device.nickName
1162
- )
1155
+ _LOGGER.debug("%s: Operation already in progress, waiting for it to complete;", self.device.nickName)
1163
1156
  with self._operation_lock:
1164
1157
  try:
1165
1158
  command_bytes = getattr(self._commands, key)(**kwargs)
@@ -1187,7 +1180,7 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
1187
1180
  _LOGGER.debug("Thing event received")
1188
1181
  event = ThingEventMessage.from_dicts(payload)
1189
1182
  params = event.params
1190
- if params.identifier is None:
1183
+ if params.get("identifier", None) is None:
1191
1184
  return
1192
1185
  if params.identifier == "device_protobuf_msg_event" and event.method == "thing.events":
1193
1186
  _LOGGER.debug("Protobuf event")
@@ -1195,14 +1188,16 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
1195
1188
  self._update_raw_data(cast(bytes, binary_data))
1196
1189
  new_msg = LubaMsg().parse(cast(bytes, binary_data))
1197
1190
 
1198
- if self._commands.get_device_product_key() == "" and self._commands.get_device_name() == event.params.deviceName:
1191
+ if (
1192
+ self._commands.get_device_product_key() == ""
1193
+ and self._commands.get_device_name() == event.params.deviceName
1194
+ ):
1199
1195
  self._commands.set_device_product_key(event.params.productKey)
1200
1196
 
1201
1197
  if betterproto.serialized_on_wire(new_msg.net):
1202
1198
  if new_msg.net.todev_ble_sync != 0 or has_field(new_msg.net.toapp_wifi_iot_status):
1203
1199
  return
1204
1200
 
1205
-
1206
1201
  if len(self._waiting_queue) > 0:
1207
1202
  fut: MammotionFuture = self._waiting_queue.popleft()
1208
1203
  while fut.fut.cancelled() and len(self._waiting_queue) > 0:
@@ -1220,6 +1215,3 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
1220
1215
  def _disconnect(self):
1221
1216
  """Disconnect the MQTT client."""
1222
1217
  self._mqtt_client.disconnect()
1223
-
1224
-
1225
-
@@ -6,6 +6,7 @@ import async_timeout
6
6
 
7
7
  class MammotionFuture:
8
8
  """Create futures for each MQTT Message."""
9
+
9
10
  def __init__(self):
10
11
  self.fut: Future = Future()
11
12
  self.loop = self.fut.get_loop()
@@ -1,4 +1,5 @@
1
1
  """MammotionMQTT."""
2
+
2
3
  import asyncio
3
4
  import hashlib
4
5
  import hmac
@@ -36,11 +37,11 @@ class MammotionMQTT:
36
37
  self._cloud_client = None
37
38
  self.is_connected = False
38
39
  self.is_ready = False
39
- self.on_connected: Optional[Callable[[],Awaitable[None]]] = None
40
- self.on_ready: Optional[Callable[[],Awaitable[None]]] = None
41
- self.on_error: Optional[Callable[[str],Awaitable[None]]] = None
42
- self.on_disconnected: Optional[Callable[[],Awaitable[None]]] = None
43
- self.on_message: Optional[Callable[[str, str, str],Awaitable[None]]] = None
40
+ self.on_connected: Optional[Callable[[], Awaitable[None]]] = None
41
+ self.on_ready: Optional[Callable[[], Awaitable[None]]] = None
42
+ self.on_error: Optional[Callable[[str], Awaitable[None]]] = None
43
+ self.on_disconnected: Optional[Callable[[], Awaitable[None]]] = None
44
+ self.on_message: Optional[Callable[[str, str, str], Awaitable[None]]] = None
44
45
 
45
46
  self._product_key = product_key
46
47
  self._device_name = device_name
@@ -84,7 +85,6 @@ class MammotionMQTT:
84
85
  self._linkkit_client.thing_setup()
85
86
  self._linkkit_client.connect_async()
86
87
 
87
-
88
88
  def disconnect(self):
89
89
  """Disconnect from MQTT Server."""
90
90
  logger.info("Disconnecting...")
@@ -143,7 +143,6 @@ class MammotionMQTT:
143
143
  future = asyncio.run_coroutine_threadsafe(self.on_message(topic, payload, iot_id), self.loop)
144
144
  asyncio.wrap_future(future, loop=self.loop)
145
145
 
146
-
147
146
  def _thing_on_connect(self, session_flag, rc, user_data):
148
147
  """Is called on thing connect."""
149
148
  self.is_connected = True
@@ -164,7 +163,6 @@ class MammotionMQTT:
164
163
  future = asyncio.run_coroutine_threadsafe(self.on_disconnected(), self.loop)
165
164
  asyncio.wrap_future(future, loop=self.loop)
166
165
 
167
-
168
166
  def _on_message(self, _client, _userdata, message: MQTTMessage):
169
167
  """Is called when message is received."""
170
168
  logger.info("Message on topic %s", message.topic)
@@ -194,4 +192,3 @@ class MammotionMQTT:
194
192
  def get_cloud_client(self) -> Optional[CloudIOTGateway]:
195
193
  """Return internal cloud client."""
196
194
  return self._cloud_client
197
-
@@ -2,4 +2,4 @@ import math
2
2
 
3
3
 
4
4
  def parse_double(val: float, d: float):
5
- return val / math.pow(10.0, d)
5
+ return val / math.pow(10.0, d)
@@ -54,11 +54,15 @@ class DeviceType(Enum):
54
54
  return DeviceType.LUBA_2
55
55
  elif value == 3:
56
56
  return DeviceType.LUBA_YUKA
57
+ elif value == 4:
58
+ return DeviceType.YUKA_MINI
59
+ elif value == 5:
60
+ return DeviceType.YUKA_MINI2
57
61
  else:
58
62
  return DeviceType.UNKNOWN
59
63
 
60
64
  @staticmethod
61
- def value_of_str(device_name, product_key=""):
65
+ def value_of_str(device_name: str, product_key=""):
62
66
  """Determine the type of device based on the provided device name and
63
67
  product key.
64
68
 
@@ -78,15 +82,20 @@ class DeviceType(Enum):
78
82
  substring = device_name[:3]
79
83
  substring2 = device_name[:7]
80
84
 
81
- if DeviceType.RTK.name in substring or DeviceType.contain_rtk_product_key(product_key):
85
+ if DeviceType.RTK.get_name() in substring or DeviceType.contain_rtk_product_key(product_key):
82
86
  return DeviceType.RTK
83
- elif DeviceType.LUBA_2.name in substring2 or DeviceType.contain_luba_2_product_key(product_key):
87
+ elif DeviceType.LUBA_2.get_name() in substring2 or DeviceType.contain_luba_2_product_key(product_key):
84
88
  return DeviceType.LUBA_2
85
- elif DeviceType.LUBA_YUKA.name in substring2:
89
+ elif DeviceType.LUBA_YUKA.get_name() in substring2:
86
90
  return DeviceType.LUBA_YUKA
87
- elif DeviceType.LUBA.name in substring2 or DeviceType.contain_luba_product_key(product_key):
91
+ elif DeviceType.YUKA_MINI.get_name() in substring2:
92
+ return DeviceType.YUKA_MINI
93
+ elif DeviceType.YUKA_MINI2.get_name() in substring2:
94
+ return DeviceType.YUKA_MINI2
95
+ elif DeviceType.LUBA.get_name() in substring2 or DeviceType.contain_luba_product_key(product_key):
88
96
  return DeviceType.LUBA
89
97
  else:
98
+ print("unknown device type")
90
99
  return DeviceType.UNKNOWN
91
100
  except Exception:
92
101
  return DeviceType.UNKNOWN
@@ -173,7 +182,11 @@ class DeviceType(Enum):
173
182
 
174
183
  """
175
184
 
176
- return DeviceType.value_of_str(device_name).get_value() == DeviceType.LUBA_YUKA.get_value()
185
+ return (
186
+ DeviceType.value_of_str(device_name).get_value() == DeviceType.LUBA_YUKA.get_value()
187
+ or DeviceType.value_of_str(device_name).get_value() == DeviceType.YUKA_MINI.get_value()
188
+ or DeviceType.value_of_str(device_name).get_value() == DeviceType.YUKA_MINI2.get_value()
189
+ )
177
190
 
178
191
  @staticmethod
179
192
  def is_rtk(device_name, product_key=""):
@@ -260,5 +273,21 @@ class DeviceType(Enum):
260
273
  return False
261
274
  return product_key in ["a1iMygIwxFC", "a1LLmy1zc0j", "a1LLmy1zc0j"]
262
275
 
276
+ @staticmethod
277
+ def contain_yuka_product_key(product_key):
278
+ """Check if the given product key is present in a predefined list.
279
+
280
+ Args:
281
+ product_key (str): The product key to be checked.
282
+
283
+ Returns:
284
+ bool: True if the product key is in the predefined list, False otherwise.
285
+
286
+ """
287
+
288
+ if not product_key:
289
+ return False
290
+ return product_key in ["a1IQV0BrnXb"]
291
+
263
292
  def is_support_video(self):
264
- return self == DeviceType.LUBA_YUKA
293
+ return self != DeviceType.LUBA
@@ -10,8 +10,9 @@ def transform_both_speeds(linear: float, angular: float, linear_percent: float,
10
10
  angular_speed = int(transform4[1] * 4.5)
11
11
  return linear_speed, angular_speed
12
12
 
13
+
13
14
  def get_percent(percent: float):
14
15
  if percent <= 15.0:
15
16
  return 0.0
16
17
 
17
- return percent - 15.0
18
+ return percent - 15.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pymammotion
3
- Version: 0.2.27
3
+ Version: 0.2.29
4
4
  Summary:
5
5
  License: GNU-3.0
6
6
  Author: Michael Arthur
@@ -1,16 +1,16 @@
1
- pymammotion/__init__.py,sha256=kmnjdt3AEMejIz5JK7h1tTJj5ZriAgKwZBa3ScA4-Ao,1516
1
+ pymammotion/__init__.py,sha256=IDqM_7hZPTOYRMqmNAHl0Z5qkdoByNR9gjbguJ4Hpgs,1526
2
2
  pymammotion/aliyun/__init__.py,sha256=T1lkX7TRYiL4nqYanG4l4MImV-SlavSbuooC-W-uUGw,29
3
- pymammotion/aliyun/cloud_gateway.py,sha256=L3efICH21kHNz2eXpReGoTBoTQU-PERX5RTyS7J6eDA,22576
3
+ pymammotion/aliyun/cloud_gateway.py,sha256=NkxSeGXJQ16tWKAncsDfdwG1OJEOCdmSJ212VX2H0CE,22726
4
4
  pymammotion/aliyun/cloud_service.py,sha256=YWcKuKK6iRWy5mTnBYgHxcCusiRGGzQt3spSf7dGDss,2183
5
5
  pymammotion/aliyun/dataclass/aep_response.py,sha256=8f6GIP58ve8gd6AL3HBoXxsy0n2q4ygWvjELGnoOnVc,452
6
6
  pymammotion/aliyun/dataclass/connect_response.py,sha256=Yz-fEbDzgGPTo5Of2oAjmFkSv08T7ze80pQU4k-gKIU,824
7
- pymammotion/aliyun/dataclass/dev_by_account_response.py,sha256=YV7I1Gd3RyHXi369AzEKa604VOV5bmtjD1mVElg9wOw,1033
7
+ pymammotion/aliyun/dataclass/dev_by_account_response.py,sha256=o9X8hNc1y3XXhFa9Nbw7TLUG3F9rIpTj79tEg2uEmUI,1032
8
8
  pymammotion/aliyun/dataclass/login_by_oauth_response.py,sha256=IXSLZ6XnOliOnyXo5Bh0ErqFjA11puACh_9NH0sSJGQ,1262
9
9
  pymammotion/aliyun/dataclass/regions_response.py,sha256=CVPpdFhDD6_emWHyLRzOdp2j3HLPtP8tlNyzGnr8AcI,690
10
- pymammotion/aliyun/dataclass/session_by_authcode_response.py,sha256=_5vsyIxocoecEqygPTmhOQnzjwaK845ssIqbTeaLcmk,406
10
+ pymammotion/aliyun/dataclass/session_by_authcode_response.py,sha256=1K8Uu_V6flSBU8kLCh1Qj59tD9aZQSn7X3hLKeouE_g,407
11
11
  pymammotion/aliyun/tmp_constant.py,sha256=M4Hq_lrGB3LZdX6R2XohRPFoK1NDnNV-pTJwJcJ9838,6650
12
12
  pymammotion/bluetooth/__init__.py,sha256=LAl8jqZ1fPh-3mLmViNQsP3s814C1vsocYUa6oSaXt0,36
13
- pymammotion/bluetooth/ble.py,sha256=9fA8yw5xXNJRSbC68SdOMf2QJnHoD7RhhCjLFoF0c0I,2404
13
+ pymammotion/bluetooth/ble.py,sha256=AIRxXw-ZTY3gTFLclYAasdJ8Djs1s4dRl6KY9hUDUcw,2409
14
14
  pymammotion/bluetooth/ble_message.py,sha256=byibcxZ5b_hE3UzHAImyTcxdBOcya9rdswdtuLV8_hY,15663
15
15
  pymammotion/bluetooth/const.py,sha256=CCqyHsYbB0BAYjwdhXt_n6eWWxmhlUrAFjvVv57mbvE,1749
16
16
  pymammotion/bluetooth/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -20,29 +20,29 @@ pymammotion/bluetooth/data/notifydata.py,sha256=N1bphpueWUWbsWUcpZmMGt2CyCgLcKAF
20
20
  pymammotion/const.py,sha256=EEmZ1v4MqN2nPiNpS_mJQqaCkSNEa1EXUmtwZBUhXIg,329
21
21
  pymammotion/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  pymammotion/data/model/__init__.py,sha256=d8FlIgCcWqoH3jJSpnm-IY-25RM-l2nbRwLtWjSHo74,222
23
- pymammotion/data/model/account.py,sha256=stWLDtWPw1vOPp3xNdX-jWziMmdCvww4IYl00UUGKuI,139
24
- pymammotion/data/model/device.py,sha256=wDmmuCkWvuyoFhZxULCGeigMErmN5GX08NqYJrZbVhQ,10903
25
- pymammotion/data/model/device_config.py,sha256=E3rhLvUH4BuWEpBfylBYBEwn4G8u7c0QbKxWRElw3Sg,177
23
+ pymammotion/data/model/account.py,sha256=vJM-KTf2q6eBfVC-UlNHBSmJvqHiCawZ40vnuhXhaz8,140
24
+ pymammotion/data/model/device.py,sha256=65wBLvnbsx4ngARDW6rI3wyL_G19xhgngtogvvGXolc,10968
25
+ pymammotion/data/model/device_config.py,sha256=KnrBytR7ADxcXXwPKMaH17-BVhJy6ph9PD_Hd_7jf-0,2567
26
26
  pymammotion/data/model/enums.py,sha256=tD_vYsxstOV_lUkYF9uWkrjVOgAJPNnGevy_xmiu3WE,1558
27
27
  pymammotion/data/model/excute_boarder_params.py,sha256=kadSth4y-VXlXIZ6R-Ng-kDvBbM-3YRr8bmR86qR0U0,1355
28
28
  pymammotion/data/model/execute_boarder.py,sha256=oDb2h5tFtOQIa8OCNYaDugqCgCZBLjQRzQTNVcJVAGQ,1072
29
- pymammotion/data/model/generate_route_information.py,sha256=5w1MM1-gXGXb_AoEap_I5xTxAFbNSzXuqQFEkw4NmDs,4301
30
- pymammotion/data/model/hash_list.py,sha256=wHdM46r42_EF2OnXqBHNAyf-muDOibEB7NWVjc0gXYA,2683
31
- pymammotion/data/model/location.py,sha256=mafcOPFcb6niTiiZdDWIh_TbnS76jA3Cqd5paKbnhgc,752
32
- pymammotion/data/model/mowing_modes.py,sha256=2GAF-xaHpv6CSGobYoNtfSi2if8_jW0nonCqN50SNx0,665
29
+ pymammotion/data/model/generate_route_information.py,sha256=MkUBoqGtCAKmiVQ4Q1pEoDVHZs5uLIo7vhfWT4nGbtY,801
30
+ pymammotion/data/model/hash_list.py,sha256=LnCU6Bdr4uy4OF8VGCs5t-JM1Ah6fyaBC3Z_Qwlaa4Y,2682
31
+ pymammotion/data/model/location.py,sha256=sLWObWvfRoR_hmpX5r32iaPOEyk1gJ1vbTRKNVSrdLU,753
32
+ pymammotion/data/model/mowing_modes.py,sha256=UxQrRAHQNC_lJTOA0ms2wuLAAi5UEVTkvvSZjnFpUjE,826
33
33
  pymammotion/data/model/plan.py,sha256=7JvqAo0a9Yg1Vtifd4J3Dx3StEppxrMOfmq2-877kYg,2891
34
34
  pymammotion/data/model/rapid_state.py,sha256=BdJFD_DlhrVneg-PqEruqCoMty-CR7q_9Qna-hk1yd8,1171
35
35
  pymammotion/data/model/region_data.py,sha256=75xOTM1qeRbSROp53eIczw3yCmYM9DgMjMh8qE9xkKo,2880
36
36
  pymammotion/data/model/report_info.py,sha256=1Rj_yRZ9apWX296QHae5prdObDbs32bsQf9rzMz2KEQ,4458
37
37
  pymammotion/data/mqtt/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
38
- pymammotion/data/mqtt/event.py,sha256=ec5TfIBaPZefTKTM3SBweM758hYR3ocjb7rNUMpttn0,4522
38
+ pymammotion/data/mqtt/event.py,sha256=ih6_Evd09GsdfeHZ_5a_L93dhFhFd0s_4zFC1EBck98,4528
39
39
  pymammotion/data/mqtt/properties.py,sha256=HkBPghr26L9_b4QaOi1DtPgb0UoPIOGSe9wb3kgnM6Y,2815
40
40
  pymammotion/data/mqtt/status.py,sha256=zqnlo-MzejEQZszl0i0Wucoc3E76x6UtI9JLxoBnu54,1067
41
- pymammotion/data/state_manager.py,sha256=T6slACOpbnhWVKmJD_XrBY9vrcCFc6q2l6jEf-g89NQ,3095
41
+ pymammotion/data/state_manager.py,sha256=5zP7DdnYXWUhbFYBWiv25L64q8OOzQNzUXzbWEWzYVc,3099
42
42
  pymammotion/event/__init__.py,sha256=mgATR6vPHACNQ-0zH5fi7NdzeTCDV1CZyaWPmtUusi8,115
43
43
  pymammotion/event/event.py,sha256=Fy5-I1p92AO_D67VW4eHQqA4pOt7MZsrP--tVfIVUz8,1820
44
44
  pymammotion/http/_init_.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
- pymammotion/http/http.py,sha256=XJ8_i20HwZ94BqKxCF29dRtRyP_gNOwqJ8gue4F7SOw,2491
45
+ pymammotion/http/http.py,sha256=wE05Yo8K592d1anNaZF-GJHr9D3kYWsd7Axpc1Yei_k,2834
46
46
  pymammotion/mammotion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
47
  pymammotion/mammotion/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
48
  pymammotion/mammotion/commands/abstract_message.py,sha256=nw6r7694yzl7iJKqRqhLmAPRjd_TL_Xo_-JXq2_a_ug,222
@@ -56,12 +56,12 @@ pymammotion/mammotion/commands/messages/ota.py,sha256=XkeuWBZtpYMMBze6r8UN7dJXbe
56
56
  pymammotion/mammotion/commands/messages/system.py,sha256=m4Cc1zUcLgMFCu6zp5rMupwjDnMD-1PM6hqXpXuXRtw,10984
57
57
  pymammotion/mammotion/commands/messages/video.py,sha256=_8lJsU4sLm2CGnc7RDkueA0A51Ysui6x7SqFnhX8O2g,1007
58
58
  pymammotion/mammotion/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
- pymammotion/mammotion/control/joystick.py,sha256=mresPubTlWCuLQBOFD9KTyYJz5BjZgqt52BaRodemhM,5557
59
+ pymammotion/mammotion/control/joystick.py,sha256=xHOp_YmEA-MLaZh3n9VhVOh7oHCti7aKARyfyvUJSjc,5558
60
60
  pymammotion/mammotion/devices/__init__.py,sha256=T72jt0ejtMjo1rPmn_FeMF3pmp0LLeRRpc9WcDKEYYY,126
61
- pymammotion/mammotion/devices/mammotion.py,sha256=Cu6N_zq93TsOnvpd-nH7StopmkKTrzBOQsDKv_RYKEE,49478
61
+ pymammotion/mammotion/devices/mammotion.py,sha256=nAPbc1cG5VoKAIf8g_sJMUx0gduoVKUOz8FxncwT-Kk,49380
62
62
  pymammotion/mqtt/__init__.py,sha256=Ocs5e-HLJvTuDpVXyECEsWIvwsUaxzj7lZ9mSYutNDY,105
63
- pymammotion/mqtt/mammotion_future.py,sha256=WKnHqeHiS2Ut-SaDBNOxqh1jDLeTiyLTsJ7PNUexrjk,687
64
- pymammotion/mqtt/mammotion_mqtt.py,sha256=X012f5RvmZI52FlgchLSWbngvPnkv9M_gTYvJHakYaM,8127
63
+ pymammotion/mqtt/mammotion_future.py,sha256=sQsEpy957YsfejsiVul1vHE62yDk66KID0kRNjMPxH0,688
64
+ pymammotion/mqtt/mammotion_mqtt.py,sha256=VrvO7jfBeKCHwryIa5Elm6D_xrbLX1Hk-0M65h5-C2c,8129
65
65
  pymammotion/proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
66
66
  pymammotion/proto/basestation.proto,sha256=_x5gAz3FkZXS1jtq4GgZgaDCuRU-UV-7HTFdsfQ3zbo,1034
67
67
  pymammotion/proto/basestation.py,sha256=js64_N2xQYRxWPRdVNEapO0qe7vBlfYnjW5sE8hi7hw,2026
@@ -106,14 +106,14 @@ pymammotion/proto/mctrl_sys_pb2.pyi,sha256=Dj_1UM86kZ5MfcVyNC76Z0gKrfl5YFsVWP2b-
106
106
  pymammotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
107
107
  pymammotion/utility/constant/__init__.py,sha256=tZz7szqIhrzNjfcLU3ysfINfg5VEBUAisd9AhP4mvT0,38
108
108
  pymammotion/utility/constant/device_constant.py,sha256=rAEK60F52VyJL31uLnq0Y60D-0VK5gVm59yi9kBfndM,7123
109
- pymammotion/utility/conversions.py,sha256=ioVTEGTmq2-H9GSQbHlm2D9mNAhOTbabuiTaKPElyXQ,88
109
+ pymammotion/utility/conversions.py,sha256=v3YICy0zZwwBBzrUZgabI7GRfiDBnkiAX2qdtk3NxOY,89
110
110
  pymammotion/utility/datatype_converter.py,sha256=v6zym2Zu0upxQjR-xDqXwi3516zpntSlg7LP8tQF5K8,4216
111
- pymammotion/utility/device_type.py,sha256=KYawu2glZMVlPmxRbA4kVFujXz3miHp3rJiOWRVj-GI,8285
111
+ pymammotion/utility/device_type.py,sha256=2B0RPqBo1mveGfFInPQmiON3-TcFMQGZpt7oMSFevL4,9363
112
112
  pymammotion/utility/map.py,sha256=aoi-Luzuph02hKynTofMoq3mnPstanx75MDAVv49CuY,2211
113
- pymammotion/utility/movement.py,sha256=JISPBWCOe4MqHbhmkewhV5aWykr1p6f01DzJfvOF-J8,613
113
+ pymammotion/utility/movement.py,sha256=N75oAoAgFydqoaOedYIxGUHmuTCtPzAOtb-d_29tpfI,615
114
114
  pymammotion/utility/periodic.py,sha256=9wJMfwXPlx6Mbp3Fws7LLTI34ZDKphH1bva_Ggyk32g,3281
115
115
  pymammotion/utility/rocker_util.py,sha256=syPL0QN4zMzHiTIkUKS7RXBBptjdbkfNlPddwUD5V3A,7171
116
- pymammotion-0.2.27.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
117
- pymammotion-0.2.27.dist-info/METADATA,sha256=5ks2LDgNm-FfMrx3bda29NSW3pyW1s8h50iSj9p1pQQ,3969
118
- pymammotion-0.2.27.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
119
- pymammotion-0.2.27.dist-info/RECORD,,
116
+ pymammotion-0.2.29.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
117
+ pymammotion-0.2.29.dist-info/METADATA,sha256=kP1T_XwrwQcFmcNL5Vu0WhYWzQgtH92pv5H2JN86AvM,3969
118
+ pymammotion-0.2.29.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
119
+ pymammotion-0.2.29.dist-info/RECORD,,