pymammotion 0.5.69__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.
Files changed (154) hide show
  1. pymammotion/__init__.py +53 -0
  2. pymammotion/agora/__init__.py +0 -0
  3. pymammotion/agora/agora_api.py +755 -0
  4. pymammotion/agora/agora_rtc_capabilities.py +748 -0
  5. pymammotion/agora/agora_websockets.py +1175 -0
  6. pymammotion/aliyun/__init__.py +1 -0
  7. pymammotion/aliyun/client.py +235 -0
  8. pymammotion/aliyun/cloud_gateway.py +982 -0
  9. pymammotion/aliyun/model/aep_response.py +21 -0
  10. pymammotion/aliyun/model/connect_response.py +51 -0
  11. pymammotion/aliyun/model/dev_by_account_response.py +195 -0
  12. pymammotion/aliyun/model/login_by_oauth_response.py +64 -0
  13. pymammotion/aliyun/model/regions_response.py +29 -0
  14. pymammotion/aliyun/model/session_by_authcode_response.py +19 -0
  15. pymammotion/aliyun/model/thing_response.py +12 -0
  16. pymammotion/aliyun/regions.py +62 -0
  17. pymammotion/aliyun/tea/core.py +297 -0
  18. pymammotion/aliyun/tmp_constant.py +171 -0
  19. pymammotion/bluetooth/__init__.py +1 -0
  20. pymammotion/bluetooth/ble.py +62 -0
  21. pymammotion/bluetooth/ble_message.py +676 -0
  22. pymammotion/bluetooth/const.py +27 -0
  23. pymammotion/bluetooth/data/__init__.py +0 -0
  24. pymammotion/bluetooth/data/convert.py +25 -0
  25. pymammotion/bluetooth/data/framectrldata.py +40 -0
  26. pymammotion/bluetooth/data/notifydata.py +62 -0
  27. pymammotion/bluetooth/model/__init__.py +0 -0
  28. pymammotion/bluetooth/model/atomic_integer.py +54 -0
  29. pymammotion/const.py +13 -0
  30. pymammotion/data/__init__.py +0 -0
  31. pymammotion/data/model/__init__.py +8 -0
  32. pymammotion/data/model/account.py +8 -0
  33. pymammotion/data/model/device.py +192 -0
  34. pymammotion/data/model/device_config.py +72 -0
  35. pymammotion/data/model/device_info.py +60 -0
  36. pymammotion/data/model/device_limits.py +49 -0
  37. pymammotion/data/model/enums.py +77 -0
  38. pymammotion/data/model/errors.py +12 -0
  39. pymammotion/data/model/events.py +14 -0
  40. pymammotion/data/model/generate_geojson.py +565 -0
  41. pymammotion/data/model/generate_route_information.py +26 -0
  42. pymammotion/data/model/hash_list.py +475 -0
  43. pymammotion/data/model/location.py +36 -0
  44. pymammotion/data/model/mowing_modes.py +77 -0
  45. pymammotion/data/model/rapid_state.py +45 -0
  46. pymammotion/data/model/raw_data.py +215 -0
  47. pymammotion/data/model/region_data.py +102 -0
  48. pymammotion/data/model/report_info.py +182 -0
  49. pymammotion/data/model/work.py +27 -0
  50. pymammotion/data/mower_state_manager.py +369 -0
  51. pymammotion/data/mqtt/__init__.py +1 -0
  52. pymammotion/data/mqtt/event.py +227 -0
  53. pymammotion/data/mqtt/mammotion_properties.py +276 -0
  54. pymammotion/data/mqtt/properties.py +203 -0
  55. pymammotion/data/mqtt/status.py +57 -0
  56. pymammotion/event/__init__.py +6 -0
  57. pymammotion/event/event.py +96 -0
  58. pymammotion/homeassistant/__init__.py +3 -0
  59. pymammotion/homeassistant/mower_api.py +514 -0
  60. pymammotion/homeassistant/rtk_api.py +54 -0
  61. pymammotion/http/__init__.py +0 -0
  62. pymammotion/http/encryption.py +220 -0
  63. pymammotion/http/http.py +673 -0
  64. pymammotion/http/model/__init__.py +0 -0
  65. pymammotion/http/model/camera_stream.py +31 -0
  66. pymammotion/http/model/http.py +249 -0
  67. pymammotion/http/model/response_factory.py +61 -0
  68. pymammotion/http/model/rtk.py +16 -0
  69. pymammotion/mammotion/__init__.py +0 -0
  70. pymammotion/mammotion/commands/__init__.py +0 -0
  71. pymammotion/mammotion/commands/abstract_message.py +24 -0
  72. pymammotion/mammotion/commands/mammotion_command.py +81 -0
  73. pymammotion/mammotion/commands/messages/__init__.py +0 -0
  74. pymammotion/mammotion/commands/messages/basestation.py +43 -0
  75. pymammotion/mammotion/commands/messages/driver.py +122 -0
  76. pymammotion/mammotion/commands/messages/media.py +87 -0
  77. pymammotion/mammotion/commands/messages/navigation.py +564 -0
  78. pymammotion/mammotion/commands/messages/network.py +205 -0
  79. pymammotion/mammotion/commands/messages/ota.py +38 -0
  80. pymammotion/mammotion/commands/messages/system.py +330 -0
  81. pymammotion/mammotion/commands/messages/video.py +33 -0
  82. pymammotion/mammotion/control/__init__.py +0 -0
  83. pymammotion/mammotion/control/joystick.py +145 -0
  84. pymammotion/mammotion/devices/__init__.py +29 -0
  85. pymammotion/mammotion/devices/base.py +163 -0
  86. pymammotion/mammotion/devices/mammotion.py +571 -0
  87. pymammotion/mammotion/devices/mammotion_bluetooth.py +496 -0
  88. pymammotion/mammotion/devices/mammotion_cloud.py +355 -0
  89. pymammotion/mammotion/devices/mammotion_mower_ble.py +48 -0
  90. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  91. pymammotion/mammotion/devices/managers/managers.py +81 -0
  92. pymammotion/mammotion/devices/mower_device.py +120 -0
  93. pymammotion/mammotion/devices/mower_manager.py +107 -0
  94. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  95. pymammotion/mammotion/devices/rtk_cloud.py +115 -0
  96. pymammotion/mammotion/devices/rtk_device.py +50 -0
  97. pymammotion/mammotion/devices/rtk_manager.py +125 -0
  98. pymammotion/mqtt/__init__.py +6 -0
  99. pymammotion/mqtt/aliyun_mqtt.py +237 -0
  100. pymammotion/mqtt/linkkit/__init__.py +5 -0
  101. pymammotion/mqtt/linkkit/h2client.py +585 -0
  102. pymammotion/mqtt/linkkit/linkkit.py +3025 -0
  103. pymammotion/mqtt/mammotion_future.py +26 -0
  104. pymammotion/mqtt/mammotion_mqtt.py +214 -0
  105. pymammotion/mqtt/mqtt_models.py +66 -0
  106. pymammotion/proto/__init__.py +4841 -0
  107. pymammotion/proto/basestation.proto +51 -0
  108. pymammotion/proto/basestation_pb2.py +35 -0
  109. pymammotion/proto/basestation_pb2.pyi +89 -0
  110. pymammotion/proto/common.proto +7 -0
  111. pymammotion/proto/common_pb2.py +25 -0
  112. pymammotion/proto/common_pb2.pyi +13 -0
  113. pymammotion/proto/dev_net.proto +321 -0
  114. pymammotion/proto/dev_net_pb2.py +111 -0
  115. pymammotion/proto/dev_net_pb2.pyi +515 -0
  116. pymammotion/proto/luba_msg.proto +76 -0
  117. pymammotion/proto/luba_msg_pb2.py +41 -0
  118. pymammotion/proto/luba_msg_pb2.pyi +97 -0
  119. pymammotion/proto/luba_mul.proto +129 -0
  120. pymammotion/proto/luba_mul_pb2.py +61 -0
  121. pymammotion/proto/luba_mul_pb2.pyi +178 -0
  122. pymammotion/proto/mctrl_driver.proto +107 -0
  123. pymammotion/proto/mctrl_driver_pb2.py +57 -0
  124. pymammotion/proto/mctrl_driver_pb2.pyi +167 -0
  125. pymammotion/proto/mctrl_nav.proto +591 -0
  126. pymammotion/proto/mctrl_nav_pb2.py +136 -0
  127. pymammotion/proto/mctrl_nav_pb2.pyi +1067 -0
  128. pymammotion/proto/mctrl_ota.proto +80 -0
  129. pymammotion/proto/mctrl_ota_pb2.py +45 -0
  130. pymammotion/proto/mctrl_ota_pb2.pyi +128 -0
  131. pymammotion/proto/mctrl_pept.proto +34 -0
  132. pymammotion/proto/mctrl_pept_pb2.py +33 -0
  133. pymammotion/proto/mctrl_pept_pb2.pyi +58 -0
  134. pymammotion/proto/mctrl_sys.proto +741 -0
  135. pymammotion/proto/mctrl_sys_pb2.py +206 -0
  136. pymammotion/proto/mctrl_sys_pb2.pyi +1213 -0
  137. pymammotion/proto/message_pool.py +3 -0
  138. pymammotion/proto/py.typed +0 -0
  139. pymammotion/py.typed +0 -0
  140. pymammotion/utility/constant/__init__.py +3 -0
  141. pymammotion/utility/constant/device_constant.py +315 -0
  142. pymammotion/utility/conversions.py +5 -0
  143. pymammotion/utility/datatype_converter.py +124 -0
  144. pymammotion/utility/device_config.py +755 -0
  145. pymammotion/utility/device_type.py +489 -0
  146. pymammotion/utility/map.py +259 -0
  147. pymammotion/utility/movement.py +18 -0
  148. pymammotion/utility/mur_mur_hash.py +159 -0
  149. pymammotion/utility/periodic.py +106 -0
  150. pymammotion/utility/rocker_util.py +194 -0
  151. pymammotion-0.5.69.dist-info/METADATA +93 -0
  152. pymammotion-0.5.69.dist-info/RECORD +154 -0
  153. pymammotion-0.5.69.dist-info/WHEEL +4 -0
  154. pymammotion-0.5.69.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,12 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from mashumaro.mixins.orjson import DataClassORJSONMixin
