pymammotion 0.4.56__tar.gz → 0.4.57__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.

Potentially problematic release.


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

Files changed (135) hide show
  1. {pymammotion-0.4.56 → pymammotion-0.4.57}/PKG-INFO +1 -1
  2. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/aliyun/cloud_gateway.py +13 -3
  3. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/model/device.py +2 -1
  4. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/mqtt/properties.py +56 -44
  5. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/state_manager.py +16 -3
  6. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/http/http.py +43 -8
  7. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/http/model/http.py +46 -1
  8. pymammotion-0.4.57/pymammotion/http/model/response_factory.py +39 -0
  9. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mammotion/commands/abstract_message.py +1 -4
  10. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mammotion/devices/mammotion.py +30 -9
  11. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mammotion/devices/mammotion_bluetooth.py +3 -3
  12. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mammotion/devices/mammotion_cloud.py +7 -2
  13. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/__init__.py +2 -6
  14. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/basestation.proto +8 -0
  15. pymammotion-0.4.57/pymammotion/proto/basestation_pb2.py +35 -0
  16. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/basestation_pb2.pyi +16 -2
  17. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/dev_net.proto +2 -0
  18. pymammotion-0.4.57/pymammotion/proto/dev_net_pb2.py +111 -0
  19. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/dev_net_pb2.pyi +8 -4
  20. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/luba_mul.proto +2 -2
  21. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/luba_mul_pb2.py +15 -15
  22. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/luba_mul_pb2.pyi +1 -1
  23. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/mctrl_driver.proto +23 -4
  24. pymammotion-0.4.57/pymammotion/proto/mctrl_driver_pb2.py +57 -0
  25. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/mctrl_driver_pb2.pyi +38 -10
  26. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/mctrl_nav.proto +18 -1
  27. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/mctrl_nav_pb2.py +5 -3
  28. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/mctrl_nav_pb2.pyi +34 -2
  29. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/mctrl_pept.proto +6 -1
  30. pymammotion-0.4.57/pymammotion/proto/mctrl_pept_pb2.py +33 -0
  31. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
  32. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/mctrl_sys.proto +82 -9
  33. pymammotion-0.4.57/pymammotion/proto/mctrl_sys_pb2.py +206 -0
  34. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/mctrl_sys_pb2.pyi +151 -34
  35. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/utility/device_type.py +3 -0
  36. {pymammotion-0.4.56 → pymammotion-0.4.57}/pyproject.toml +3 -3
  37. pymammotion-0.4.56/pymammotion/proto/basestation_pb2.py +0 -33
  38. pymammotion-0.4.56/pymammotion/proto/dev_net_pb2.py +0 -111
  39. pymammotion-0.4.56/pymammotion/proto/mctrl_driver_pb2.py +0 -51
  40. pymammotion-0.4.56/pymammotion/proto/mctrl_pept_pb2.py +0 -31
  41. pymammotion-0.4.56/pymammotion/proto/mctrl_sys_pb2.py +0 -190
  42. {pymammotion-0.4.56 → pymammotion-0.4.57}/LICENSE +0 -0
  43. {pymammotion-0.4.56 → pymammotion-0.4.57}/README.md +0 -0
  44. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/__init__.py +0 -0
  45. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/aliyun/__init__.py +0 -0
  46. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/aliyun/client.py +0 -0
  47. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/aliyun/model/aep_response.py +0 -0
  48. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/aliyun/model/connect_response.py +0 -0
  49. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/aliyun/model/dev_by_account_response.py +0 -0
  50. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/aliyun/model/login_by_oauth_response.py +0 -0
  51. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/aliyun/model/regions_response.py +0 -0
  52. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/aliyun/model/session_by_authcode_response.py +0 -0
  53. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/aliyun/regions.py +0 -0
  54. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/aliyun/tea/core.py +0 -0
  55. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/aliyun/tmp_constant.py +0 -0
  56. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/bluetooth/__init__.py +0 -0
  57. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/bluetooth/ble.py +0 -0
  58. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/bluetooth/ble_message.py +0 -0
  59. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/bluetooth/const.py +0 -0
  60. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/bluetooth/data/__init__.py +0 -0
  61. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/bluetooth/data/convert.py +0 -0
  62. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/bluetooth/data/framectrldata.py +0 -0
  63. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/bluetooth/data/notifydata.py +0 -0
  64. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/bluetooth/model/__init__.py +0 -0
  65. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/bluetooth/model/atomic_integer.py +0 -0
  66. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/const.py +0 -0
  67. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/__init__.py +0 -0
  68. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/model/__init__.py +0 -0
  69. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/model/account.py +0 -0
  70. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/model/device_config.py +0 -0
  71. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/model/device_info.py +0 -0
  72. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/model/device_limits.py +0 -0
  73. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/model/enums.py +0 -0
  74. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/model/excute_boarder_params.py +0 -0
  75. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/model/execute_boarder.py +0 -0
  76. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/model/generate_route_information.py +0 -0
  77. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/model/hash_list.py +0 -0
  78. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/model/location.py +0 -0
  79. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/model/mowing_modes.py +0 -0
  80. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/model/rapid_state.py +0 -0
  81. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/model/raw_data.py +0 -0
  82. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/model/region_data.py +0 -0
  83. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/model/report_info.py +0 -0
  84. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/model/work.py +0 -0
  85. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/mqtt/__init__.py +0 -0
  86. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/mqtt/event.py +0 -0
  87. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/data/mqtt/status.py +0 -0
  88. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/event/__init__.py +0 -0
  89. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/event/event.py +0 -0
  90. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/http/__init__.py +0 -0
  91. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/http/_init_.py +0 -0
  92. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/http/encryption.py +0 -0
  93. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/http/model/__init__.py +0 -0
  94. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/http/model/camera_stream.py +0 -0
  95. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mammotion/__init__.py +0 -0
  96. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mammotion/commands/__init__.py +0 -0
  97. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mammotion/commands/mammotion_command.py +0 -0
  98. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mammotion/commands/messages/__init__.py +0 -0
  99. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mammotion/commands/messages/driver.py +0 -0
  100. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mammotion/commands/messages/media.py +0 -0
  101. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mammotion/commands/messages/navigation.py +0 -0
  102. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mammotion/commands/messages/network.py +0 -0
  103. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mammotion/commands/messages/ota.py +0 -0
  104. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mammotion/commands/messages/system.py +0 -0
  105. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mammotion/commands/messages/video.py +0 -0
  106. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mammotion/control/__init__.py +0 -0
  107. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mammotion/control/joystick.py +0 -0
  108. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mammotion/devices/__init__.py +0 -0
  109. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mammotion/devices/base.py +0 -0
  110. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mqtt/__init__.py +0 -0
  111. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mqtt/linkkit/__init__.py +0 -0
  112. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mqtt/linkkit/h2client.py +0 -0
  113. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mqtt/linkkit/linkkit.py +0 -0
  114. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mqtt/mammotion_future.py +0 -0
  115. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/mqtt/mammotion_mqtt.py +0 -0
  116. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/common.proto +0 -0
  117. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/common_pb2.py +0 -0
  118. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/common_pb2.pyi +0 -0
  119. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/luba_msg.proto +0 -0
  120. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/luba_msg_pb2.py +0 -0
  121. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/luba_msg_pb2.pyi +0 -0
  122. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/mctrl_ota.proto +0 -0
  123. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/mctrl_ota_pb2.py +0 -0
  124. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/proto/mctrl_ota_pb2.pyi +0 -0
  125. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/py.typed +0 -0
  126. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/utility/constant/__init__.py +0 -0
  127. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/utility/constant/device_constant.py +0 -0
  128. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/utility/conversions.py +0 -0
  129. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/utility/datatype_converter.py +0 -0
  130. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/utility/device_config.py +0 -0
  131. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/utility/map.py +0 -0
  132. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/utility/movement.py +0 -0
  133. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/utility/mur_mur_hash.py +0 -0
  134. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/utility/periodic.py +0 -0
  135. {pymammotion-0.4.56 → pymammotion-0.4.57}/pymammotion/utility/rocker_util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pymammotion
