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.
- pymammotion/__init__.py +9 -6
- pymammotion/aliyun/client.py +235 -0
- pymammotion/aliyun/cloud_gateway.py +320 -69
- pymammotion/aliyun/model/aep_response.py +1 -2
- pymammotion/aliyun/model/dev_by_account_response.py +170 -23
- pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
- pymammotion/aliyun/model/regions_response.py +3 -3
- pymammotion/aliyun/model/session_by_authcode_response.py +2 -2
- pymammotion/aliyun/model/thing_response.py +12 -0
- pymammotion/aliyun/regions.py +62 -0
- pymammotion/aliyun/tea/core.py +297 -0
- pymammotion/bluetooth/ble.py +11 -15
- pymammotion/bluetooth/ble_message.py +389 -106
- pymammotion/bluetooth/model/atomic_integer.py +54 -0
- pymammotion/const.py +3 -0
- pymammotion/data/model/__init__.py +1 -2
- pymammotion/data/model/device.py +92 -240
- pymammotion/data/model/device_config.py +10 -24
- pymammotion/data/model/device_info.py +35 -0
- pymammotion/data/model/device_limits.py +49 -0
- pymammotion/data/model/enums.py +12 -2
- pymammotion/data/model/errors.py +12 -0
- pymammotion/data/model/events.py +14 -0
- pymammotion/data/model/generate_geojson.py +521 -0
- pymammotion/data/model/generate_route_information.py +3 -4
- pymammotion/data/model/hash_list.py +384 -48
- pymammotion/data/model/location.py +4 -4
- pymammotion/data/model/mowing_modes.py +24 -1
- pymammotion/data/model/raw_data.py +215 -0
- pymammotion/data/model/region_data.py +10 -11
- pymammotion/data/model/report_info.py +62 -6
- pymammotion/data/model/work.py +27 -0
- pymammotion/data/mower_state_manager.py +316 -0
- pymammotion/data/mqtt/event.py +73 -28
- pymammotion/data/mqtt/mammotion_properties.py +257 -0
- pymammotion/data/mqtt/properties.py +93 -78
- pymammotion/data/mqtt/status.py +18 -17
- pymammotion/event/event.py +32 -8
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +484 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/__init__.py +0 -0
- pymammotion/http/encryption.py +220 -0
- pymammotion/http/http.py +652 -44
- pymammotion/http/model/__init__.py +0 -0
- pymammotion/{aliyun/model/stream_subscription_response.py → http/model/camera_stream.py} +14 -2
- pymammotion/http/model/http.py +160 -9
- pymammotion/http/model/response_factory.py +61 -0
- pymammotion/http/model/rtk.py +16 -0
- pymammotion/mammotion/commands/abstract_message.py +7 -5
- pymammotion/mammotion/commands/mammotion_command.py +32 -3
- pymammotion/mammotion/commands/messages/basestation.py +43 -0
- pymammotion/mammotion/commands/messages/driver.py +61 -29
- pymammotion/mammotion/commands/messages/media.py +68 -15
- pymammotion/mammotion/commands/messages/navigation.py +61 -25
- pymammotion/mammotion/commands/messages/network.py +93 -100
- pymammotion/mammotion/commands/messages/ota.py +18 -18
- pymammotion/mammotion/commands/messages/system.py +97 -72
- pymammotion/mammotion/commands/messages/video.py +17 -12
- pymammotion/mammotion/devices/__init__.py +27 -3
- pymammotion/mammotion/devices/base.py +50 -127
- pymammotion/mammotion/devices/mammotion.py +447 -212
- pymammotion/mammotion/devices/mammotion_bluetooth.py +105 -60
- pymammotion/mammotion/devices/mammotion_cloud.py +157 -105
- pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
- pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
- pymammotion/mammotion/devices/managers/managers.py +81 -0
- pymammotion/mammotion/devices/mower_device.py +124 -0
- pymammotion/mammotion/devices/mower_manager.py +107 -0
- pymammotion/mammotion/devices/rtk_ble.py +89 -0
- pymammotion/mammotion/devices/rtk_cloud.py +113 -0
- pymammotion/mammotion/devices/rtk_device.py +50 -0
- pymammotion/mammotion/devices/rtk_manager.py +122 -0
- pymammotion/mqtt/__init__.py +2 -1
- pymammotion/mqtt/aliyun_mqtt.py +232 -0
- pymammotion/mqtt/linkkit/__init__.py +5 -0
- pymammotion/mqtt/linkkit/h2client.py +585 -0
- pymammotion/mqtt/linkkit/linkkit.py +3023 -0
- pymammotion/mqtt/mammotion_mqtt.py +176 -169
- pymammotion/mqtt/mqtt_models.py +66 -0
- pymammotion/proto/__init__.py +4839 -4
- pymammotion/proto/basestation.proto +8 -0
- pymammotion/proto/basestation_pb2.py +11 -9
- pymammotion/proto/basestation_pb2.pyi +16 -2
- pymammotion/proto/dev_net.proto +79 -55
- pymammotion/proto/dev_net_pb2.py +60 -56
- pymammotion/proto/dev_net_pb2.pyi +49 -6
- pymammotion/proto/luba_msg.proto +2 -1
- pymammotion/proto/luba_msg_pb2.py +6 -6
- pymammotion/proto/luba_msg_pb2.pyi +1 -0
- pymammotion/proto/luba_mul.proto +62 -1
- pymammotion/proto/luba_mul_pb2.py +38 -22
- pymammotion/proto/luba_mul_pb2.pyi +94 -7
- pymammotion/proto/mctrl_driver.proto +44 -4
- pymammotion/proto/mctrl_driver_pb2.py +26 -14
- pymammotion/proto/mctrl_driver_pb2.pyi +66 -11
- pymammotion/proto/mctrl_nav.proto +97 -51
- pymammotion/proto/mctrl_nav_pb2.py +75 -67
- pymammotion/proto/mctrl_nav_pb2.pyi +142 -56
- pymammotion/proto/mctrl_ota.proto +40 -2
- pymammotion/proto/mctrl_ota_pb2.py +23 -13
- pymammotion/proto/mctrl_ota_pb2.pyi +67 -4
- pymammotion/proto/mctrl_pept.proto +8 -3
- pymammotion/proto/mctrl_pept_pb2.py +8 -6
- pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
- pymammotion/proto/mctrl_sys.proto +325 -86
- pymammotion/proto/mctrl_sys_pb2.py +162 -98
- pymammotion/proto/mctrl_sys_pb2.pyi +451 -25
- pymammotion/proto/message_pool.py +3 -0
- pymammotion/proto/py.typed +0 -0
- pymammotion/utility/constant/device_constant.py +65 -21
- pymammotion/utility/datatype_converter.py +13 -12
- pymammotion/utility/device_config.py +755 -0
- pymammotion/utility/device_type.py +218 -21
- pymammotion/utility/map.py +238 -51
- pymammotion/utility/mur_mur_hash.py +159 -0
- {pymammotion-0.2.62.dist-info → pymammotion-0.5.51.dist-info}/METADATA +27 -31
- pymammotion-0.5.51.dist-info/RECORD +152 -0
- {pymammotion-0.2.62.dist-info → pymammotion-0.5.51.dist-info}/WHEEL +1 -1
- pymammotion/aliyun/cloud_service.py +0 -65
- pymammotion/data/model/plan.py +0 -58
- pymammotion/data/state_manager.py +0 -130
- pymammotion/proto/basestation.py +0 -59
- pymammotion/proto/common.py +0 -12
- pymammotion/proto/dev_net.py +0 -381
- pymammotion/proto/luba_msg.py +0 -81
- pymammotion/proto/luba_mul.py +0 -76
- pymammotion/proto/mctrl_driver.py +0 -100
- pymammotion/proto/mctrl_nav.py +0 -660
- pymammotion/proto/mctrl_ota.py +0 -48
- pymammotion/proto/mctrl_pept.py +0 -41
- pymammotion/proto/mctrl_sys.py +0 -574
- pymammotion-0.2.62.dist-info/RECORD +0 -125
- /pymammotion/{http/_init_.py → bluetooth/model/__init__.py} +0 -0
- {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", "
|
|
8
|
+
__all__ = ["GenerateRouteInformation", "HashList", "RapidState", "RTKStatus", "RegionData"]
|
pymammotion/data/model/device.py
CHANGED
|
@@ -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
|
|
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.
|
|
11
|
-
from pymammotion.data.model.
|
|
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.
|
|
16
|
-
from pymammotion.
|
|
17
|
-
from pymammotion.proto
|
|
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 =
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
+
)
|
pymammotion/data/model/enums.py
CHANGED
|
@@ -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:
|