4
+
5
+ from pymammotion.http.model.http import ErrorInfo
6
+
7
+
8
+ @dataclass
9
+ class DeviceErrors(DataClassORJSONMixin):
10
+ err_code_list: list[int] = field(default_factory=list)
11
+ err_code_list_time: list[int] = field(default_factory=list)
12
+ error_codes: dict[str, ErrorInfo] = field(default_factory=dict)
@@ -0,0 +1,14 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from mashumaro.mixins.orjson import DataClassORJSONMixin
4
+
5
+
6
+ @dataclass
7
+ class WorkTaskEvent(DataClassORJSONMixin):
8
+ hash_area_map: dict[int, int] = field(default_factory=dict)
9
+ ids: list[int] = field(default_factory=list)
10
+
11
+
12
+ @dataclass
13
+ class Events(DataClassORJSONMixin):
14
+ work_tasks_event: WorkTaskEvent = field(default_factory=WorkTaskEvent)
@@ -0,0 +1,565 @@
1
+ import json
2
+ import logging
3
+ import math
4
+ from typing import Any
5
+
6
+ from shapely.geometry import Point
7
+
8
+ from pymammotion.data.model.hash_list import (
9
+ AreaHashNameList,
10
+ CommDataCouple,
11
+ FrameList,
12
+ HashList,
13
+ MowPath,
14
+ NavGetCommData,
15
+ )
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ GEOMETRY_TYPES: list[str] = ["Polygon", "Polygon", "LineString", "Point"]
21
+ MAP_OBJECT_TYPES: list[str] = ["area", "path", "obstacle", "dump"]
22
+
23
+ image_path = "/local/community/ha-mammotion-map/dist/assets/map/"
24
+
25
+ RTK_IMAGE = {
26
+ "iconImage": "map_icon_base_station_rtk.webp",
27
+ "iconSize": [30, 30],
28
+ "iconAnchor": [15, 30],
29
+ "iconUrl": f"{image_path}map_icon_base_station_rtk.webp",
30
+ }
31
+
32
+ DOCK_IMAGE = {
33
+ "iconImage": "icon_map_recharge.webp",
34
+ "iconSize": [30, 30],
35
+ "iconAnchor": [15, 15],
36
+ "iconUrl": f"{image_path}icon_map_recharge.webp",
37
+ }
38
+
39
+ #############################
40
+ # STYLE CONFIGURATION
41
+ #############################
42
+
43
+ DOCK_STYLE = {"color": "lightgray", "fill": "lightgray", "weight": 2, "opacity": 1.0, "fillOpacity": 0.7, "radius": 10}
44
+
45
+ RTK_STYLE = {"color": "purple", "fill": "purple", "weight": 2, "opacity": 1.0, "fillOpacity": 0.7, "radius": 7}
46
+
47
+ AREA_STYLE = {
48
+ "color": "green",
49
+ "fill": "darkgreen",
50
+ "weight": 3,
51
+ "opacity": 0.8,
52
+ "fillOpacity": 0.3,
53
+ "dashArray": "",
54
+ "lineCap": "round",
55
+ "lineJoin": "round",
56
+ }
57
+
58
+ OBSTACLE_STYLE = {
59
+ "color": "#FF4D00",
60
+ "fill": "darkorange",
61
+ "weight": 2,
62
+ "opacity": 0.9,
63
+ "fillOpacity": 0.4,
64
+ "dashArray": "",
65
+ "lineCap": "round",
66
+ "lineJoin": "round",
67
+ }
68
+
69
+ PATH_STYLE = {
70
+ "color": "#ffffff",
71
+ "weight": 8,
72
+ "opacity": 1.0,
73
+ "zIndex": -1,
74
+ "dashArray": "",
75
+ "lineCap": "round",
76
+ "lineJoin": "round",
77
+ "road_center_color": "#696969",
78
+ "road_center_dash": "8, 8",
79
+ }
80
+
81
+ POINT_STYLE = {"color": "blue", "fill": "lightblue", "weight": 2, "opacity": 1.0, "fillOpacity": 0.7, "radius": 5}
82
+
83
+ geojson_metadata = {"name": "Lawn Areas", "description": "Generated from Mammotion diagnostics data"}
84
+
85
+ # Map type IDs
86
+ TYPE_MOWING_ZONE: int = 0
87
+ TYPE_OBSTACLE: int = 1
88
+ TYPE_PATH: int = 2
89
+ TYPE_MOW_PATH: int = 4
90
+
91
+ # Coordinate conversion constants
92
+ METERS_PER_DEGREE: int = 111320
93
+
94
+ # Type aliases
95
+ Coordinate = tuple[float, float]
96
+ CoordinateList = list[list[float]]
97
+ GeoJSONFeature = dict[str, Any]
98
+ GeoJSONCollection = dict[str, Any]
99
+ LocationDict = dict[str, Coordinate]
100
+
101
+ # Path to mammotion integration diagnostics file
102
+ geometry_types: list[str] = GEOMETRY_TYPES
103
+
104
+
105
+ class GeojsonGenerator:
106
+ """Class for logging GeoJSON data."""
107
+
108
+ @staticmethod
109
+ def is_overlapping(p: Point, placed_points: list[Point], min_distance: float = 0.00005) -> bool:
110
+ """Check if point p is too close to any previously placed label."""
111
+ return any(p.distance(existing) < min_distance for existing in placed_points)
112
+
113
+ @staticmethod
114
+ def apply_meter_offsets(lon: float, lat: float, lon_offset: float, lat_offset: float) -> list[float]:
115
+ """Apply meter-based offsets to coordinates (in degrees)"""
116
+ new_lon = lon + (lon_offset / (METERS_PER_DEGREE * math.cos(math.radians(lat))))
117
+ new_lat = lat + (lat_offset / METERS_PER_DEGREE)
118
+ return [new_lon, new_lat]
119
+
120
+ @staticmethod
121
+ def generate_geojson(
122
+ hash_list: HashList, rtk_location: Point, dock_location: Point, dock_rotation: int
123
+ ) -> GeoJSONCollection:
124
+ """Generate GeoJSON from hash list data.
125
+
126
+ Args:
127
+ hash_list: HashList object containing map data
128
+ rtk_location: Tuple of (longitude, latitude) for rtk position
129
+ :param hash_list:
130
+ :param rtk_location:
131
+ :param dock_rotation:
132
+ :param dock_location:
133
+
134
+ """
135
+ area_names = GeojsonGenerator._build_area_name_lookup(hash_list.area_name)
136
+
137
+ geo_json: GeoJSONCollection = {"type": "FeatureCollection", "name": "Lawn Areas", "features": []}
138
+ GeojsonGenerator._add_rtk_and_dock(rtk_location, dock_location, dock_rotation, geo_json)
139
+ total_frames = GeojsonGenerator._process_map_objects(hash_list, rtk_location, area_names, geo_json)
140
+
141
+ # _save_geojson(geo_json)
142
+ return geo_json
143
+
144
+ @staticmethod
145
+ def generate_mow_path_geojson(hash_list: HashList, rtk_location: Point) -> GeoJSONCollection:
146
+ """Generate GeoJSON from hash list data."""
147
+ geo_json: GeoJSONCollection = {"type": "FeatureCollection", "name": "Mowing Lawn Areas", "features": []}
148
+
149
+ total_frames = GeojsonGenerator._process_mow_map_objects(hash_list, rtk_location, geo_json)
150
+ return geo_json
151
+
152
+ @staticmethod
153
+ def _add_rtk_and_dock(
154
+ rtk_location: Point, dock_location: Point, dock_rotation: int, geo_json: GeoJSONCollection
155
+ ) -> None:
156
+ geo_json["features"].append(
157
+ {
158
+ "type": "Feature",
159
+ "properties": {
160
+ "title": "RTK Base",
161
+ "Name": "RTK Base",
162
+ "description": "RTK Base Station location",
163
+ "type_name": "station",
164
+ **RTK_STYLE,
165
+ **RTK_IMAGE,
166
+ },
167
+ "geometry": {"type": "Point", "coordinates": [rtk_location.y, rtk_location.x]},
168
+ }
169
+ )
170
+
171
+ geo_json["features"].append(
172
+ {
173
+ "type": "Feature",
174
+ "properties": {
175
+ "title": "Dock",
176
+ "Name": "Dock",
177
+ "description": "Charging dock location",
178
+ "type_name": "station",
179
+ "rotation": dock_rotation,
180
+ **DOCK_STYLE,
181
+ **DOCK_IMAGE,
182
+ },
183
+ "geometry": {"type": "Point", "coordinates": [dock_location.y, dock_location.x]},
184
+ }
185
+ )
186
+
187
+ @staticmethod
188
+ def _build_area_name_lookup(area_names: list[AreaHashNameList]) -> dict[int, str]:
189
+ """Build a hash lookup table for area names.
190
+
191
+ Args:
192
+ area_names: List of AreaHashNameList objects
193
+
194
+ Returns:
195
+ Dictionary mapping hash to area name
196
+
197
+ """
198
+ return {item.hash: item.name for item in area_names}
199
+
200
+ @staticmethod
201
+ def _process_map_objects(
202
+ hash_list: HashList, rtk_location: Point, area_names: dict[int, str], geo_json: GeoJSONCollection
203
+ ) -> int:
204
+ """Process all map objects and add them to GeoJSON.
205
+
206
+ Args:
207
+ hash_list: HashList object containing map data
208
+ rtk_location: Tuple of (longitude, latitude) for rtk position
209
+ area_names: Dictionary mapping hash to area name
210
+ geo_json: GeoJSON collection to add features to
211
+
212
+ Returns:
213
+ Total number of frames processed
214
+
215
+ """
216
+ total_frames = 0
217
+
218
+ # Map type names to their corresponding dictionaries in HashList
219
+ type_mapping: dict[str, dict[int, FrameList]] = {
220
+ "area": hash_list.area,
221
+ "path": hash_list.path,
222
+ "obstacle": hash_list.obstacle,
223
+ "dump": hash_list.dump,
224
+ }
225
+
226
+ for type_name, map_objects in type_mapping.items():
227
+ for hash_key, frame_list in map_objects.items():
228
+ if not GeojsonGenerator._validate_frame_list(frame_list, hash_key, area_names):
229
+ continue
230
+
231
+ local_coords = GeojsonGenerator._collect_frame_coordinates(frame_list)
232
+ total_frames += len(frame_list.data)
233
+
234
+ lonlat_coords = GeojsonGenerator._convert_to_lonlat_coords(local_coords, rtk_location)
235
+ length, area = GeojsonGenerator.map_object_stats(local_coords)
236
+
237
+ feature = GeojsonGenerator._create_feature(hash_key, frame_list, type_name, lonlat_coords, length, area)
238
+ if feature:
239
+ geo_json["features"].append(feature)
240
+
241
+ return total_frames
242
+
243
+ @staticmethod
244
+ def _process_mow_map_objects(hash_list: HashList, rtk_location: Point, geo_json: GeoJSONCollection) -> int:
245
+ """Process all mow path objects and add them to GeoJSON.
246
+
247
+ Each transaction_id in current_mow_path represents a separate mowing path
248
+ consisting of multiple frames. A feature is generated only when all
249
+ frames for that transaction_id have been received.
250
+ """
251
+ total_frames = 0
252
+
253
+ for transaction_id, frames_by_index in hash_list.current_mow_path.items():
254
+ if not frames_by_index:
255
+ continue
256
+
257
+ # Use any frame to determine total_frame and other metadata
258
+ any_mow_path = next(iter(frames_by_index.values()))
259
+ total_frame = any_mow_path.total_frame
260
+
261
+ if total_frame == 0:
262
+ continue
263
+
264
+ # Only generate a feature when we have all frames for this transaction
265
+ if len(frames_by_index) != total_frame:
266
+ continue
267
+
268
+ ordered_mow_paths = [frames_by_index[i] for i in sorted(frames_by_index.keys())]
269
+ local_coords = GeojsonGenerator._collect_mow_frame_coordinates(ordered_mow_paths)
270
+ total_frames += 1
271
+
272
+ lonlat_coords = GeojsonGenerator._convert_to_lonlat_coords(local_coords, rtk_location)
273
+ length, area = GeojsonGenerator.map_object_stats(local_coords)
274
+
275
+ feature = GeojsonGenerator._create_mow_path_feature(any_mow_path, lonlat_coords, length, area)
276
+ if feature:
277
+ geo_json["features"].append(feature)
278
+
279
+ return total_frames
280
+
281
+ @staticmethod
282
+ def _process_svg_map_objects(hash_list: HashList, rtk_location: Point, geo_json: GeoJSONCollection) -> None:
283
+ """Process all SVG map objects and add them to GeoJSON."""
284
+ for hash_key, frame_list in hash_list.svg.items():
285
+ logger.debug(hash_key, frame_list)
286
+
287
+ @staticmethod
288
+ def _validate_frame_list(frame_list: FrameList, hash_key: int, area_names: dict[int, str] | None = None) -> bool:
289
+ """Validate that frame list has complete frame data.
290
+
291
+ Args:
292
+ frame_list: FrameList object to validate
293
+ hash_key: Hash key for the area
294
+ area_names: Dictionary mapping hash to area name
295
+
296
+ Returns:
297
+ True if valid, False otherwise
298
+
299
+ """
300
+ if len(frame_list.data) != frame_list.total_frame:
301
+ area_name = area_names.get(hash_key, "Unknown") if area_names else "Unknown"
302
+ logger.debug(f"Error: full coord data not available for area: '{area_name}' - '{hash_key}'")
303
+ return False
304
+ return True
305
+
306
+ @staticmethod
307
+ def _collect_frame_coordinates(frame_list: FrameList) -> list[CommDataCouple]:
308
+ """Collect coordinates from all frames in a FrameList.
309
+
310
+ Args:
311
+ frame_list: FrameList containing frame data
312
+
313
+ Returns:
314
+ List of coordinate dictionaries with 'x' and 'y' keys
315
+
316
+ """
317
+ local_coords: list[CommDataCouple] = []
318
+ for frame in frame_list.data:
319
+ if isinstance(frame, NavGetCommData):
320
+ local_coords.extend(frame.data_couple)
321
+ # TODO svg message needs different transform
322
+ # elif isinstance(frame, SvgMessage):
323
+ # local_coords.extend(frame.)
324
+ return local_coords
325
+
326
+ @staticmethod
327
+ def _collect_mow_frame_coordinates(mow_path_list: list[MowPath]) -> list[CommDataCouple]:
328
+ """Collect coordinates from all frames in a FrameList."""
329
+ local_coords: list[CommDataCouple] = []
330
+ for mow_frame in mow_path_list:
331
+ for frame in mow_frame.path_packets:
332
+ local_coords.extend(frame.data_couple)
333
+ return local_coords
334
+
335
+ @staticmethod
336
+ def _convert_to_lonlat_coords(
337
+ local_coords: list[CommDataCouple], rtk_location: Point, x_offset: int = 0, y_offset: int = 0
338
+ ) -> CoordinateList:
339
+ """Convert local x,y coordinates to lon,lat coordinates.
340
+
341
+ Args:
342
+ local_coords: List of coordinate dictionaries with 'x' and 'y' keys
343
+ rtk_location: Tuple of (longitude, latitude) for rtk position
344
+
345
+ Returns:
346
+ List of [longitude, latitude] coordinate pairs
347
+
348
+ """
349
+ lonlat_coords: CoordinateList = [
350
+ list(GeojsonGenerator.lon_lat_delta(rtk_location, xy.x + x_offset, xy.y + y_offset)) for xy in local_coords
351
+ ]
352
+ lonlat_coords.reverse() # GeoJSON polygons go clockwise
353
+ return lonlat_coords
354
+
355
+ @staticmethod
356
+ def _create_feature(
357
+ hash_key: int, frame_list: FrameList, type_name: str, lonlat_coords: CoordinateList, length: float, area: float
358
+ ) -> GeoJSONFeature | None:
359
+ """Create a GeoJSON feature from frame list data.
360
+
361
+ Args:
362
+ hash_key: Hash identifier for the feature
363
+ frame_list: FrameList containing frame data
364
+ type_name: Type name of the map object
365
+ lonlat_coords: List of [longitude, latitude] coordinate pairs
366
+ length: Calculated length of the feature
367
+ area: Calculated area of the feature
368
+
369
+ Returns:
370
+ GeoJSON feature dictionary or None if invalid
371
+
372
+ """
373
+ first_frame = frame_list.data[0]
374
+ type_id = first_frame.type
375
+ object_name = ""
376
+ if isinstance(first_frame, NavGetCommData):
377
+ object_name = first_frame.name_time.name
378
+
379
+ properties = GeojsonGenerator._create_feature_properties(
380
+ hash_key, type_id, type_name, first_frame, length, area, object_name
381
+ )
382
+ geometry = GeojsonGenerator._create_feature_geometry(type_id, lonlat_coords, properties)
383
+
384
+ if geometry is None:
385
+ return None
386
+
387
+ return {"type": "Feature", "properties": properties, "geometry": geometry}
388
+
389
+ @staticmethod
390
+ def _create_mow_path_feature(
391
+ path_packet_list: MowPath, lonlat_coords: CoordinateList, length: float, area: float
392
+ ) -> GeoJSONFeature | None:
393
+ properties = GeojsonGenerator._create_feature_mow_path_properties(
394
+ path_packet_list, length, path_packet_list.area
395
+ )
396
+ geometry = GeojsonGenerator._create_feature_geometry(4, lonlat_coords, properties)
397
+
398
+ if geometry is None:
399
+ return None
400
+
401
+ return {"type": "Feature", "properties": properties, "geometry": geometry}
402
+
403
+ @staticmethod
404
+ def _create_feature_mow_path_properties(first_frame: MowPath, length: float, area: float) -> dict[str, Any]:
405
+ """Create properties dictionary for GeoJSON feature."""
406
+ return {
407
+ "transaction_id": first_frame.transaction_id,
408
+ "type_name": "mow_path",
409
+ "total_path_num": first_frame.total_path_num,
410
+ "length": length,
411
+ "area": area,
412
+ "time": first_frame.time,
413
+ }
414
+
415
+ @staticmethod
416
+ def _create_feature_properties(
417
+ hash_key: int, type_id: int, type_name: str, first_frame: Any, length: float, area: float, object_name: str = ""
418
+ ) -> dict[str, Any]:
419
+ """Create properties dictionary for GeoJSON feature.
420
+
421
+ Args:
422
+ hash_key: Hash identifier
423
+ object_name: Name of the object
424
+ type_id: Type ID of the feature
425
+ type_name: Type name of the feature
426
+ first_frame: First frame from the FrameList
427
+ length: Calculated length
428
+ area: Calculated area
429
+
430
+ Returns:
431
+ Properties dictionary
432
+
433
+ """
434
+ return {
435
+ "hash": hash_key,
436
+ "title": object_name,
437
+ "Name": object_name,
438
+ "description": "description <b>test</b>",
439
+ "type_id": type_id,
440
+ "type_name": type_name,
441
+ "parent_hash_a": first_frame.paternal_hash_a,
442
+ "parent_hash_b": first_frame.paternal_hash_b,
443
+ "length": length,
444
+ "area": area,
445
+ }
446
+
447
+ @staticmethod
448
+ def _create_feature_geometry(
449
+ type_id: int, lonlat_coords: CoordinateList, properties: dict[str, Any]
450
+ ) -> dict[str, Any] | None:
451
+ """Create geometry dictionary for GeoJSON feature based on type.
452
+
453
+ Args:
454
+ type_id: Type ID determining geometry type
455
+ lonlat_coords: List of [longitude, latitude] coordinate pairs
456
+ properties: Properties dictionary to modify with style information
457
+
458
+ Returns:
459
+ Geometry dictionary or None if invalid type
460
+
461
+ """
462
+ if type_id == TYPE_MOWING_ZONE:
463
+ properties.update(AREA_STYLE)
464
+ return {"type": "Polygon", "coordinates": [lonlat_coords]}
465
+ elif type_id == TYPE_OBSTACLE:
466
+ properties.update(OBSTACLE_STYLE)
467
+ return {"type": "Polygon", "coordinates": [lonlat_coords]}
468
+ elif type_id == TYPE_PATH and len(lonlat_coords) > 1:
469
+ properties.update(PATH_STYLE)
470
+ return {"type": "LineString", "coordinates": lonlat_coords}
471
+ elif type_id == TYPE_MOW_PATH and len(lonlat_coords) > 1:
472
+ properties["color"] = "green"
473
+ return {"type": "LineString", "coordinates": lonlat_coords}
474
+ else:
475
+ return None # Point (ignore)
476
+
477
+ @staticmethod
478
+ def _save_geojson(geoJSON: GeoJSONCollection) -> None:
479
+ """Save GeoJSON data to file.
480
+
481
+ Args:
482
+ geoJSON: GeoJSON collection to save
483
+
484
+ """
485
+ with open("areas.json", "w") as json_file:
486
+ json.dump(geoJSON, json_file, indent=2)
487
+
488
+ @staticmethod
489
+ def lon_lat_delta(rtk: Point, x: float, y: float) -> Coordinate:
490
+ """Add delta (in meters) to lon/lat, return new lon/lat.
491
+
492
+ Args:
493
+ lon: Longitude in degrees
494
+ lat: Latitude in degrees
495
+ x: X offset in meters
496
+ y: Y offset in meters
497
+
498
+ Returns:
499
+ Tuple of (new_longitude, new_latitude)
500
+
501
+ """
502
+ new_lon = rtk.y + (x / (METERS_PER_DEGREE * math.cos(math.radians(rtk.x))))
503
+ new_lat = rtk.x + (y / METERS_PER_DEGREE)
504
+ return new_lon, new_lat
505
+
506
+ @staticmethod
507
+ def map_object_stats(coords: list[CommDataCouple]) -> Coordinate:
508
+ """Calculate length and area statistics for map object coordinates.
509
+
510
+ Args:
511
+ coords: List of coordinate dictionaries with 'x' and 'y' keys
512
+
513
+ Returns:
514
+ Tuple of (length, area) in meters and square meters
515
+
516
+ """
517
+ # Point Object
518
+ if len(coords) < 2:
519
+ return 0.0, 0.0
520
+
521
+ def distance(p1: CommDataCouple, p2: CommDataCouple) -> float:
522
+ """Calculate Euclidean distance between two points."""
523
+ return math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2)
524
+
525
+ length = sum(distance(coords[i], coords[i + 1]) for i in range(len(coords) - 1))
526
+
527
+ # Open line
528
+ if coords[0] != coords[-1]:
529
+ return length, 0.0
530
+
531
+ # Closed Polygon - Calculate area using shoelace formula
532
+ area = 0.5 * abs(
533
+ sum(coords[i].x * coords[i + 1].y - coords[i + 1].x * coords[i].y for i in range(len(coords) - 1))
534
+ )
535
+
536
+ return length, area
537
+
538
+ @staticmethod
539
+ def is_point_in_polygon(x: float, y: float, poly: list[list[float]]) -> bool:
540
+ """Test if a point is inside a polygon using ray casting algorithm.
541
+
542
+ Args:
543
+ x: X coordinate of the point
544
+ y: Y coordinate of the point
545
+ poly: Polygon as list of [x, y] coordinate pairs
546
+
547
+ Returns:
548
+ True if point is inside polygon, False otherwise
549
+
550
+ """
551
+ return (
552
+ sum(
553
+ (y > poly[i][1]) != (y > poly[(i + 1) % len(poly)][1])
554
+ and (
555
+ x
556
+ < (poly[(i + 1) % len(poly)][0] - poly[i][0])
557
+ * (y - poly[i][1])
558
+ / (poly[(i + 1) % len(poly)][1] - poly[i][1])
559
+ + poly[i][0]
560
+ )
561
+ for i in range(len(poly))
562
+ )
563
+ % 2
564
+ == 1
565
+ )
@@ -0,0 +1,26 @@
1
+ from dataclasses import dataclass, field
2
+ import logging
3
+
4
+ logger = logging.getLogger(__name__)
5
+
6
+
7
+ @dataclass
8
+ class GenerateRouteInformation:
9
+ """Creates a model for generating route information and mowing plan before starting a job."""
10
+
11
+ one_hashs: list[int] = field(default_factory=list)
12
+ job_mode: int = 4 # taskMode
13
+ job_version: int = 0
14
+ job_id: int = 0
15
+ speed: float = 0.3
16
+ ultra_wave: int = 2 # touch no touch etc
17
+ channel_mode: int = 0 # line mode is grid single double or single2
18
+ channel_width: int = 25
19
+ rain_tactics: int = 0
20
+ blade_height: int = 0
21
+ path_order: str = ""
22
+ toward: int = 0 # is just angle
23
+ toward_included_angle: int = 0
24
+ toward_mode: int = 0 # angle type relative etc
25
+ edge_mode: int = 1 # border laps
26
+ obstacle_laps: int = 1