3
- Version: 0.4.56
3
+ Version: 0.4.57
4
4
  Summary:
5
5
  License: GPL-3.0
6
6
  Author: Michael Arthur
@@ -15,8 +15,8 @@ from aiohttp import ClientSession, ConnectionTimeoutError
15
15
  from alibabacloud_iot_api_gateway.models import CommonParams, Config, IoTApiRequest
16
16
  from alibabacloud_tea_util.client import Client as UtilClient
17
17
  from alibabacloud_tea_util.models import RuntimeOptions
18
- from Tea.exceptions import UnretryableException
19
18
  from orjson.orjson import JSONDecodeError
19
+ from Tea.exceptions import UnretryableException
20
20
 
21
21
  from pymammotion.aliyun.client import Client
22
22
  from pymammotion.aliyun.model.aep_response import AepResponse
@@ -65,6 +65,14 @@ class DeviceOfflineException(Exception):
65
65
  self.iot_id = args[1]
66
66
 
67
67
 
68
+ class FailedRequestException(Exception):
69
+ """Raise exception when request response is bad."""
70
+
71
+ def __init__(self, *args: object) -> None:
72
+ super().__init__(args)
73
+ self.iot_id = args[0]
74
+
75
+
68
76
  class NoConnectionException(UnretryableException):
69
77
  """Raise exception when device is unreachable."""
70
78
 
@@ -145,7 +153,7 @@ class CloudIOTGateway:
145
153
  return json.loads(response_body_str) if response_body_str is not None else {}
146
154
  except JSONDecodeError:
147
155
  logger.error("Couldn't decode message %s", response_body_str)
148
- return {}
156
+ return {'code': 22000}
149
157
 
150
158
  def sign(self, data):
151
159
  """Generate signature for the given data."""
@@ -739,7 +747,6 @@ class CloudIOTGateway:
739
747
  logger.debug(response.body)
740
748
  logger.debug(iot_id)
741
749
 
742
-
743
750
  response_body_str = response.body.decode("utf-8")
744
751
  response_body_dict = self.parse_json_response(response_body_str)
745
752
 
@@ -749,6 +756,9 @@ class CloudIOTGateway:
749
756
  str(response_body_dict.get("code")),
750
757
  str(response_body_dict.get("message")),
751
758
  )
759
+ if response_body_dict.get("code") == 22000:
760
+ logger.error(response)
761
+ raise FailedRequestException(iot_id)
752
762
  if response_body_dict.get("code") == 20056:
753
763
  logger.debug("Gateway timeout.")
754
764
  raise GatewayTimeoutException(response_body_dict.get("code"), iot_id)
@@ -12,7 +12,7 @@ from pymammotion.data.model.report_info import ReportData
12
12
  from pymammotion.data.model.work import CurrentTaskSettings
13
13
  from pymammotion.data.mqtt.properties import ThingPropertiesMessage
14
14
  from pymammotion.data.mqtt.status import ThingStatusMessage
15
- from pymammotion.http.model.http import ErrorInfo
15
+ from pymammotion.http.model.http import CheckDeviceVersion, ErrorInfo
16
16
  from pymammotion.proto import DeviceFwInfo, MowToAppInfoT, ReportInfoData, SystemRapidStateTunnelMsg, SystemUpdateBufMsg
17
17
  from pymammotion.utility.constant import WorkMode
18
18
  from pymammotion.utility.conversions import parse_double
@@ -26,6 +26,7 @@ class MowingDevice(DataClassORJSONMixin):
26
26
  name: str = ""
27
27
  online: bool = True
28
28
  enabled: bool = True
29
+ update_check: CheckDeviceVersion = field(default_factory=CheckDeviceVersion)
29
30
  mower_state: MowerInfo = field(default_factory=MowerInfo)
30
31
  mqtt_properties: ThingPropertiesMessage | None = None
31
32
  status_properties: ThingStatusMessage | None = None
@@ -15,85 +15,95 @@ class Item(DataClassDictMixin, Generic[DataT]):
15
15
 
16
16
  @dataclass
17
17
  class BatteryPercentageItems(DataClassORJSONMixin):
18
- batteryPercentage: Item[int]
18
+ batteryPercentage: int
19
19
 
20
20
 
21
21
  @dataclass
22
22
  class BMSHardwareVersionItems(DataClassORJSONMixin):
23
- bmsHardwareVersion: Item[str]
23
+ bmsHardwareVersion: str
24
24
 
25
25
 
26
26
  @dataclass
27
27
  class CoordinateItems(DataClassORJSONMixin):
28
- coordinate: Item[str] # '{"lon":0.303903,"lat":1.051868}'
28
+ coordinate: str # '{"lon":0.303903,"lat":1.051868}'
29
29
 
30
30
 
31
31
  @dataclass
32
32
  class DeviceStateItems(DataClassORJSONMixin):
33
- deviceState: Item[int]
33
+ deviceState: int
34
34
 
35
35
 
36
36
  @dataclass
37
37
  class DeviceVersionItems(DataClassORJSONMixin):
38
- deviceVersion: Item[str]
38
+ deviceVersion: str
39
39
 
40
40
 
41
41
  @dataclass
42
42
  class DeviceVersionInfoItems(DataClassORJSONMixin):
43
- deviceVersionInfo: Item[str]
43
+ deviceVersionInfo: str
44
44
 
45
45
 
46
46
  @dataclass
47
47
  class ESP32VersionItems(DataClassORJSONMixin):
48
- esp32Version: Item[str]
48
+ esp32Version: str
49
49
 
50
50
 
51
51
  @dataclass
52
52
  class LeftMotorBootVersionItems(DataClassORJSONMixin):
53
- leftMotorBootVersion: Item[str]
53
+ leftMotorBootVersion: str
54
54
 
55
55
 
56
56
  @dataclass
57
57
  class LeftMotorVersionItems(DataClassORJSONMixin):
58
- leftMotorVersion: Item[str]
58
+ leftMotorVersion: str
59
59
 
60
60
 
61
61
  @dataclass
62
62
  class MCBootVersionItems(DataClassORJSONMixin):
63
- mcBootVersion: Item[str]
63
+ mcBootVersion: str
64
64
 
65
65
 
66
66
  @dataclass
67
67
  class NetworkInfoItems(DataClassORJSONMixin):
68
- networkInfo: Item[str]
68
+ networkInfo: str
69
69
 
70
70
 
71
71
  @dataclass
72
72
  class RightMotorBootVersionItems(DataClassORJSONMixin):
73
- rightMotorBootVersion: Item[str]
73
+ rightMotorBootVersion: str
74
74
 
75
75
 
76
76
  @dataclass
77
77
  class RightMotorVersionItems(DataClassORJSONMixin):
