pymammotion 0.2.62__py3-none-any.whl → 0.5.51__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.

Potentially problematic release.


This version of pymammotion might be problematic. Click here for more details.

Files changed (135) hide show
  1. pymammotion/__init__.py +9 -6
  2. pymammotion/aliyun/client.py +235 -0
  3. pymammotion/aliyun/cloud_gateway.py +320 -69
  4. pymammotion/aliyun/model/aep_response.py +1 -2
  5. pymammotion/aliyun/model/dev_by_account_response.py +170 -23
  6. pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
  7. pymammotion/aliyun/model/regions_response.py +3 -3
  8. pymammotion/aliyun/model/session_by_authcode_response.py +2 -2
  9. pymammotion/aliyun/model/thing_response.py +12 -0
  10. pymammotion/aliyun/regions.py +62 -0
  11. pymammotion/aliyun/tea/core.py +297 -0
  12. pymammotion/bluetooth/ble.py +11 -15
  13. pymammotion/bluetooth/ble_message.py +389 -106
  14. pymammotion/bluetooth/model/atomic_integer.py +54 -0
  15. pymammotion/const.py +3 -0
  16. pymammotion/data/model/__init__.py +1 -2
  17. pymammotion/data/model/device.py +92 -240
  18. pymammotion/data/model/device_config.py +10 -24
  19. pymammotion/data/model/device_info.py +35 -0
  20. pymammotion/data/model/device_limits.py +49 -0
  21. pymammotion/data/model/enums.py +12 -2
  22. pymammotion/data/model/errors.py +12 -0
  23. pymammotion/data/model/events.py +14 -0
  24. pymammotion/data/model/generate_geojson.py +521 -0
  25. pymammotion/data/model/generate_route_information.py +3 -4
  26. pymammotion/data/model/hash_list.py +384 -48
  27. pymammotion/data/model/location.py +4 -4
  28. pymammotion/data/model/mowing_modes.py +24 -1
  29. pymammotion/data/model/raw_data.py +215 -0
  30. pymammotion/data/model/region_data.py +10 -11
  31. pymammotion/data/model/report_info.py +62 -6
  32. pymammotion/data/model/work.py +27 -0
  33. pymammotion/data/mower_state_manager.py +316 -0
  34. pymammotion/data/mqtt/event.py +73 -28
  35. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  36. pymammotion/data/mqtt/properties.py +93 -78
  37. pymammotion/data/mqtt/status.py +18 -17
  38. pymammotion/event/event.py +32 -8
  39. pymammotion/homeassistant/__init__.py +3 -0
  40. pymammotion/homeassistant/mower_api.py +484 -0
  41. pymammotion/homeassistant/rtk_api.py +54 -0
  42. pymammotion/http/__init__.py +0 -0
  43. pymammotion/http/encryption.py +220 -0
  44. pymammotion/http/http.py +652 -44
  45. pymammotion/http/model/__init__.py +0 -0
  46. pymammotion/{aliyun/model/stream_subscription_response.py → http/model/camera_stream.py} +14 -2
  47. pymammotion/http/model/http.py +160 -9
  48. pymammotion/http/model/response_factory.py +61 -0
  49. pymammotion/http/model/rtk.py +16 -0
  50. pymammotion/mammotion/commands/abstract_message.py +7 -5
  51. pymammotion/mammotion/commands/mammotion_command.py +32 -3
  52. pymammotion/mammotion/commands/messages/basestation.py +43 -0
  53. pymammotion/mammotion/commands/messages/driver.py +61 -29
  54. pymammotion/mammotion/commands/messages/media.py +68 -15
  55. pymammotion/mammotion/commands/messages/navigation.py +61 -25
  56. pymammotion/mammotion/commands/messages/network.py +93 -100
  57. pymammotion/mammotion/commands/messages/ota.py +18 -18
  58. pymammotion/mammotion/commands/messages/system.py +97 -72
  59. pymammotion/mammotion/commands/messages/video.py +17 -12
  60. pymammotion/mammotion/devices/__init__.py +27 -3
  61. pymammotion/mammotion/devices/base.py +50 -127
  62. pymammotion/mammotion/devices/mammotion.py +447 -212
  63. pymammotion/mammotion/devices/mammotion_bluetooth.py +105 -60
  64. pymammotion/mammotion/devices/mammotion_cloud.py +157 -105
  65. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  66. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  67. pymammotion/mammotion/devices/managers/managers.py +81 -0
  68. pymammotion/mammotion/devices/mower_device.py +124 -0
  69. pymammotion/mammotion/devices/mower_manager.py +107 -0
  70. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  71. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  72. pymammotion/mammotion/devices/rtk_device.py +50 -0
  73. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  74. pymammotion/mqtt/__init__.py +2 -1
  75. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  76. pymammotion/mqtt/linkkit/__init__.py +5 -0
  77. pymammotion/mqtt/linkkit/h2client.py +585 -0
  78. pymammotion/mqtt/linkkit/linkkit.py +3023 -0
  79. pymammotion/mqtt/mammotion_mqtt.py +176 -169
  80. pymammotion/mqtt/mqtt_models.py +66 -0
  81. pymammotion/proto/__init__.py +4839 -4
  82. pymammotion/proto/basestation.proto +8 -0
  83. pymammotion/proto/basestation_pb2.py +11 -9
  84. pymammotion/proto/basestation_pb2.pyi +16 -2
  85. pymammotion/proto/dev_net.proto +79 -55
  86. pymammotion/proto/dev_net_pb2.py +60 -56
  87. pymammotion/proto/dev_net_pb2.pyi +49 -6
  88. pymammotion/proto/luba_msg.proto +2 -1
  89. pymammotion/proto/luba_msg_pb2.py +6 -6
  90. pymammotion/proto/luba_msg_pb2.pyi +1 -0
  91. pymammotion/proto/luba_mul.proto +62 -1
  92. pymammotion/proto/luba_mul_pb2.py +38 -22
  93. pymammotion/proto/luba_mul_pb2.pyi +94 -7
  94. pymammotion/proto/mctrl_driver.proto +44 -4
  95. pymammotion/proto/mctrl_driver_pb2.py +26 -14
  96. pymammotion/proto/mctrl_driver_pb2.pyi +66 -11
  97. pymammotion/proto/mctrl_nav.proto +97 -51
  98. pymammotion/proto/mctrl_nav_pb2.py +75 -67
  99. pymammotion/proto/mctrl_nav_pb2.pyi +142 -56
  100. pymammotion/proto/mctrl_ota.proto +40 -2
  101. pymammotion/proto/mctrl_ota_pb2.py +23 -13
  102. pymammotion/proto/mctrl_ota_pb2.pyi +67 -4
  103. pymammotion/proto/mctrl_pept.proto +8 -3
  104. pymammotion/proto/mctrl_pept_pb2.py +8 -6
  105. pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
  106. pymammotion/proto/mctrl_sys.proto +325 -86
  107. pymammotion/proto/mctrl_sys_pb2.py +162 -98
  108. pymammotion/proto/mctrl_sys_pb2.pyi +451 -25
  109. pymammotion/proto/message_pool.py +3 -0
  110. pymammotion/proto/py.typed +0 -0
  111. pymammotion/utility/constant/device_constant.py +65 -21
  112. pymammotion/utility/datatype_converter.py +13 -12
  113. pymammotion/utility/device_config.py +755 -0
  114. pymammotion/utility/device_type.py +218 -21
  115. pymammotion/utility/map.py +238 -51
  116. pymammotion/utility/mur_mur_hash.py +159 -0
  117. {pymammotion-0.2.62.dist-info → pymammotion-0.5.51.dist-info}/METADATA +27 -31
  118. pymammotion-0.5.51.dist-info/RECORD +152 -0
  119. {pymammotion-0.2.62.dist-info → pymammotion-0.5.51.dist-info}/WHEEL +1 -1
  120. pymammotion/aliyun/cloud_service.py +0 -65
  121. pymammotion/data/model/plan.py +0 -58
  122. pymammotion/data/state_manager.py +0 -130
  123. pymammotion/proto/basestation.py +0 -59
  124. pymammotion/proto/common.py +0 -12
  125. pymammotion/proto/dev_net.py +0 -381
  126. pymammotion/proto/luba_msg.py +0 -81
  127. pymammotion/proto/luba_mul.py +0 -76
  128. pymammotion/proto/mctrl_driver.py +0 -100
  129. pymammotion/proto/mctrl_nav.py +0 -660
  130. pymammotion/proto/mctrl_ota.py +0 -48
  131. pymammotion/proto/mctrl_pept.py +0 -41
  132. pymammotion/proto/mctrl_sys.py +0 -574
  133. pymammotion-0.2.62.dist-info/RECORD +0 -125
  134. /pymammotion/{http/_init_.py → bluetooth/model/__init__.py} +0 -0
  135. {pymammotion-0.2.62.dist-info → pymammotion-0.5.51.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,54 @@
1
+ from threading import Lock
2
+
3
+
4
+ class AtomicInteger:
5
+ """Thread-safe atomic integer implementation."""
6
+
7
+ def __init__(self, initial_value: int = 0) -> None:
8
+ """Initialize atomic integer with given value."""
9
+ self._value = initial_value
10
+ self._lock = Lock()
11
+
12
+ def get(self) -> int:
13
+ """Get the current value."""
14
+ with self._lock:
15
+ return self._value
16
+
17
+ def set(self, value: int) -> None:
18
+ """Set a new value."""
19
+ with self._lock:
20
+ self._value = value
21
+
22
+ def increment_and_get(self) -> int:
23
+ """Increment the value and return the new value."""
24
+ with self._lock:
25
+ self._value += 1
26
+ return self._value
27
+
28
+ def decrement_and_get(self) -> int:
29
+ """Decrement the value and return the new value."""
30
+ with self._lock:
31
+ self._value -= 1
32
+ return self._value
33
+
34
+ def add_and_get(self, delta: int) -> int:
35
+ """Add delta to value and return the new value."""
36
+ with self._lock:
37
+ self._value += delta
38
+ return self._value
39
+
40
+ def compare_and_set(self, expect: int, update: int) -> bool:
41
+ """Compare value with expected and set to update if they match."""
42
+ with self._lock:
43
+ if self._value == expect:
44
+ self._value = update
45
+ return True
46
+ return False
47
+
48
+ def __str__(self) -> str:
49
+ """Returns string representation of the atomic integer."""
50
+ return str(self.get())
51
+
52
+ def __repr__(self) -> str:
53
+ """Detailed string representation of the atomic integer."""
54
+ return f"AtomicInteger({self.get()})"
pymammotion/const.py CHANGED
@@ -8,3 +8,6 @@ MAMMOTION_DOMAIN = "https://id.mammotion.com"
8
8
  MAMMOTION_API_DOMAIN = "https://domestic.mammotion.com"
9
9
  MAMMOTION_CLIENT_ID = "MADKALUBAS"
10
10
  MAMMOTION_CLIENT_SECRET = "GshzGRZJjuMUgd2sYHM7"
11
+
12
+ MAMMOTION_OUATH2_CLIENT_ID = "GxebgSt8si6pKqR"
13
+ MAMMOTION_OUATH2_CLIENT_SECRET = "JP0508SRJFa0A90ADpzLINDBxMa4Vj"
@@ -2,8 +2,7 @@
2
2
 
3
3
  from .generate_route_information import GenerateRouteInformation
4
4
  from .hash_list import HashList
5
- from .plan import Plan
6
5
  from .rapid_state import RapidState, RTKStatus
7
6
  from .region_data import RegionData
8
7
 
9
- __all__ = ["GenerateRouteInformation", "HashList", "Plan", "RapidState", "RTKStatus", "RegionData"]
8
+ __all__ = ["GenerateRouteInformation", "HashList", "RapidState", "RTKStatus", "RegionData"]
@@ -1,31 +1,22 @@
1
1
  """MowingDevice class to wrap around the betterproto dataclasses."""
2
2
 
3
3
  from dataclasses import dataclass, field
4
- from typing import Optional
5
4
 
6
- import betterproto
5
+ import betterproto2
7
6
  from mashumaro.mixins.orjson import DataClassORJSONMixin
8
7
 
9
8
  from pymammotion.data.model import HashList, RapidState
10
- from pymammotion.data.model.device_config import DeviceLimits
11
- from pymammotion.data.model.device_info import MowerInfo
9
+ from pymammotion.data.model.device_info import DeviceFirmwares, DeviceNonWorkingHours, MowerInfo
10
+ from pymammotion.data.model.errors import DeviceErrors
11
+ from pymammotion.data.model.events import Events
12
12
  from pymammotion.data.model.location import Location
13
13
  from pymammotion.data.model.report_info import ReportData
14
+ from pymammotion.data.model.work import CurrentTaskSettings
15
+ from pymammotion.data.mqtt.event import ThingEventMessage
14
16
  from pymammotion.data.mqtt.properties import ThingPropertiesMessage
15
- from pymammotion.proto.dev_net import DevNet
16
- from pymammotion.proto.luba_msg import LubaMsg
17
- from pymammotion.proto.luba_mul import SocMul
18
- from pymammotion.proto.mctrl_driver import MctlDriver
19
- from pymammotion.proto.mctrl_nav import MctlNav
20
- from pymammotion.proto.mctrl_ota import MctlOta
21
- from pymammotion.proto.mctrl_pept import MctlPept
22
- from pymammotion.proto.mctrl_sys import (
23
- MctlSys,
24
- MowToAppInfoT,
25
- ReportInfoData,
26
- SystemRapidStateTunnelMsg,
27
- SystemUpdateBufMsg,
28
- )
17
+ from pymammotion.data.mqtt.status import ThingStatusMessage
18
+ from pymammotion.http.model.http import CheckDeviceVersion
19
+ from pymammotion.proto import DeviceFwInfo, MowToAppInfoT, ReportInfoData, SystemRapidStateTunnelMsg, SystemUpdateBufMsg
29
20
  from pymammotion.utility.constant import WorkMode
30
21
  from pymammotion.utility.conversions import parse_double
31
22
  from pymammotion.utility.map import CoordinateConverter
@@ -35,42 +26,42 @@ from pymammotion.utility.map import CoordinateConverter
35
26
  class MowingDevice(DataClassORJSONMixin):
36
27
  """Wraps the betterproto dataclasses, so we can bypass the groups for keeping all data."""
37
28
 
29
+ name: str = ""
30
+ online: bool = True
31
+ enabled: bool = True
32
+ update_check: CheckDeviceVersion = field(default_factory=CheckDeviceVersion)
38
33
  mower_state: MowerInfo = field(default_factory=MowerInfo)
39
- mqtt_properties: ThingPropertiesMessage = field(default_factory=ThingPropertiesMessage)
34
+ mqtt_properties: ThingPropertiesMessage | None = None
35
+ status_properties: ThingStatusMessage | None = None
36
+ device_event: ThingEventMessage | None = None
40
37
  map: HashList = field(default_factory=HashList)
38
+ work: CurrentTaskSettings = field(default_factory=CurrentTaskSettings)
41
39
  location: Location = field(default_factory=Location)
42
40
  mowing_state: RapidState = field(default_factory=RapidState)
43
41
  report_data: ReportData = field(default_factory=ReportData)
44
- err_code_list: list = field(default_factory=list)
45
- err_code_list_time: Optional[list] = field(default_factory=list)
46
- limits: DeviceLimits = field(default_factory=DeviceLimits)
47
- device: Optional[LubaMsg] = field(default_factory=LubaMsg)
48
-
49
- @classmethod
50
- def from_raw(cls, raw: dict) -> "MowingDevice":
51
- """Take in raw data to hold in the betterproto dataclass."""
52
- mowing_device = MowingDevice()
53
- mowing_device.device = LubaMsg(**raw)
54
- return mowing_device
55
-
56
- def update_raw(self, raw: dict) -> None:
57
- """Update the raw LubaMsg data."""
58
- self.device = LubaMsg(**raw)
42
+ device_firmwares: DeviceFirmwares = field(default_factory=DeviceFirmwares)
43
+ errors: DeviceErrors = field(default_factory=DeviceErrors)
44
+ non_work_hours: DeviceNonWorkingHours = field(default_factory=DeviceNonWorkingHours)
45
+ events: Events = field(default_factory=Events)
59
46
 
60
47
  def buffer(self, buffer_list: SystemUpdateBufMsg) -> None:
61
48
  """Update the device based on which buffer we are reading from."""
62
49
  match buffer_list.update_buf_data[0]:
63
50
  case 1:
64
- # 4 speed
65
- self.location.RTK.latitude = parse_double(buffer_list.update_buf_data[5], 8.0)
66
- self.location.RTK.longitude = parse_double(buffer_list.update_buf_data[6], 8.0)
67
- self.location.dock.latitude = parse_double(buffer_list.update_buf_data[7], 4.0)
68
- self.location.dock.longitude = parse_double(buffer_list.update_buf_data[8], 4.0)
69
- self.location.dock.rotation = buffer_list.update_buf_data[3] + 180
51
+ # 4 speed?
52
+ if buffer_list.update_buf_data[5] != 0:
53
+ self.location.RTK.latitude = parse_double(buffer_list.update_buf_data[5], 8.0)
54
+ self.location.RTK.longitude = parse_double(buffer_list.update_buf_data[6], 8.0)
55
+ if buffer_list.update_buf_data[7] != 0:
56
+ # latitude Y longitude X
57
+ self.location.dock.longitude = parse_double(buffer_list.update_buf_data[7], 4.0)
58
+ self.location.dock.latitude = parse_double(buffer_list.update_buf_data[8], 4.0)
59
+ self.location.dock.rotation = buffer_list.update_buf_data[3]
60
+
70
61
  case 2:
71
- self.err_code_list.clear()
72
- self.err_code_list_time.clear()
73
- self.err_code_list.extend(
62
+ self.errors.err_code_list.clear()
63
+ self.errors.err_code_list_time.clear()
64
+ self.errors.err_code_list.extend(
74
65
  [
75
66
  buffer_list.update_buf_data[3],
76
67
  buffer_list.update_buf_data[5],
@@ -84,7 +75,7 @@ class MowingDevice(DataClassORJSONMixin):
84
75
  buffer_list.update_buf_data[21],
85
76
  ]
86
77
  )
87
- self.err_code_list_time.extend(
78
+ self.errors.err_code_list_time.extend(
88
79
  [
89
80
  buffer_list.update_buf_data[4],
90
81
  buffer_list.update_buf_data[6],
@@ -98,28 +89,48 @@ class MowingDevice(DataClassORJSONMixin):
98
89
  buffer_list.update_buf_data[22],
99
90
  ]
100
91
  )
92
+ case 3:
93
+ # task state event
94
+ task_area_map: dict[int, int] = {}
95
+ task_area_ids = []
96
+
97
+ for i in range(3, len(buffer_list.update_buf_data), 2):
98
+ area_id = buffer_list.update_buf_data[i]
99
+
100
+ if area_id != 0:
101
+ area_value = int(buffer_list.update_buf_data[i + 1])
102
+ task_area_map[area_id] = area_value
103
+ task_area_ids.append(area_id)
104
+ self.events.work_tasks_event.hash_list = task_area_map
105
+ self.events.work_tasks_event.ids = task_area_ids
101
106
 
102
107
  def update_report_data(self, toapp_report_data: ReportInfoData) -> None:
108
+ """Set report data for the mower."""
103
109
  coordinate_converter = CoordinateConverter(self.location.RTK.latitude, self.location.RTK.longitude)
104
110
  for index, location in enumerate(toapp_report_data.locations):
105
111
  if index == 0 and location.real_pos_y != 0:
106
112
  self.location.position_type = location.pos_type
107
- self.location.orientation = location.real_toward / 10000
113
+ self.location.orientation = int(location.real_toward / 10000)
108
114
  self.location.device = coordinate_converter.enu_to_lla(
109
115
  parse_double(location.real_pos_y, 4.0), parse_double(location.real_pos_x, 4.0)
110
116
  )
117
+ self.map.invalidate_maps(location.bol_hash)
111
118
  if location.zone_hash:
112
119
  self.location.work_zone = (
113
120
  location.zone_hash if self.report_data.dev.sys_status == WorkMode.MODE_WORKING else 0
114
121
  )
115
122
 
116
- self.report_data.update(toapp_report_data.to_dict(casing=betterproto.Casing.SNAKE))
123
+ if toapp_report_data.fw_info:
124
+ self.update_device_firmwares(toapp_report_data.fw_info)
125
+
126
+ self.report_data.update(toapp_report_data.to_dict(casing=betterproto2.Casing.SNAKE))
117
127
 
118
128
  def run_state_update(self, rapid_state: SystemRapidStateTunnelMsg) -> None:
129
+ """Set lat long, work zone of RTK and robot."""
119
130
  coordinate_converter = CoordinateConverter(self.location.RTK.latitude, self.location.RTK.longitude)
120
131
  self.mowing_state = RapidState().from_raw(rapid_state.rapid_state_data)
121
132
  self.location.position_type = self.mowing_state.pos_type
122
- self.location.orientation = self.mowing_state.toward / 10000
133
+ self.location.orientation = int(self.mowing_state.toward / 10000)
123
134
  self.location.device = coordinate_converter.enu_to_lla(
124
135
  parse_double(self.mowing_state.pos_y, 4.0), parse_double(self.mowing_state.pos_x, 4.0)
125
136
  )
@@ -129,203 +140,44 @@ class MowingDevice(DataClassORJSONMixin):
129
140
  )
130
141
 
131
142
  def mow_info(self, toapp_mow_info: MowToAppInfoT) -> None:
132
- pass
143
+ """Set mow info."""
133
144
 
134
145
  def report_missing_data(self) -> None:
135
146
  """Report missing data so we can refetch it."""
136
147
 
137
- @property
138
- def net(self):
139
- """Will return a wrapped betterproto of net."""
140
- return DevNetData(net=self.device.net)
141
-
142
- @property
143
- def sys(self):
144
- """Will return a wrapped betterproto of sys."""
145
- return SysData(sys=self.device.sys)
146
-
147
- @property
148
- def nav(self):
149
- """Will return a wrapped betterproto of nav."""
150
- return NavData(nav=self.device.nav)
151
-
152
- @property
153
- def driver(self):
154
- """Will return a wrapped betterproto of driver."""
155
- return DriverData(driver=self.device.driver)
156
-
157
- @property
158
- def mul(self):
159
- """Will return a wrapped betterproto of mul."""
160
- return MulData(mul=self.device.mul)
161
-
162
- @property
163
- def ota(self):
164
- """Will return a wrapped betterproto of ota."""
165
- return OtaData(ota=self.device.ota)
166
-
167
- @property
168
- def pept(self):
169
- """Will return a wrapped betterproto of pept."""
170
- return PeptData(pept=self.device.pept)
148
+ def update_device_firmwares(self, fw_info: DeviceFwInfo) -> None:
149
+ """Set firmware versions on all parts of the robot or RTK."""
150
+ for mod in fw_info.mod:
151
+ match mod.type:
152
+ case 1:
153
+ self.device_firmwares.main_controller = mod.version
154
+ case 3:
155
+ self.device_firmwares.left_motor_driver = mod.version
156
+ case 4:
157
+ self.device_firmwares.right_motor_driver = mod.version
158
+ case 5:
159
+ self.device_firmwares.rtk_rover_station = mod.version
160
+ case 101:
161
+ # RTK main board
162
+ self.device_firmwares.main_controller = mod.version
163
+ case 102:
164
+ self.device_firmwares.rtk_version = mod.version
165
+ case 103:
166
+ self.device_firmwares.lora_version = mod.version
171
167
 
172
168
 
173
169
  @dataclass
174
- class DevNetData(DataClassORJSONMixin):
175
- """Wrapping class around LubaMsg to return a dataclass from the raw dict."""
176
-
177
- net: dict
178
-
179
- def __init__(self, net: DevNet) -> None:
180
- if isinstance(net, dict):
181
- self.net = net
182
- else:
183
- self.net = net.to_dict()
184
-
185
- def __getattr__(self, item):
186
- """Intercept call to get net in dict and return a betterproto dataclass."""
187
- if self.net.get(item) is None:
188
- return DevNet().__getattribute__(item)
189
-
190
- if not isinstance(self.net.get(item), dict):
191
- return self.net.get(item)
192
-
193
- return DevNet().__getattribute__(item).from_dict(value=self.net.get(item))
194
-
195
-
196
- @dataclass
197
- class SysData(DataClassORJSONMixin):
198
- """Wrapping class around LubaMsg to return a dataclass from the raw dict."""
199
-
200
- sys: dict
201
-
202
- def __init__(self, sys: MctlSys) -> None:
203
- if isinstance(sys, dict):
204
- self.sys = sys
205
- else:
206
- self.sys = sys.to_dict()
207
-
208
- def __getattr__(self, item: str):
209
- """Intercept call to get sys in dict and return a betterproto dataclass."""
210
- if self.sys.get(item) is None:
211
- return MctlSys().__getattribute__(item)
212
-
213
- if not isinstance(self.sys.get(item), dict):
214
- return self.sys.get(item)
215
-
216
- return MctlSys().__getattribute__(item).from_dict(value=self.sys.get(item))
217
-
218
-
219
- @dataclass
220
- class NavData(DataClassORJSONMixin):
221
- """Wrapping class around LubaMsg to return a dataclass from the raw dict."""
222
-
223
- nav: dict
224
-
225
- def __init__(self, nav: MctlNav) -> None:
226
- if isinstance(nav, dict):
227
- self.nav = nav
228
- else:
229
- self.nav = nav.to_dict()
230
-
231
- def __getattr__(self, item: str):
232
- """Intercept call to get nav in dict and return a betterproto dataclass."""
233
- if self.nav.get(item) is None:
234
- return MctlNav().__getattribute__(item)
235
-
236
- if not isinstance(self.nav.get(item), dict):
237
- return self.nav.get(item)
238
-
239
- return MctlNav().__getattribute__(item).from_dict(value=self.nav.get(item))
240
-
241
-
242
- @dataclass
243
- class DriverData(DataClassORJSONMixin):
244
- """Wrapping class around LubaMsg to return a dataclass from the raw dict."""
245
-
246
- driver: dict
247
-
248
- def __init__(self, driver: MctlDriver) -> None:
249
- if isinstance(driver, dict):
250
- self.driver = driver
251
- else:
252
- self.driver = driver.to_dict()
253
-
254
- def __getattr__(self, item: str):
255
- """Intercept call to get driver in dict and return a betterproto dataclass."""
256
- if self.driver.get(item) is None:
257
- return MctlDriver().__getattribute__(item)
258
-
259
- if not isinstance(self.driver.get(item), dict):
260
- return self.driver.get(item)
261
-
262
- return MctlDriver().__getattribute__(item).from_dict(value=self.driver.get(item))
263
-
264
-
265
- @dataclass
266
- class MulData(DataClassORJSONMixin):
267
- """Wrapping class around LubaMsg to return a dataclass from the raw dict."""
268
-
269
- mul: dict
270
-
271
- def __init__(self, mul: SocMul) -> None:
272
- if isinstance(mul, dict):
273
- self.mul = mul
274
- else:
275
- self.mul = mul.to_dict()
276
-
277
- def __getattr__(self, item: str):
278
- """Intercept call to get mul in dict and return a betterproto dataclass."""
279
- if self.mul.get(item) is None:
280
- return SocMul().__getattribute__(item)
281
-
282
- if not isinstance(self.mul.get(item), dict):
283
- return self.mul.get(item)
284
-
285
- return SocMul().__getattribute__(item).from_dict(value=self.mul.get(item))
286
-
287
-
288
- @dataclass
289
- class OtaData(DataClassORJSONMixin):
290
- """Wrapping class around LubaMsg to return a dataclass from the raw dict."""
291
-
292
- ota: dict
293
-
294
- def __init__(self, ota: MctlOta) -> None:
295
- if isinstance(ota, dict):
296
- self.ota = ota
297
- else:
298
- self.ota = ota.to_dict()
299
-
300
- def __getattr__(self, item: str):
301
- """Intercept call to get ota in dict and return a betterproto dataclass."""
302
- if self.ota.get(item) is None:
303
- return MctlOta().__getattribute__(item)
304
-
305
- if not isinstance(self.ota.get(item), dict):
306
- return self.ota.get(item)
307
-
308
- return MctlOta().__getattribute__(item).from_dict(value=self.ota.get(item))
309
-
310
-
311
- @dataclass
312
- class PeptData(DataClassORJSONMixin):
313
- """Wrapping class around LubaMsg to return a dataclass from the raw dict."""
314
-
315
- pept: dict
316
-
317
- def __init__(self, pept: MctlPept) -> None:
318
- if isinstance(pept, dict):
319
- self.pept = pept
320
- else:
321
- self.pept = pept.to_dict()
322
-
323
- def __getattr__(self, item: str):
324
- """Intercept call to get pept in dict and return a betterproto dataclass."""
325
- if self.pept.get(item) is None:
326
- return MctlPept().__getattribute__(item)
327
-
328
- if not isinstance(self.pept.get(item), dict):
329
- return self.pept.get(item)
330
-
331
- return MctlPept().__getattribute__(item).from_dict(value=self.pept.get(item))
170
+ class RTKDevice(DataClassORJSONMixin):
171
+ name: str
172
+ iot_id: str
173
+ product_key: str
174
+ online: bool = True
175
+ lat: float = 0.0
176
+ lon: float = 0.0
177
+ lora: str = ""
178
+ wifi_rssi: int = 0
179
+ device_version: str = ""
180
+ lora_version: str = ""
181
+ wifi_sta_mac: str = ""
182
+ bt_mac: str = ""
183
+ update_check: CheckDeviceVersion = field(default_factory=CheckDeviceVersion)
@@ -5,16 +5,6 @@ from mashumaro.mixins.orjson import DataClassORJSONMixin
5
5
  from pymammotion.utility.device_type import DeviceType
6
6
 
7
7
 
8
- @dataclass
9
- class DeviceLimits(DataClassORJSONMixin):
10
- blade_height_min: int = 30
11
- blade_height_max: int = 100
12
- working_speed_min: float = 0.2
13
- working_speed_max: float = 1.2
14
- working_path_min: int = 15
15
- working_path_max: int = 35
16
-
17
-
18
8
  @dataclass
19
9
  class OperationSettings(DataClassORJSONMixin):
20
10
  """Operation settings for a device."""
@@ -36,33 +26,29 @@ class OperationSettings(DataClassORJSONMixin):
36
26
  toward: int = 0 # is just angle
37
27
  toward_included_angle: int = 90
38
28
  toward_mode: int = 0 # angle type relative etc
39
- border_mode: int = 1 # border laps
29
+ border_mode: int = 0
40
30
  obstacle_laps: int = 1
41
- mowing_laps: int = 1
31
+ mowing_laps: int = 1 # border laps
42
32
  start_progress: int = 0
43
- areas: list[int] = field(default_factory=list)
33
+ areas: set[int] = field(default_factory=set)
44
34
 
45
35
 
46
36
  def create_path_order(operation_mode: OperationSettings, device_name: str) -> str:
47
- i = 8
37
+ # TODO add scheduling logic from getReserved() WorkSettingViewModel.java
48
38
  bArr = bytearray(8)
49
39
  bArr[0] = operation_mode.border_mode
50
40
  bArr[1] = operation_mode.obstacle_laps
51
41
  bArr[3] = int(operation_mode.start_progress)
52
42
  bArr[2] = 0
53
-
43
+ bArr[5] = 0
54
44
  if not DeviceType.is_luba1(device_name):
55
45
  bArr[4] = 0
56
- if DeviceType.is_yuka(device_name):
57
- i = calculate_yuka_mode(operation_mode)
58
- elif not DeviceType.is_luba_2(device_name):
59
- i = 0
60
- bArr[5] = i
61
- if operation_mode.is_dump:
62
- b = int(operation_mode.collect_grass_frequency)
46
+ if DeviceType.is_yuka(device_name) and not DeviceType.is_yuka_mini(device_name):
47
+ bArr[5] = calculate_yuka_mode(operation_mode)
63
48
  else:
64
- b = 10
65
- bArr[6] = b
49
+ bArr[5] = 8 if DeviceType.is_luba_pro(device_name) else 0
50
+
51
+ bArr[6] = int(operation_mode.collect_grass_frequency) if operation_mode.is_dump else 10
66
52
  if DeviceType.is_luba1(device_name):
67
53
  bArr[4] = operation_mode.toward_mode
68
54
  return bArr.decode()
@@ -14,12 +14,47 @@ class SideLight(DataClassORJSONMixin):
14
14
  action: int = 0
15
15
 
16
16
 
17
+ @dataclass
18
+ class DeviceNonWorkingHours(DataClassORJSONMixin):
19
+ sub_cmd: int = 0
20
+ start_time: str = ""
21
+ end_time: str = ""
22
+
23
+
24
+ @dataclass
25
+ class LampInfo(DataClassORJSONMixin):
26
+ lamp_bright: int = 0
27
+ manual_light: bool = False
28
+ night_light: bool = False
29
+
30
+
17
31
  @dataclass
18
32
  class MowerInfo(DataClassORJSONMixin):
19
33
  blade_status: bool = False
34
+ rain_detection: bool = False
35
+ traversal_mode: int = 0
36
+ turning_mode: int = 0
37
+ blade_mode: int = 0
38
+ blade_rpm: int = 0
20
39
  side_led: SideLight = field(default_factory=SideLight)
21
40
  collector_installation_status: bool = False
22
41
  model: str = ""
23
42
  swversion: str = ""
24
43
  product_key: str = ""
25
44
  model_id: str = ""
45
+ sub_model_id: str = ""
46
+ ble_mac: str = ""
47
+ wifi_mac: str = ""
48
+ lamp_info: LampInfo = field(default_factory=LampInfo)
49
+
50
+
51
+ @dataclass
52
+ class DeviceFirmwares(DataClassORJSONMixin):
53
+ device_version: str = ""
54
+ left_motor_driver: str = ""
55
+ lora_version: str = ""
56
+ main_controller: str = ""
57
+ model_name: str = ""
58
+ right_motor_driver: str = ""
59
+ rtk_rover_station: str = ""
60
+ rtk_version: str = ""
@@ -0,0 +1,49 @@
1
+ from dataclasses import dataclass, field
2
+
3
+
4
+ @dataclass
5
+ class RangeLimit:
6
+ min: float
7
+ max: float
8
+
9
+
10
+ @dataclass
11
+ class DeviceLimits:
12
+ blade_height: RangeLimit = field(default_factory=RangeLimit)
13
+ working_speed: RangeLimit = field(default_factory=RangeLimit)
14
+ path_spacing: RangeLimit = field(default_factory=RangeLimit)
15
+ work_area_num_max: int = 60
16
+ display_image_type: int = 0
17
+
18
+ def to_dict(self) -> dict:
19
+ """Convert the device limits to a dictionary format."""
20
+ return {
21
+ "blade_height": {"min": self.blade_height.min, "max": self.blade_height.max},
22
+ "working_speed": {"min": self.working_speed.min, "max": self.working_speed.max},
23
+ "path_spacing": {"min": self.path_spacing.min, "max": self.path_spacing.max},
24
+ "work_area_num_max": self.work_area_num_max,
25
+ "display_image_type": self.display_image_type,
26
+ }
27
+
28
+ @classmethod
29
+ def from_dict(cls, data: dict) -> "DeviceLimits":
30
+ """Create a DeviceLimits instance from a dictionary."""
31
+ return cls(
32
+ blade_height=RangeLimit(min=data["blade_height"]["min"], max=data["blade_height"]["max"]),
33
+ working_speed=RangeLimit(min=data["working_speed"]["min"], max=data["working_speed"]["max"]),
34
+ path_spacing=RangeLimit(min=data["path_spacing"]["min"], max=data["path_spacing"]["max"]),
35
+ work_area_num_max=data["work_area_num_max"],
36
+ display_image_type=data["display_image_type"],
37
+ )
38
+
39
+ def validate(self) -> bool:
40
+ """Validate that all ranges are logical (min <= max)."""
41
+ return all(
42
+ [
43
+ self.blade_height.min <= self.blade_height.max,
44
+ self.working_speed.min <= self.working_speed.max,
45
+ self.path_spacing.min <= self.path_spacing.max,
46
+ self.work_area_num_max > 0,
47
+ self.display_image_type in (0, 1),
48
+ ]
49
+ )
@@ -1,6 +1,16 @@
1
1
  from enum import Enum
2
2
 
3
3
 
4
+ class ConnectionPreference(Enum):
5
+ """Enum for connection preference."""
6
+
7
+ ANY = 0
8
+ WIFI = 1
9
+ BLUETOOTH = 2
10
+ PREFER_WIFI = 3
11
+ PREFER_BLUETOOTH = 4
12
+
13
+
4
14
  class PositionMode(Enum):
5
15
  FIX = 0
6
16
  SINGLE = 1
@@ -9,7 +19,7 @@ class PositionMode(Enum):
9
19
  UNKNOWN = 4
10
20
 
11
21
  @staticmethod
12
- def from_value(value: int):
22
+ def from_value(value: int) -> "PositionMode":
13
23
  if value == 0:
14
24
  return PositionMode.FIX
15
25
  elif value == 1:
@@ -42,7 +52,7 @@ class RTKStatus(Enum):
42
52
  UNKNOWN = 6
43
53
 
44
54
  @staticmethod
45
- def from_value(value: int):
55
+ def from_value(value: int) -> "RTKStatus":
46
56
  if value == 0:
47
57
  return RTKStatus.NONE
48
58
  elif value == 1 or value == 2: