pymammotion 0.3.7__tar.gz → 0.4.0a0__tar.gz

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 (132) hide show
  1. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/PKG-INFO +2 -1
  2. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/__init__.py +2 -2
  3. pymammotion-0.4.0a0/pymammotion/data/model/device.py +115 -0
  4. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/model/device_config.py +0 -10
  5. pymammotion-0.4.0a0/pymammotion/data/model/device_limits.py +49 -0
  6. pymammotion-0.3.7/pymammotion/data/model/device.py → pymammotion-0.4.0a0/pymammotion/data/model/raw_data.py +13 -123
  7. pymammotion-0.4.0a0/pymammotion/http/encryption.py +221 -0
  8. pymammotion-0.4.0a0/pymammotion/http/http.py +147 -0
  9. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/http/model/http.py +2 -0
  10. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mammotion/commands/messages/system.py +5 -5
  11. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mammotion/devices/base.py +4 -2
  12. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mammotion/devices/mammotion.py +2 -2
  13. pymammotion-0.4.0a0/pymammotion/utility/device_config.py +363 -0
  14. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pyproject.toml +4 -3
  15. pymammotion-0.3.7/pymammotion/http/http.py +0 -89
  16. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/LICENSE +0 -0
  17. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/README.md +0 -0
  18. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/aliyun/__init__.py +0 -0
  19. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/aliyun/cloud_gateway.py +0 -0
  20. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/aliyun/cloud_service.py +0 -0
  21. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/aliyun/model/aep_response.py +0 -0
  22. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/aliyun/model/connect_response.py +0 -0
  23. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/aliyun/model/dev_by_account_response.py +0 -0
  24. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/aliyun/model/login_by_oauth_response.py +0 -0
  25. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/aliyun/model/regions_response.py +0 -0
  26. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/aliyun/model/session_by_authcode_response.py +0 -0
  27. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/aliyun/model/stream_subscription_response.py +0 -0
  28. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/aliyun/tmp_constant.py +0 -0
  29. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/bluetooth/__init__.py +0 -0
  30. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/bluetooth/ble.py +0 -0
  31. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/bluetooth/ble_message.py +0 -0
  32. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/bluetooth/const.py +0 -0
  33. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/bluetooth/data/__init__.py +0 -0
  34. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/bluetooth/data/convert.py +0 -0
  35. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/bluetooth/data/framectrldata.py +0 -0
  36. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/bluetooth/data/notifydata.py +0 -0
  37. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/bluetooth/model/__init__.py +0 -0
  38. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/bluetooth/model/atomic_integer.py +0 -0
  39. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/const.py +0 -0
  40. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/__init__.py +0 -0
  41. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/model/__init__.py +0 -0
  42. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/model/account.py +0 -0
  43. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/model/device_info.py +0 -0
  44. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/model/enums.py +0 -0
  45. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/model/excute_boarder_params.py +0 -0
  46. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/model/execute_boarder.py +0 -0
  47. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/model/generate_route_information.py +0 -0
  48. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/model/hash_list.py +0 -0
  49. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/model/location.py +0 -0
  50. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/model/mowing_modes.py +0 -0
  51. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/model/plan.py +0 -0
  52. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/model/rapid_state.py +0 -0
  53. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/model/region_data.py +0 -0
  54. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/model/report_info.py +0 -0
  55. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/mqtt/__init__.py +0 -0
  56. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/mqtt/event.py +0 -0
  57. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/mqtt/properties.py +0 -0
  58. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/mqtt/status.py +0 -0
  59. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/data/state_manager.py +0 -0
  60. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/event/__init__.py +0 -0
  61. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/event/event.py +0 -0
  62. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/http/_init_.py +0 -0
  63. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mammotion/__init__.py +0 -0
  64. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mammotion/commands/__init__.py +0 -0
  65. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mammotion/commands/abstract_message.py +0 -0
  66. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mammotion/commands/mammotion_command.py +0 -0
  67. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mammotion/commands/messages/__init__.py +0 -0
  68. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mammotion/commands/messages/driver.py +0 -0
  69. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mammotion/commands/messages/media.py +0 -0
  70. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mammotion/commands/messages/navigation.py +0 -0
  71. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mammotion/commands/messages/network.py +2 -2
  72. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mammotion/commands/messages/ota.py +0 -0
  73. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mammotion/commands/messages/video.py +0 -0
  74. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mammotion/control/__init__.py +0 -0
  75. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mammotion/control/joystick.py +0 -0
  76. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mammotion/devices/__init__.py +0 -0
  77. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mammotion/devices/mammotion_bluetooth.py +0 -0
  78. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mammotion/devices/mammotion_cloud.py +0 -0
  79. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mqtt/__init__.py +0 -0
  80. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mqtt/mammotion_future.py +0 -0
  81. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/mqtt/mammotion_mqtt.py +0 -0
  82. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/__init__.py +0 -0
  83. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/basestation.proto +0 -0
  84. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/basestation.py +0 -0
  85. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/basestation_pb2.py +0 -0
  86. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/basestation_pb2.pyi +0 -0
  87. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/common.proto +0 -0
  88. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/common.py +0 -0
  89. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/common_pb2.py +0 -0
  90. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/common_pb2.pyi +0 -0
  91. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/dev_net.proto +0 -0
  92. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/dev_net.py +0 -0
  93. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/dev_net_pb2.py +0 -0
  94. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/dev_net_pb2.pyi +0 -0
  95. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/luba_msg.proto +0 -0
  96. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/luba_msg.py +0 -0
  97. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/luba_msg_pb2.py +0 -0
  98. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/luba_msg_pb2.pyi +0 -0
  99. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/luba_mul.proto +0 -0
  100. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/luba_mul.py +0 -0
  101. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/luba_mul_pb2.py +0 -0
  102. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/luba_mul_pb2.pyi +0 -0
  103. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_driver.proto +0 -0
  104. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_driver.py +0 -0
  105. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_driver_pb2.py +0 -0
  106. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_driver_pb2.pyi +0 -0
  107. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_nav.proto +0 -0
  108. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_nav.py +0 -0
  109. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_nav_pb2.py +0 -0
  110. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_nav_pb2.pyi +0 -0
  111. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_ota.proto +0 -0
  112. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_ota.py +0 -0
  113. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_ota_pb2.py +0 -0
  114. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_ota_pb2.pyi +0 -0
  115. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_pept.proto +0 -0
  116. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_pept.py +0 -0
  117. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_pept_pb2.py +0 -0
  118. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_pept_pb2.pyi +0 -0
  119. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_sys.proto +0 -0
  120. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_sys.py +0 -0
  121. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_sys_pb2.py +0 -0
  122. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/proto/mctrl_sys_pb2.pyi +0 -0
  123. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/py.typed +0 -0
  124. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/utility/constant/__init__.py +0 -0
  125. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/utility/constant/device_constant.py +0 -0
  126. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/utility/conversions.py +0 -0
  127. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/utility/datatype_converter.py +0 -0
  128. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/utility/device_type.py +0 -0
  129. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/utility/map.py +0 -0
  130. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/utility/movement.py +0 -0
  131. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/utility/periodic.py +0 -0
  132. {pymammotion-0.3.7 → pymammotion-0.4.0a0}/pymammotion/utility/rocker_util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pymammotion
3
- Version: 0.3.7
3
+ Version: 0.4.0a0
4
4
  Summary:
5
5
  License: GNU-3.0
6
6
  Author: Michael Arthur
@@ -21,6 +21,7 @@ Requires-Dist: async-timeout (>=4.0.3,<5.0.0)
21
21
  Requires-Dist: betterproto (>=1.2.5,<2.0.0)
22
22
  Requires-Dist: bleak (>=0.21.0)
23
23
  Requires-Dist: bleak-retry-connector (>=3.5.0,<4.0.0)
24
+ Requires-Dist: cryptography (>=43.0.1)
24
25
  Requires-Dist: jsonic (>=1.0.0,<2.0.0)
25
26
  Requires-Dist: mashumaro (>=3.13,<4.0)
26
27
  Requires-Dist: numpy (>=1.26.0)
@@ -12,7 +12,7 @@ from pymammotion.aliyun.cloud_gateway import CloudIOTGateway
12
12
 
13
13
  # works outside HA on its own
14
14
  from pymammotion.bluetooth.ble import MammotionBLE
15
- from pymammotion.http.http import MammotionHTTP, connect_http
15
+ from pymammotion.http.http import MammotionHTTP
16
16
 
17
17
  # TODO make a working device that will work outside HA too.
18
18
  from pymammotion.mqtt import MammotionMQTT
@@ -20,7 +20,7 @@ from pymammotion.mqtt import MammotionMQTT
20
20
  logger = logging.getLogger(__name__)
21
21
 
22
22
 
23
- __all__ = ["MammotionBLE", "MammotionHTTP", "connect_http", "MammotionMQTT", "logger"]
23
+ __all__ = ["MammotionBLE", "MammotionHTTP", "MammotionMQTT", "logger"]
24
24
 
25
25
 
26
26
  # TODO provide interface to pick between mqtt/cloud/bluetooth
@@ -0,0 +1,115 @@
1
+ """MowingDevice class to wrap around the betterproto dataclasses."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Optional
5
+
6
+ import betterproto
7
+ from mashumaro.mixins.orjson import DataClassORJSONMixin
8
+
9
+ from pymammotion.data.model import HashList, RapidState
10
+ from pymammotion.data.model.device_info import MowerInfo
11
+ from pymammotion.data.model.location import Location
12
+ from pymammotion.data.model.report_info import ReportData
13
+ from pymammotion.data.mqtt.properties import ThingPropertiesMessage
14
+ from pymammotion.http.model.http import ErrorInfo
15
+ from pymammotion.proto.mctrl_sys import (
16
+ MowToAppInfoT,
17
+ ReportInfoData,
18
+ SystemRapidStateTunnelMsg,
19
+ SystemUpdateBufMsg,
20
+ )
21
+ from pymammotion.utility.constant import WorkMode
22
+ from pymammotion.utility.conversions import parse_double
23
+ from pymammotion.utility.map import CoordinateConverter
24
+
25
+
26
+ @dataclass
27
+ class MowingDevice(DataClassORJSONMixin):
28
+ """Wraps the betterproto dataclasses, so we can bypass the groups for keeping all data."""
29
+
30
+ mower_state: MowerInfo = field(default_factory=MowerInfo)
31
+ mqtt_properties: ThingPropertiesMessage | None = None
32
+ map: HashList = field(default_factory=HashList)
33
+ location: Location = field(default_factory=Location)
34
+ mowing_state: RapidState = field(default_factory=RapidState)
35
+ report_data: ReportData = field(default_factory=ReportData)
36
+ err_code_list: list = field(default_factory=list)
37
+ err_code_list_time: Optional[list] = field(default_factory=list)
38
+ error_codes: dict[str, ErrorInfo] = field(default_factory=dict)
39
+
40
+ def buffer(self, buffer_list: SystemUpdateBufMsg) -> None:
41
+ """Update the device based on which buffer we are reading from."""
42
+ match buffer_list.update_buf_data[0]:
43
+ case 1:
44
+ # 4 speed
45
+ self.location.RTK.latitude = parse_double(buffer_list.update_buf_data[5], 8.0)
46
+ self.location.RTK.longitude = parse_double(buffer_list.update_buf_data[6], 8.0)
47
+ self.location.dock.latitude = parse_double(buffer_list.update_buf_data[7], 4.0)
48
+ self.location.dock.longitude = parse_double(buffer_list.update_buf_data[8], 4.0)
49
+ self.location.dock.rotation = buffer_list.update_buf_data[3] + 180
50
+ case 2:
51
+ self.err_code_list.clear()
52
+ self.err_code_list_time.clear()
53
+ self.err_code_list.extend(
54
+ [
55
+ buffer_list.update_buf_data[3],
56
+ buffer_list.update_buf_data[5],
57
+ buffer_list.update_buf_data[7],
58
+ buffer_list.update_buf_data[9],
59
+ buffer_list.update_buf_data[11],
60
+ buffer_list.update_buf_data[13],
61
+ buffer_list.update_buf_data[15],
62
+ buffer_list.update_buf_data[17],
63
+ buffer_list.update_buf_data[19],
64
+ buffer_list.update_buf_data[21],
65
+ ]
66
+ )
67
+ self.err_code_list_time.extend(
68
+ [
69
+ buffer_list.update_buf_data[4],
70
+ buffer_list.update_buf_data[6],
71
+ buffer_list.update_buf_data[8],
72
+ buffer_list.update_buf_data[10],
73
+ buffer_list.update_buf_data[12],
74
+ buffer_list.update_buf_data[14],
75
+ buffer_list.update_buf_data[16],
76
+ buffer_list.update_buf_data[18],
77
+ buffer_list.update_buf_data[20],
78
+ buffer_list.update_buf_data[22],
79
+ ]
80
+ )
81
+
82
+ def update_report_data(self, toapp_report_data: ReportInfoData) -> None:
83
+ coordinate_converter = CoordinateConverter(self.location.RTK.latitude, self.location.RTK.longitude)
84
+ for index, location in enumerate(toapp_report_data.locations):
85
+ if index == 0 and location.real_pos_y != 0:
86
+ self.location.position_type = location.pos_type
87
+ self.location.orientation = location.real_toward / 10000
88
+ self.location.device = coordinate_converter.enu_to_lla(
89
+ parse_double(location.real_pos_y, 4.0), parse_double(location.real_pos_x, 4.0)
90
+ )
91
+ if location.zone_hash:
92
+ self.location.work_zone = (
93
+ location.zone_hash if self.report_data.dev.sys_status == WorkMode.MODE_WORKING else 0
94
+ )
95
+
96
+ self.report_data.update(toapp_report_data.to_dict(casing=betterproto.Casing.SNAKE))
97
+
98
+ def run_state_update(self, rapid_state: SystemRapidStateTunnelMsg) -> None:
99
+ coordinate_converter = CoordinateConverter(self.location.RTK.latitude, self.location.RTK.longitude)
100
+ self.mowing_state = RapidState().from_raw(rapid_state.rapid_state_data)
101
+ self.location.position_type = self.mowing_state.pos_type
102
+ self.location.orientation = self.mowing_state.toward / 10000
103
+ self.location.device = coordinate_converter.enu_to_lla(
104
+ parse_double(self.mowing_state.pos_y, 4.0), parse_double(self.mowing_state.pos_x, 4.0)
105
+ )
106
+ if self.mowing_state.zone_hash:
107
+ self.location.work_zone = (
108
+ self.mowing_state.zone_hash if self.report_data.dev.sys_status == WorkMode.MODE_WORKING else 0
109
+ )
110
+
111
+ def mow_info(self, toapp_mow_info: MowToAppInfoT) -> None:
112
+ pass
113
+
114
+ def report_missing_data(self) -> None:
115
+ """Report missing data so we can refetch it."""
@@ -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."""
@@ -0,0 +1,49 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class RangeLimit:
6
+ min: float
7
+ max: float
8
+
9
+
10
+ @dataclass
11
+ class DeviceLimits:
12
+ cutter_height: RangeLimit = RangeLimit(min=30, max=100)
13
+ working_speed: RangeLimit = RangeLimit(min=0.2, max=1.2)
14
+ working_path: RangeLimit = RangeLimit(min=15, max=35)
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
+ "cutter_height": {"min": self.cutter_height.min, "max": self.cutter_height.max},
22
+ "working_speed": {"min": self.working_speed.min, "max": self.working_speed.max},
23
+ "working_path": {"min": self.working_path.min, "max": self.working_path.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
+ cutter_height=RangeLimit(min=data["cutter_height"]["min"], max=data["cutter_height"]["max"]),
33
+ working_speed=RangeLimit(min=data["working_speed"]["min"], max=data["working_speed"]["max"]),
34
+ working_path=RangeLimit(min=data["working_path"]["min"], max=data["working_path"]["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.cutter_height.min <= self.cutter_height.max,
44
+ self.working_speed.min <= self.working_speed.max,
45
+ self.working_path.min <= self.working_path.max,
46
+ self.work_area_num_max > 0,
47
+ self.display_image_type in (0, 1),
48
+ ]
49
+ )
@@ -1,18 +1,8 @@
1
- """MowingDevice class to wrap around the betterproto dataclasses."""
2
-
3
1
  from dataclasses import dataclass, field
4
2
  from typing import Optional
5
3
 
6
- import betterproto
7
4
  from mashumaro.mixins.orjson import DataClassORJSONMixin
8
5
 
9
- 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
12
- from pymammotion.data.model.location import Location
13
- from pymammotion.data.model.report_info import ReportData
14
- from pymammotion.data.mqtt.properties import ThingPropertiesMessage
15
- from pymammotion.http.model.http import ErrorInfo
16
6
  from pymammotion.proto.dev_net import DevNet
17
7
  from pymammotion.proto.luba_msg import LubaMsg
18
8
  from pymammotion.proto.luba_mul import SocMul
@@ -20,156 +10,56 @@ from pymammotion.proto.mctrl_driver import MctlDriver
20
10
  from pymammotion.proto.mctrl_nav import MctlNav
21
11
  from pymammotion.proto.mctrl_ota import MctlOta
22
12
  from pymammotion.proto.mctrl_pept import MctlPept
23
- from pymammotion.proto.mctrl_sys import (
24
- MctlSys,
25
- MowToAppInfoT,
26
- ReportInfoData,
27
- SystemRapidStateTunnelMsg,
28
- SystemUpdateBufMsg,
29
- )
30
- from pymammotion.utility.constant import WorkMode
31
- from pymammotion.utility.conversions import parse_double
32
- from pymammotion.utility.map import CoordinateConverter
13
+ from pymammotion.proto.mctrl_sys import MctlSys
33
14
 
34
15
 
35
16
  @dataclass
36
- class MowingDevice(DataClassORJSONMixin):
37
- """Wraps the betterproto dataclasses, so we can bypass the groups for keeping all data."""
38
-
39
- mower_state: MowerInfo = field(default_factory=MowerInfo)
40
- mqtt_properties: ThingPropertiesMessage | None = None
41
- map: HashList = field(default_factory=HashList)
42
- location: Location = field(default_factory=Location)
43
- mowing_state: RapidState = field(default_factory=RapidState)
44
- report_data: ReportData = field(default_factory=ReportData)
45
- err_code_list: list = field(default_factory=list)
46
- err_code_list_time: Optional[list] = field(default_factory=list)
47
- limits: DeviceLimits = field(default_factory=DeviceLimits)
48
- device: Optional[LubaMsg] = field(default_factory=LubaMsg)
49
- error_codes: dict[str, ErrorInfo] = field(default_factory=dict)
17
+ class RawMowerData:
18
+ raw: Optional[LubaMsg] = field(default_factory=LubaMsg)
50
19
 
51
20
  @classmethod
52
- def from_raw(cls, raw: dict) -> "MowingDevice":
21
+ def from_raw(cls, raw: dict) -> "RawMowerData":
53
22
  """Take in raw data to hold in the betterproto dataclass."""
54
- mowing_device = MowingDevice()
55
- mowing_device.device = LubaMsg(**raw)
56
- return mowing_device
23
+ return RawMowerData(raw=LubaMsg(**raw))
57
24
 
58
25
  def update_raw(self, raw: dict) -> None:
59
26
  """Update the raw LubaMsg data."""
60
- self.device = LubaMsg(**raw)
61
-
62
- def buffer(self, buffer_list: SystemUpdateBufMsg) -> None:
63
- """Update the device based on which buffer we are reading from."""
64
- match buffer_list.update_buf_data[0]:
65
- case 1:
66
- # 4 speed
67
- self.location.RTK.latitude = parse_double(buffer_list.update_buf_data[5], 8.0)
68
- self.location.RTK.longitude = parse_double(buffer_list.update_buf_data[6], 8.0)
69
- self.location.dock.latitude = parse_double(buffer_list.update_buf_data[7], 4.0)
70
- self.location.dock.longitude = parse_double(buffer_list.update_buf_data[8], 4.0)
71
- self.location.dock.rotation = buffer_list.update_buf_data[3] + 180
72
- case 2:
73
- self.err_code_list.clear()
74
- self.err_code_list_time.clear()
75
- self.err_code_list.extend(
76
- [
77
- buffer_list.update_buf_data[3],
78
- buffer_list.update_buf_data[5],
79
- buffer_list.update_buf_data[7],
80
- buffer_list.update_buf_data[9],
81
- buffer_list.update_buf_data[11],
82
- buffer_list.update_buf_data[13],
83
- buffer_list.update_buf_data[15],
84
- buffer_list.update_buf_data[17],
85
- buffer_list.update_buf_data[19],
86
- buffer_list.update_buf_data[21],
87
- ]
88
- )
89
- self.err_code_list_time.extend(
90
- [
91
- buffer_list.update_buf_data[4],
92
- buffer_list.update_buf_data[6],
93
- buffer_list.update_buf_data[8],
94
- buffer_list.update_buf_data[10],
95
- buffer_list.update_buf_data[12],
96
- buffer_list.update_buf_data[14],
97
- buffer_list.update_buf_data[16],
98
- buffer_list.update_buf_data[18],
99
- buffer_list.update_buf_data[20],
100
- buffer_list.update_buf_data[22],
101
- ]
102
- )
103
-
104
- def update_report_data(self, toapp_report_data: ReportInfoData) -> None:
105
- coordinate_converter = CoordinateConverter(self.location.RTK.latitude, self.location.RTK.longitude)
106
- for index, location in enumerate(toapp_report_data.locations):
107
- if index == 0 and location.real_pos_y != 0:
108
- self.location.position_type = location.pos_type
109
- self.location.orientation = location.real_toward / 10000
110
- self.location.device = coordinate_converter.enu_to_lla(
111
- parse_double(location.real_pos_y, 4.0), parse_double(location.real_pos_x, 4.0)
112
- )
113
- if location.zone_hash:
114
- self.location.work_zone = (
115
- location.zone_hash if self.report_data.dev.sys_status == WorkMode.MODE_WORKING else 0
116
- )
117
-
118
- self.report_data.update(toapp_report_data.to_dict(casing=betterproto.Casing.SNAKE))
119
-
120
- def run_state_update(self, rapid_state: SystemRapidStateTunnelMsg) -> None:
121
- coordinate_converter = CoordinateConverter(self.location.RTK.latitude, self.location.RTK.longitude)
122
- self.mowing_state = RapidState().from_raw(rapid_state.rapid_state_data)
123
- self.location.position_type = self.mowing_state.pos_type
124
- self.location.orientation = self.mowing_state.toward / 10000
125
- self.location.device = coordinate_converter.enu_to_lla(
126
- parse_double(self.mowing_state.pos_y, 4.0), parse_double(self.mowing_state.pos_x, 4.0)
127
- )
128
- if self.mowing_state.zone_hash:
129
- self.location.work_zone = (
130
- self.mowing_state.zone_hash if self.report_data.dev.sys_status == WorkMode.MODE_WORKING else 0
131
- )
132
-
133
- def mow_info(self, toapp_mow_info: MowToAppInfoT) -> None:
134
- pass
135
-
136
- def report_missing_data(self) -> None:
137
- """Report missing data so we can refetch it."""
27
+ self.raw = LubaMsg(**raw)
138
28
 
139
29
  @property
140
30
  def net(self):
141
31
  """Will return a wrapped betterproto of net."""
142
- return DevNetData(net=self.device.net)
32
+ return DevNetData(net=self.raw.net)
143
33
 
144
34
  @property
145
35
  def sys(self):
146
36
  """Will return a wrapped betterproto of sys."""
147
- return SysData(sys=self.device.sys)
37
+ return SysData(sys=self.raw.sys)
148
38
 
149
39
  @property
150
40
  def nav(self):
151
41
  """Will return a wrapped betterproto of nav."""
152
- return NavData(nav=self.device.nav)
42
+ return NavData(nav=self.raw.nav)
153
43
 
154
44
  @property
155
45
  def driver(self):
156
46
  """Will return a wrapped betterproto of driver."""
157
- return DriverData(driver=self.device.driver)
47
+ return DriverData(driver=self.raw.driver)
158
48
 
159
49
  @property
160
50
  def mul(self):
161
51
  """Will return a wrapped betterproto of mul."""
162
- return MulData(mul=self.device.mul)
52
+ return MulData(mul=self.raw.mul)
163
53
 
164
54
  @property
165
55
  def ota(self):
166
56
  """Will return a wrapped betterproto of ota."""
167
- return OtaData(ota=self.device.ota)
57
+ return OtaData(ota=self.raw.ota)
168
58
 
169
59
  @property
170
60
  def pept(self):
171
61
  """Will return a wrapped betterproto of pept."""
172
- return PeptData(pept=self.device.pept)
62
+ return PeptData(pept=self.raw.pept)
173
63
 
174
64
 
175
65
  @dataclass
@@ -0,0 +1,221 @@
1
+ import base64
2
+ import logging
3
+ import secrets
4
+ import string
5
+ from typing import Optional
6
+
7
+ from cryptography.hazmat.backends import default_backend
8
+ from cryptography.hazmat.primitives import padding, serialization
9
+ from cryptography.hazmat.primitives.asymmetric import padding as rsa_padding
10
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
11
+
12
+ _LOGGER = logging.getLogger(__name__)
13
+
14
+
15
+ class EncryptionUtils:
16
+ PRIVATE_KEY = """MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOFizbd1fC5XNKJ89u0XNvPZNR/L
17
+ 0h547iSWjOCuvvMu76ZSaS3/Tu2C1C+XmlnmBWTyY4ON+xECiNUXm/aWQ3P0g+wf60zjPbNzgL2Q
18
+ 7njXJG6wka4KkbdQxUdS0TTpL256LnV1LsG855bsbJIJiQPbfUq6HbB5xH7sXdrmFu1DAgMBAAEC
19
+ gYEAoT2TGE1ncquWjyxBZup1uMvKkp25C23OSMSfslmxZ75LWjyY3HxK1eYDsKyPkwLZFxfFE6du
20
+ VwPuKiyCuk1ToPfnb4niTGzXPyC2PbO4SFrWL8n1YZ80M0bfTGI9dMCZvpmZJ41WYUsBaf2374lt
21
+ oEiDEHJp7MeXk/970xiKP1ECQQD65rLHk840q+FZS6kZVexJucPZj/YAII6klU1E20ctioe8Pi5m
22
+ WSPqclH27/t4FqdvP7tFqaavyXg+CEQpxmxLAkEA5fddDuzcjWgF9pl9fP7/baFMYjUS9z1Vc3gx
23
+ CnvAgCnv71wjDQhvsUc6sAiidsBGFDyud06RyyLcOlQchMb36QJBAIui/Xjpn+fciQxjeXcqRNk7
24
+ U+6vml+zvu+GUHyz9Uc5RBXWHYjEr6J5gXiHU1MgeIsH0zgQFT7cR9luTFFbp0UCQFIntfogCocG
25
+ E6NOoHMoUi5jQnuPRHBJXB69YJ/DKDlhQhN8EhWU3voxXTkITKop9J9EMnvy+MjecljwNaQFxQkC
26
+ QB9lz67iDe9Gj8NxSElVZxUm9EfbL1RPqTZPx/lADR06CPB8pP3Bl5/5/5RGzc+UTZ+wX5GWKvC7
27
+ zUJaROxQB+E=""".replace(" ", "")
28
+
29
+ PUBLIC_KEY_PROD = """MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApLbeSgOvnwLTWbhaBQWNnnHMtSDAi
30
+ Gz0PEDbrtd1tLYoO0hukW5PSa6eHykch0Hc6etiqEx1xziS+vNf+iOXds70I4htaYit6yRToZlQ
31
+ Mim3DQxaZX68nIHIZogur0zGv9U8j01v5l/rHRxyDdlVx3+JkBg6Cqx4U1PXEnAJriqcyg0B8Gm
32
+ V8Lnmfng+aJLRyq5MkhstYCRv9AsmWu8NpZDJ1ffbkaS02Z9/wpubXTiFP6DG3V2mDw2VvzEcHi
33
+ cchw49oXmTi92yui+kBgSYlNygssOAyU6H071AfmRUeH3+TsV5u5rg+bCiKyHemVmcKdd3hhZB+
34
+ HjA8o3On6rg5wIDAQAB""".replace(" ", "")
35
+
36
+ PUBLIC_KEY_TEST = """MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC1nAzH31arNBmYKvTlvKgkxI1MIr4HpfLbmM
37
+ XPIhd8D/cXB0dYY1ppUq4a/ezq41YShN88e0elyZgqdnFrkhiLpnKWa7jXtVRgXi9eS18PLO8ns
38
+ eHude9URaj7relK1AZ0xovKsbLKHd01PpmngLXZfnKA06J2ru/zH+cnpXdy8QIDAQAB""".replace(" ", "")
39
+
40
+ def __init__(self) -> None:
41
+ self.AES_PASW = self.get_aes_key() # Get from previous implementation
42
+ self.IV = self.get_iv() # Get from previous implementation
43
+ self._public_key = self.load_public_key()
44
+ self._private_key = self.load_private_key()
45
+
46
+ @staticmethod
47
+ def load_private_key():
48
+ """Load the private key from base64 encoded string"""
49
+ try:
50
+ private_key_bytes = base64.b64decode(EncryptionUtils.PRIVATE_KEY)
51
+ return serialization.load_der_private_key(private_key_bytes, password=None, backend=default_backend())
52
+ except Exception as e:
53
+ raise Exception(f"Failed to load private key: {str(e)}")
54
+
55
+ @staticmethod
56
+ def load_public_key(is_production: bool = True):
57
+ """Load the public key from base64 encoded string
58
+
59
+ Args:
60
+ is_production (bool): If True, uses production key, else uses test key
61
+
62
+ """
63
+ try:
64
+ key_string = EncryptionUtils.PUBLIC_KEY_PROD if is_production else EncryptionUtils.PUBLIC_KEY_TEST
65
+ public_key_bytes = base64.b64decode(key_string)
66
+ return serialization.load_der_public_key(public_key_bytes, backend=default_backend())
67
+ except Exception as e:
68
+ raise Exception(f"Failed to load public key: {str(e)}")
69
+
70
+ @staticmethod
71
+ def encrypt(plaintext: str, key: str, iv: str) -> str:
72
+ """Encrypt text using AES/CBC/PKCS5Padding
73
+
74
+ Args:
75
+ plaintext (str): Text to encrypt
76
+ key (str): Encryption key
77
+ iv (str): Initialization vector
78
+
79
+ Returns:
80
+ str: Base64 encoded encrypted string
81
+
82
+ Raises:
83
+ Exception: If encryption fails
84
+
85
+ """
86
+ try:
87
+ # Convert strings to bytes
88
+ plaintext_bytes = plaintext.encode("utf-8")
89
+ key_bytes = key.encode("utf-8")
90
+ iv_bytes = iv.encode("utf-8")
91
+
92
+ # Create padder
93
+ padder = padding.PKCS7(128).padder()
94
+ padded_data = padder.update(plaintext_bytes) + padder.finalize()
95
+
96
+ # Create cipher
97
+ cipher = Cipher(algorithms.AES(key_bytes), modes.CBC(iv_bytes), backend=default_backend())
98
+
99
+ # Encrypt
100
+ encryptor = cipher.encryptor()
101
+ encrypted_bytes = encryptor.update(padded_data) + encryptor.finalize()
102
+
103
+ # Encode to base64
104
+ return base64.b64encode(encrypted_bytes).decode("utf-8")
105
+
106
+ except Exception as e:
107
+ raise Exception(f"Encryption failed: {str(e)}")
108
+
109
+ def encryption_by_aes(self, text: str) -> str:
110
+ """Encrypt text using AES with class-level key and IV
111
+
112
+ Args:
113
+ text (str): Text to encrypt
114
+
115
+ Returns:
116
+ str: Encrypted text or None if encryption fails
117
+
118
+ """
119
+ try:
120
+ # Perform encryption
121
+ encrypted = self.encrypt(text, self.AES_PASW, self.IV)
122
+
123
+ return encrypted
124
+
125
+ except Exception as e:
126
+ _LOGGER.error(f"Encryption failed: {str(e)}")
127
+ return None
128
+
129
+ def encrypt_by_public_key(self) -> Optional[str]:
130
+ """Encrypt data using RSA public key.
131
+
132
+ Args:
133
+
134
+ Returns:
135
+ Optional[str]: Base64 encoded encrypted data or None if encryption fails
136
+
137
+ """
138
+
139
+ data = f"{self.AES_PASW},{self.IV}"
140
+
141
+ if not self._public_key:
142
+ _LOGGER.error("Public key not initialized")
143
+ return None
144
+
145
+ try:
146
+ # Convert input string to bytes
147
+ data_bytes = data.encode("utf-8")
148
+
149
+ # Encrypt the data padding.PKCS7(128).padder()
150
+ encrypted_bytes = self._public_key.encrypt(data_bytes, rsa_padding.PKCS1v15())
151
+
152
+ # Convert to base64 string
153
+ encrypted_str = base64.b64encode(encrypted_bytes).decode("utf-8")
154
+ _LOGGER.debug("Data encrypted successfully")
155
+
156
+ return encrypted_str
157
+
158
+ except Exception as err:
159
+ _LOGGER.error("Encryption failed: %s", str(err))
160
+ return None
161
+
162
+ @staticmethod
163
+ def get_random_string(length: int) -> str:
164
+ """Generate a random string of specified length using alphanumeric characters.
165
+
166
+ Args:
167
+ length (int): The desired length of the random string
168
+
169
+ Returns:
170
+ str: A random alphanumeric string of specified length
171
+
172
+ Raises:
173
+ ValueError: If length is less than 1
174
+
175
+ """
176
+ if length < 1:
177
+ raise ValueError("Length must be positive")
178
+
179
+ charset = string.ascii_letters + string.digits
180
+ return "".join(secrets.choice(charset) for _ in range(length))
181
+
182
+ @staticmethod
183
+ def get_random_int(length: int) -> str:
184
+ """Generate a random string of specified length containing only digits.
185
+
186
+ Args:
187
+ length (int): The desired length of the random number string
188
+
189
+ Returns:
190
+ str: A string of random digits of specified length
191
+
192
+ Raises:
193
+ ValueError: If length is less than 1
194
+
195
+ """
196
+ if length < 1:
197
+ raise ValueError("Length must be positive")
198
+
199
+ return "".join(secrets.choice(string.digits) for _ in range(length))
200
+
201
+ @staticmethod
202
+ def get_aes_key() -> str:
203
+ """Generate a random AES key of 16 characters using alphanumeric characters.
204
+ Matches Java implementation behavior.
205
+
206
+ Returns:
207
+ str: A 16-character random string for AES key
208
+
209
+ """
210
+ return EncryptionUtils.get_random_string(16)
211
+
212
+ @staticmethod
213
+ def get_iv() -> str:
214
+ """Generate a random initialization vector of 16 digits.
215
+ Matches Java implementation behavior.
216
+
217
+ Returns:
218
+ str: A 16-digit random string for initialization vector
219
+
220
+ """
221
+ return EncryptionUtils.get_random_int(16)