78
- rightMotorVersion: Item[str]
78
+ rightMotorVersion: str
79
79
 
80
80
 
81
81
  @dataclass
82
82
  class RTKVersionItems(DataClassORJSONMixin):
83
- rtkVersion: Item[str]
83
+ rtkVersion: str
84
84
 
85
85
 
86
86
  @dataclass
87
87
  class StationRTKVersionItems(DataClassORJSONMixin):
88
- stationRtkVersion: Item[str]
88
+ stationRtkVersion: str
89
89
 
90
90
 
91
91
  @dataclass
92
92
  class STM32H7VersionItems(DataClassORJSONMixin):
93
- stm32H7Version: Item[str]
93
+ stm32H7Version: str
94
94
 
95
95
 
96
- Items = Union[
96
+ @dataclass
97
+ class OTAProgressItems(DataClassORJSONMixin):
98
+ result: int
99
+ otaId: str
100
+ progress: int
101
+ message: str
102
+ version: str
103
+ properties: str
104
+
105
+
106
+ ItemTypes = Union[
97
107
  BatteryPercentageItems,
98
108
  BMSHardwareVersionItems,
99
109
  CoordinateItems,
@@ -110,43 +120,45 @@ Items = Union[
110
120
  RTKVersionItems,
111
121
  StationRTKVersionItems,
112
122
  STM32H7VersionItems,
123
+ OTAProgressItems,
113
124
  ]
114
125
 
115
126
 
116
127
  @dataclass
117
128
  class Item:
118
129
  time: int
119
- value: int | float | str | dict[str, Any] # Depending on the type of value
130
+ value: int | float | str | dict[str, Any] | ItemTypes # Depending on the type of value
120
131
 
121
132
 
122
133
  @dataclass
123
134
  class Items:
124
- iotState: Item
125
- extMod: Item
126
- deviceVersionInfo: Item
127
- leftMotorBootVersion: Item
128
- knifeHeight: Item
129
- rtMrMod: Item
130
- iotMsgHz: Item
131
- iotMsgTotal: Item
132
- loraRawConfig: Item
133
- loraGeneralConfig: Item
134
- leftMotorVersion: Item
135
- intMod: Item
136
- coordinate: Item
137
- bmsVersion: Item
138
- rightMotorVersion: Item
139
- stm32H7Version: Item
140
- rightMotorBootVersion: Item
141
- deviceVersion: Item
142
- rtkVersion: Item
143
- ltMrMod: Item
144
- networkInfo: Item
145
- bmsHardwareVersion: Item
146
- batteryPercentage: Item
147
- deviceState: Item
148
- deviceOtherInfo: Item
149
- mcBootVersion: Item
135
+ iotState: Item | None = None
136
+ extMod: Item | None = None
137
+ deviceVersionInfo: Item | None = None
138
+ leftMotorBootVersion: Item | None = None
139
+ knifeHeight: Item | None = None
140
+ rtMrMod: Item | None = None
141
+ iotMsgHz: Item | None = None
142
+ iotMsgTotal: Item | None = None
143
+ loraRawConfig: Item | None = None
144
+ loraGeneralConfig: Item | None = None
145
+ leftMotorVersion: Item | None = None
146
+ intMod: Item | None = None
147
+ coordinate: Item | None = None
148
+ bmsVersion: Item | None = None
149
+ rightMotorVersion: Item | None = None
150
+ stm32H7Version: Item | None = None
151
+ rightMotorBootVersion: Item | None = None
152
+ deviceVersion: Item | None = None
153
+ rtkVersion: Item | None = None
154
+ ltMrMod: Item | None = None
155
+ networkInfo: Item | None = None
156
+ bmsHardwareVersion: Item | None = None
157
+ batteryPercentage: Item | None = None
158
+ deviceState: Item | None = None
159
+ deviceOtherInfo: Item | None = None
160
+ mcBootVersion: Item | None = None
161
+ otaProgress: Item | None = None
150
162
 
151
163
 
152
164
  @dataclass
@@ -49,8 +49,7 @@ class StateManager:
49
49
  ) = None
50
50
  self.cloud_get_plan_callback: Callable[[NavPlanJobSet], Awaitable[None]] | None = None
51
51
  self.cloud_on_notification_callback: Callable[[tuple[str, Any | None]], Awaitable[None]] | None = None
52
-
53
- self.cloud_queue_command_callback: Callable[[str, dict[str, Any]], Awaitable[bytes]] | None = None
52
+ self.cloud_queue_command_callback: Callable[[str, dict[str, Any]], Awaitable[None]] | None = None
54
53
 
55
54
  self.ble_gethash_ack_callback: Callable[[NavGetHashListAck], Awaitable[None]] | None = None
56
55
  self.ble_get_commondata_ack_callback: Callable[[NavGetCommDataAck | SvgMessageAckT], Awaitable[None]] | None = (
@@ -58,8 +57,10 @@ class StateManager:
58
57
  )
59
58
  self.ble_get_plan_callback: Callable[[NavPlanJobSet], Awaitable[None]] | None = None
60
59
  self.ble_on_notification_callback: Callable[[tuple[str, Any | None]], Awaitable[None]] | None = None
60
+ self.ble_queue_command_callback: Callable[[str, dict[str, Any]], Awaitable[None]] | None = None
61
61
 
62
- self.ble_queue_command_callback: Callable[[str, dict[str, Any]], Awaitable[bytes]] | None = None
62
+ self.properties_callback: Callable[[ThingPropertiesMessage], Awaitable[None]] | None = None
63
+ self.status_callback: Callable[[ThingStatusMessage], Awaitable[None]] | None = None
63
64
 
64
65
  def get_device(self) -> MowingDevice:
65
66
  """Get device."""
@@ -72,6 +73,7 @@ class StateManager:
72
73
  def properties(self, thing_properties: ThingPropertiesMessage) -> None:
73
74
  # TODO update device based off thing properties
74
75
  self._device.mqtt_properties = thing_properties
76
+ self.on_properties_callback(thing_properties)
75
77
 
76
78
  def status(self, thing_status: ThingStatusMessage) -> None:
77
79
  if not self._device.online:
@@ -79,6 +81,7 @@ class StateManager:
79
81
  self._device.status_properties = thing_status
80
82
  if self._device.mower_state.product_key == "":
81
83
  self._device.mower_state.product_key = thing_status.params.productKey
84
+ self.on_status_callback(thing_status)
82
85
 
83
86
  @property
84
87
  def online(self) -> bool:
@@ -100,6 +103,16 @@ class StateManager:
100
103
  elif self.ble_on_notification_callback:
101
104
  await self.ble_on_notification_callback(res)
102
105
 
106
+ async def on_properties_callback(self, thing_properties: ThingPropertiesMessage) -> None:
107
+ """Check if we have a callback for properties."""
108
+ if self.properties_callback:
109
+ await self.properties_callback(thing_properties)
110
+
111
+ async def on_status_callback(self, thing_status: ThingStatusMessage) -> None:
112
+ """Check if we have a callback for status."""
113
+ if self.status_callback:
114
+ await self.status_callback(thing_status)
115
+
103
116
  async def get_commondata_ack_callback(self, comm_data: NavGetCommDataAck | SvgMessageAckT) -> None:
104
117
  if self.cloud_get_commondata_ack_callback:
105
118
  await self.cloud_get_commondata_ack_callback(comm_data)
@@ -6,7 +6,8 @@ from aiohttp import ClientSession
6
6
  from pymammotion.const import MAMMOTION_API_DOMAIN, MAMMOTION_CLIENT_ID, MAMMOTION_CLIENT_SECRET, MAMMOTION_DOMAIN
7
7
  from pymammotion.http.encryption import EncryptionUtils
8
8
  from pymammotion.http.model.camera_stream import StreamSubscriptionResponse, VideoResourceResponse
9
- from pymammotion.http.model.http import ErrorInfo, LoginResponseData, Response
9
+ from pymammotion.http.model.http import CheckDeviceVersion, ErrorInfo, LoginResponseData, Response
10
+ from pymammotion.http.model.response_factory import response_factory
10
11
 
11
12
 
12
13
  class MammotionHTTP:
@@ -17,7 +18,7 @@ class MammotionHTTP:
17
18
  self._password = None
18
19
  self.response: Response | None = None
19
20
  self.login_info: LoginResponseData | None = None
20
- self._headers = {"User-Agent": "okhttp/3.14.9", "App-Version": "google Pixel 2 XL taimen-Android 11,1.11.332"}
21
+ self._headers = {"User-Agent": "okhttp/4.9.3", "App-Version": "google Pixel 2 XL taimen-Android 11,1.11.332"}
21
22
  self.encryption_utils = EncryptionUtils()
22
23
 
23
24
  @staticmethod
@@ -100,7 +101,7 @@ class MammotionHTTP:
100
101
  headers={
101
102
  "Authorization": f"Bearer {self.login_info.access_token}",
102
103
  "Content-Type": "application/json",
103
- "User-Agent": "okhttp/3.14.9",
104
+ "User-Agent": "okhttp/4.9.3",
104
105
  },
105
106
  ) as resp:
106
107
  data = await resp.json()
@@ -133,7 +134,7 @@ class MammotionHTTP:
133
134
  headers={
134
135
  "Authorization": f"Bearer {self.login_info.access_token}",
135
136
  "Content-Type": "application/json",
136
- "User-Agent": "okhttp/3.14.9",
137
+ "User-Agent": "okhttp/4.9.3",
137
138
  },
138
139
  ) as resp:
139
140
  data = await resp.json()
@@ -153,7 +154,7 @@ class MammotionHTTP:
153
154
  headers={
154
155
  "Authorization": f"Bearer {self.login_info.access_token}",
155
156
  "Content-Type": "application/json",
156
- "User-Agent": "okhttp/3.14.9",
157
+ "User-Agent": "okhttp/4.9.3",
157
158
  },
158
159
  ) as resp:
159
160
  data = await resp.json()
@@ -165,6 +166,40 @@ class MammotionHTTP:
165
166
  response.data = VideoResourceResponse.from_dict(data.get("data", {}))
166
167
  return response
167
168
 
169
+ async def get_device_ota_firmware(self, iot_ids: list[str]) -> Response[list[CheckDeviceVersion]]:
170
+ """Device firmware upgrade check."""
171
+ async with ClientSession(MAMMOTION_API_DOMAIN) as session:
172
+ async with session.post(
173
+ "/device-server/v1/devices/version/check",
174
+ json={"deviceIds": iot_ids},
175
+ headers={
176
+ "Authorization": f"Bearer {self.login_info.access_token}",
177
+ "Content-Type": "application/json",
178
+ "User-Agent": "okhttp/4.9.3",
179
+ },
180
+ ) as resp:
181
+ data = await resp.json()
182
+ # TODO catch errors from mismatch like token expire etc
183
+ # Assuming the data format matches the expected structure
184
+ return response_factory(Response[list[CheckDeviceVersion]], data)
185
+
186
+ async def start_ota_upgrade(self, iot_id: str, version: str) -> Response[str]:
187
+ """Device firmware upgrade."""
188
+ async with ClientSession(MAMMOTION_API_DOMAIN) as session:
189
+ async with session.post(
190
+ "/device-server/v1/ota/device/upgrade",
191
+ json={"deviceId": iot_id, "version": version},
192
+ headers={
193
+ "Authorization": f"Bearer {self.login_info.access_token}",
194
+ "Content-Type": "application/json",
195
+ "User-Agent": "okhttp/4.9.3",
196
+ },
197
+ ) as resp:
198
+ data = await resp.json()
199
+ # TODO catch errors from mismatch like token expire etc
200
+ # Assuming the data format matches the expected structure
201
+ return response_factory(Response[str], data)
202
+
168
203
  async def refresh_login(self, account: str, password: str | None = None) -> Response[LoginResponseData]:
169
204
  if self._password is None and password is not None:
170
205
  self._password = password
@@ -179,7 +214,7 @@ class MammotionHTTP:
179
214
  async with session.post(
180
215
  "/oauth/token",
181
216
  headers={
182
- "User-Agent": "okhttp/3.14.9",
217
+ "User-Agent": "okhttp/4.9.3",
183
218
  "App-Version": "google Pixel 2 XL taimen-Android 11,1.11.332",
184
219
  "Encrypt-Key": self.encryption_utils.encrypt_by_public_key(),
185
220
  "Decrypt-Type": "3",
@@ -197,11 +232,11 @@ class MammotionHTTP:
197
232
  print(resp.json())
198
233
  return Response.from_dict({"code": resp.status, "msg": "Login failed"})
199
234
  data = await resp.json()
200
- login_response = Response[LoginResponseData].from_dict(data)
235
+ login_response = response_factory(Response[LoginResponseData], data)
201
236
  if login_response.data is None:
202
237
  print(login_response)
203
238
  return Response.from_dict({"code": resp.status, "msg": "Login failed"})
204
- self.login_info = LoginResponseData.from_dict(login_response.data)
239
+ self.login_info = login_response.data
205
240
  self._headers["Authorization"] = (
206
241
  f"Bearer {self.login_info.access_token}" if login_response.data else None
207
242
  )
@@ -1,9 +1,10 @@
1
1
  from dataclasses import dataclass
2
- from typing import Generic, Literal, TypeVar
2
+ from typing import Annotated, Generic, Literal, TypeVar
3
3
 
4
4
  from mashumaro import DataClassDictMixin
5
5
  from mashumaro.config import BaseConfig
6
6
  from mashumaro.mixins.orjson import DataClassORJSONMixin
7
+ from mashumaro.types import Alias
7
8
 
8
9
  DataT = TypeVar("DataT")
9
10
 
@@ -103,3 +104,47 @@ class LoginResponseData(DataClassORJSONMixin):
103
104
 
104
105
  class Config(BaseConfig):
105
106
  omit_none = True
107
+
108
+
109
+ @dataclass
110
+ class FirmwareVersions(DataClassORJSONMixin):
111
+ firmware_version: Annotated[str, Alias("firmwareVersion")] = ""
112
+ firmware_code: Annotated[str, Alias("firmwareCode")] = ""
113
+ firmware_latest_version: Annotated[str, Alias("firmwareLatestVersion")] = ""
114
+ firmware_type: Annotated[str, Alias("firmwareType")] = ""
115
+
116
+
117
+ @dataclass
118
+ class ProductVersionInfo(DataClassORJSONMixin):
119
+ release_note: Annotated[str, Alias("releaseNote")] = ""
120
+ release_version: Annotated[str, Alias("releaseVersion")] = ""
121
+ data_location: str | None = None
122
+
123
+
124
+ @dataclass
125
+ class CheckDeviceVersion(DataClassORJSONMixin):
126
+ cause_code: Annotated[int, Alias("causeCode")] = 0
127
+ product_version_info_vo: Annotated[ProductVersionInfo | None, Alias("productVersionInfoVo")] = None
128
+ progress: int | None = 0
129
+ upgradeable: bool = False
130
+ device_id: Annotated[str, Alias("deviceId")] = ""
131
+ device_name: Annotated[str | None, Alias("deviceName")] = ""
132
+ current_version: Annotated[str, Alias("currentVersion")] = ""
133
+ isupgrading: bool | None = False
134
+ cause_msg: Annotated[str, Alias("causeMsg")] = ""
135
+
136
+ def __eq__(self, other):
137
+ if not isinstance(other, CheckDeviceVersion):
138
+ return NotImplemented
139
+
140
+ if self.device_id != other.device_id or self.current_version != other.current_version:
141
+ return False
142
+
143
+ if self.product_version_info_vo and other.product_version_info_vo:
144
+ if self.product_version_info_vo.release_version != other.product_version_info_vo.release_version:
145
+ return False
146
+ return True
147
+ elif self.product_version_info_vo is None and other.product_version_info_vo is None:
148
+ return False
149
+ else:
150
+ return True
@@ -0,0 +1,39 @@
1
+ from typing import TypeVar, Union, get_args, get_origin
2
+
3
+ from pymammotion.http.model.http import Response
4
+
5
+ T = TypeVar("T")
6
+
7
+
8
+ def deserialize_data(value, target_type):
9
+ if value is None:
10
+ return None
11
+
12
+ origin = get_origin(target_type)
13
+ args = get_args(target_type)
14
+
15
+ if origin is list and args:
16
+ item_type = args[0]
17
+ return [deserialize_data(v, item_type) for v in value]
18
+
19
+ if origin is Union:
20
+ # Support Optional[T] = Union[T, None]
21
+ non_none_types = [t for t in args if t is not type(None)]
22
+ if len(non_none_types) == 1:
23
+ return deserialize_data(value, non_none_types[0])
24
+
25
+ if hasattr(target_type, "from_dict"):
26
+ return target_type.from_dict(value)
27
+
28
+ return value # fallback: unknown type, leave as-is
29
+
30
+
31
+ def response_factory(response_cls: type[Response[T]], raw_dict: dict) -> Response[T]:
32
+ # Extract the type of the generic `data` field
33
+ data_type = get_args(response_cls)[0] if get_args(response_cls) else None
34
+
35
+ if data_type:
36
+ data_value = deserialize_data(raw_dict.get("data"), data_type)
37
+ return Response(code=raw_dict["code"], msg=raw_dict["msg"], data=data_value)
38
+ else:
39
+ return response_cls.from_dict(raw_dict)
@@ -18,9 +18,6 @@ class AbstractMessage:
18
18
 
19
19
  def get_msg_device(self, msg_type: MsgCmdType, msg_device: MsgDevice) -> MsgDevice:
20
20
  """Changes the rcver name if it's not a luba1."""
21
- if (
22
- not DeviceType.is_luba1(self.get_device_name(), self.get_device_product_key())
23
- and msg_type == MsgCmdType.NAV
24
- ):
21
+ if DeviceType.is_luba_pro(self.get_device_name(), self.get_device_product_key()) and msg_type == MsgCmdType.NAV:
25
22
  return MsgDevice.DEV_NAVIGATION
26
23
  return msg_device
@@ -32,7 +32,6 @@ class MammotionMixedDeviceManager:
32
32
  name: str,
33
33
  iot_id: str,
34
34
  cloud_client: CloudIOTGateway,
35
- mammotion_http: MammotionHTTP,
36
35
  cloud_device: Device,
37
36
  ble_device: BLEDevice | None = None,
38
37
  mqtt: MammotionCloud | None = None,
@@ -48,16 +47,22 @@ class MammotionMixedDeviceManager:
48
47
  self._device: Device = cloud_device
49
48
  self.add_ble(ble_device)
50
49
  self.add_cloud(mqtt)
51
- self.mammotion_http = mammotion_http
50
+ self.mammotion_http = cloud_client.mammotion_http
52
51
  self.preference = preference
53
52
  self._state_manager.preference = preference
54
53
 
55
54
  @property
56
- def mower_state(self):
55
+ def state_manager(self) -> StateManager:
56
+ """Return the state manager."""
57
+ return self._state_manager
58
+
59
+ @property
60
+ def state(self):
61
+ """Return the state of the device."""
57
62
  return self._state_manager.get_device()
58
63
 
59
- @mower_state.setter
60
- def mower_state(self, value: MowingDevice) -> None:
64
+ @state.setter
65
+ def state(self, value: MowingDevice) -> None:
61
66
  self._state_manager.set_device(value)
62
67
 
63
68
  def ble(self) -> MammotionBaseBLEDevice | None:
@@ -219,13 +224,12 @@ class Mammotion:
219
224
  name=device.deviceName,
220
225
  iot_id=device.iotId,
221
226
  cloud_client=mqtt_client.cloud_client,
222
- mammotion_http=mqtt_client.cloud_client.mammotion_http,
223
227
  cloud_device=device,
224
228
  mqtt=mqtt_client,
225
229
  preference=ConnectionPreference.WIFI,
226
230
  )
227
- mixed_device.mower_state.mower_state.product_key = device.productKey
228
- mixed_device.mower_state.mower_state.model = (
231
+ mixed_device.state.mower_state.product_key = device.productKey
232
+ mixed_device.state.mower_state.model = (
229
233
  device.productName if device.productModel is None else device.productModel
230
234
  )
231
235
  self.device_manager.add_device(mixed_device)
@@ -266,6 +270,18 @@ class Mammotion:
266
270
  def get_device_by_name(self, name: str) -> MammotionMixedDeviceManager:
267
271
  return self.device_manager.get_device(name)
268
272
 
273
+ def get_or_create_device_by_name(self, device: Device, mqtt_client: MammotionCloud) -> MammotionMixedDeviceManager:
274
+ if mow_device := self.device_manager.get_device(device.deviceName):
275
+ return mow_device
276
+ return MammotionMixedDeviceManager(
277
+ name=device.deviceName,
278
+ iot_id=device.iotId,
279
+ cloud_client=mqtt_client.cloud_client,
280
+ mqtt=mqtt_client,
281
+ cloud_device=device,
282
+ ble_device=None,
283
+ )
284
+
269
285
  async def send_command(self, name: str, key: str):
270
286
  """Send a command to the device."""
271
287
  device = self.get_device_by_name(name)
@@ -275,6 +291,7 @@ class Mammotion:
275
291
  if device.preference is ConnectionPreference.WIFI:
276
292
  return await device.cloud().command(key)
277
293
  # TODO work with both with EITHER
294
+ return None
278
295
 
279
296
  async def send_command_with_args(self, name: str, key: str, **kwargs: Any):
280
297
  """Send a command with args to the device."""
@@ -285,6 +302,7 @@ class Mammotion:
285
302
  if device.preference is ConnectionPreference.WIFI:
286
303
  return await device.cloud().command(key, **kwargs)
287
304
  # TODO work with both with EITHER
305
+ return None
288
306
 
289
307
  async def start_sync(self, name: str, retry: int):
290
308
  device = self.get_device_by_name(name)
@@ -294,6 +312,7 @@ class Mammotion:
294
312
  if device.preference is ConnectionPreference.WIFI:
295
313
  return await device.cloud().start_sync(retry)
296
314
  # TODO work with both with EITHER
315
+ return None
297
316
 
298
317
  async def start_map_sync(self, name: str):
299
318
  device = self.get_device_by_name(name)
@@ -303,6 +322,7 @@ class Mammotion:
303
322
  if device.preference is ConnectionPreference.WIFI:
304
323
  return await device.cloud().start_map_sync()
305
324
  # TODO work with both with EITHER
325
+ return None
306
326
 
307
327
  async def get_stream_subscription(self, name: str, iot_id: str) -> Response[StreamSubscriptionResponse] | Any:
308
328
  device = self.get_device_by_name(name)
@@ -329,4 +349,5 @@ class Mammotion:
329
349
  def mower(self, name: str) -> MowingDevice | None:
330
350
  device = self.get_device_by_name(name)
331
351
  if device:
332
- return device.mower_state
352
+ return device.state
353
+ return None
@@ -103,7 +103,7 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
103
103
  def set_notification_callback(self, func: Callable[[tuple[str, Any | None]], Awaitable[None]]) -> None:
104
104
  self._state_manager.ble_on_notification_callback = func
105
105
 
106
- def set_queue_callback(self, func: Callable[[str, dict[str, Any]], Awaitable[bytes]]) -> None:
106
+ def set_queue_callback(self, func: Callable[[str, dict[str, Any]], Awaitable[None]]) -> None:
107
107
  self._state_manager.ble_queue_command_callback = func
108
108
 
109
109
  def update_device(self, device: BLEDevice) -> None:
@@ -136,7 +136,7 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
136
136
  if self._client is not None and self._client.is_connected:
137
137
  await self._client.disconnect()
138
138
 
139
- async def queue_command(self, key: str, **kwargs: Any) -> bytes | None:
139
+ async def queue_command(self, key: str, **kwargs: Any) -> None:
140
140
  # Create a future to hold the result
141
141
  _LOGGER.debug("Queueing command: %s", key)
142
142
  future = asyncio.Future()
@@ -144,7 +144,7 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
144
144
  command_bytes = getattr(self._commands, key)(**kwargs)
145
145
  await self.command_queue.put((key, command_bytes, future))
146
146
  # Wait for the future to be resolved
147
- return await future
147
+ await future
148
148
  # return await self._send_command_with_args(key, **kwargs)
149
149
 
150
150
  async def process_queue(self) -> None: