pymammotion 0.4.55__py3-none-any.whl → 0.4.57__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (40) hide show
  1. pymammotion/aliyun/client.py +2 -1
  2. pymammotion/aliyun/cloud_gateway.py +13 -3
  3. pymammotion/data/model/device.py +2 -1
  4. pymammotion/data/mqtt/properties.py +56 -44
  5. pymammotion/data/state_manager.py +19 -6
  6. pymammotion/http/http.py +54 -11
  7. pymammotion/http/model/http.py +46 -1
  8. pymammotion/http/model/response_factory.py +39 -0
  9. pymammotion/mammotion/commands/abstract_message.py +1 -4
  10. pymammotion/mammotion/commands/messages/system.py +1 -0
  11. pymammotion/mammotion/devices/mammotion.py +30 -9
  12. pymammotion/mammotion/devices/mammotion_bluetooth.py +5 -4
  13. pymammotion/mammotion/devices/mammotion_cloud.py +9 -3
  14. pymammotion/proto/__init__.py +2 -6
  15. pymammotion/proto/basestation.proto +8 -0
  16. pymammotion/proto/basestation_pb2.py +11 -9
  17. pymammotion/proto/basestation_pb2.pyi +16 -2
  18. pymammotion/proto/dev_net.proto +2 -0
  19. pymammotion/proto/dev_net_pb2.py +60 -60
  20. pymammotion/proto/dev_net_pb2.pyi +8 -4
  21. pymammotion/proto/luba_mul.proto +2 -2
  22. pymammotion/proto/luba_mul_pb2.py +15 -15
  23. pymammotion/proto/luba_mul_pb2.pyi +1 -1
  24. pymammotion/proto/mctrl_driver.proto +23 -4
  25. pymammotion/proto/mctrl_driver_pb2.py +26 -20
  26. pymammotion/proto/mctrl_driver_pb2.pyi +38 -10
  27. pymammotion/proto/mctrl_nav.proto +18 -1
  28. pymammotion/proto/mctrl_nav_pb2.py +5 -3
  29. pymammotion/proto/mctrl_nav_pb2.pyi +34 -2
  30. pymammotion/proto/mctrl_pept.proto +6 -1
  31. pymammotion/proto/mctrl_pept_pb2.py +8 -6
  32. pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
  33. pymammotion/proto/mctrl_sys.proto +82 -9
  34. pymammotion/proto/mctrl_sys_pb2.py +162 -146
  35. pymammotion/proto/mctrl_sys_pb2.pyi +151 -34
  36. pymammotion/utility/device_type.py +3 -0
  37. {pymammotion-0.4.55.dist-info → pymammotion-0.4.57.dist-info}/METADATA +1 -1
  38. {pymammotion-0.4.55.dist-info → pymammotion-0.4.57.dist-info}/RECORD +40 -39
  39. {pymammotion-0.4.55.dist-info → pymammotion-0.4.57.dist-info}/LICENSE +0 -0
  40. {pymammotion-0.4.55.dist-info → pymammotion-0.4.57.dist-info}/WHEEL +0 -0
@@ -1,3 +1,4 @@
1
+ from datetime import UTC, datetime
1
2
  import time
2
3
 
3
4
  from Tea.exceptions import UnretryableException
@@ -103,7 +104,7 @@ class Client:
103
104
  _request.headers = TeaCore.merge(
104
105
  {
105
106
  "host": self._domain,
106
- "date": UtilClient.get_date_utcstring(),
107
+ "date": datetime.now(UTC).strftime("%a, %d %b %Y %H:%M:%S GMT"),
107
108
  "x-ca-nonce": UtilClient.get_nonce(),
108
109
  "x-ca-key": self._app_key,
109
110
  "x-ca-signaturemethod": "HmacSHA256",
@@ -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
@@ -1,7 +1,7 @@
1
1
  """Manage state from notifications into MowingDevice."""
2
2
 
3
3
  from collections.abc import Awaitable, Callable
4
- from datetime import datetime
4
+ from datetime import UTC, datetime
5
5
  import logging
6
6
  from typing import Any
7
7
 
@@ -41,7 +41,7 @@ class StateManager:
41
41
 
42
42
  def __init__(self, device: MowingDevice) -> None:
43
43
  self._device: MowingDevice = device
44
- self.last_updated_at = datetime.now()
44
+ self.last_updated_at = datetime.now(UTC)
45
45
  self.preference = ConnectionPreference.WIFI
46
46
  self.cloud_gethash_ack_callback: Callable[[NavGetHashListAck], Awaitable[None]] | None = None
47
47
  self.cloud_get_commondata_ack_callback: (
@@ -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)
@@ -115,7 +128,7 @@ class StateManager:
115
128
  async def notification(self, message: LubaMsg) -> None:
116
129
  """Handle protobuf notifications."""
117
130
  res = betterproto.which_one_of(message, "LubaSubMsg")
118
- self.last_updated_at = datetime.now()
131
+ self.last_updated_at = datetime.now(UTC)
119
132
  # additional catch all if we don't get a status update
120
133
  if not self._device.online:
121
134
  self._device.online = True
pymammotion/http/http.py CHANGED
@@ -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,14 +101,16 @@ 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()
107
108
  # TODO catch errors from mismatch like token expire etc
108
109
  # Assuming the data format matches the expected structure
109
110
  response = Response[StreamSubscriptionResponse].from_dict(data)
110
- response.data = StreamSubscriptionResponse.from_dict(data["data"])
111
+ if response.code != 0:
112
+ return response
113
+ response.data = StreamSubscriptionResponse.from_dict(data.get("data", {}))
111
114
  return response
112
115
 
113
116
  async def get_stream_subscription_mini_or_x_series(
@@ -131,14 +134,16 @@ class MammotionHTTP:
131
134
  headers={
132
135
  "Authorization": f"Bearer {self.login_info.access_token}",
133
136
  "Content-Type": "application/json",
134
- "User-Agent": "okhttp/3.14.9",
137
+ "User-Agent": "okhttp/4.9.3",
135
138
  },
136
139
  ) as resp:
137
140
  data = await resp.json()
138
141
  # TODO catch errors from mismatch like token expire etc
139
142
  # Assuming the data format matches the expected structure
140
143
  response = Response[StreamSubscriptionResponse].from_dict(data)
141
- response.data = StreamSubscriptionResponse.from_dict(data["data"])
144
+ if response.code != 0:
145
+ return response
146
+ response.data = StreamSubscriptionResponse.from_dict(data.get("data", {}))
142
147
  return response
143
148
 
144
149
  async def get_video_resource(self, iot_id: str) -> Response[VideoResourceResponse]:
@@ -149,13 +154,51 @@ class MammotionHTTP:
149
154
  headers={
150
155
  "Authorization": f"Bearer {self.login_info.access_token}",
151
156
  "Content-Type": "application/json",
152
- "User-Agent": "okhttp/3.14.9",
157
+ "User-Agent": "okhttp/4.9.3",
153
158
  },
154
159
  ) as resp:
155
160
  data = await resp.json()
156
161
  # TODO catch errors from mismatch like token expire etc
157
162
  # Assuming the data format matches the expected structure
158
- return Response[VideoResourceResponse].from_dict(data)
163
+ response = Response[VideoResourceResponse].from_dict(data)
164
+ if response.code != 0:
165
+ return response
166
+ response.data = VideoResourceResponse.from_dict(data.get("data", {}))
167
+ return response
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)
159
202
 
160
203
  async def refresh_login(self, account: str, password: str | None = None) -> Response[LoginResponseData]:
161
204
  if self._password is None and password is not None:
@@ -171,7 +214,7 @@ class MammotionHTTP:
171
214
  async with session.post(
172
215
  "/oauth/token",
173
216
  headers={
174
- "User-Agent": "okhttp/3.14.9",
217
+ "User-Agent": "okhttp/4.9.3",
175
218
  "App-Version": "google Pixel 2 XL taimen-Android 11,1.11.332",
176
219
  "Encrypt-Key": self.encryption_utils.encrypt_by_public_key(),
177
220
  "Decrypt-Type": "3",
@@ -189,11 +232,11 @@ class MammotionHTTP:
189
232
  print(resp.json())
190
233
  return Response.from_dict({"code": resp.status, "msg": "Login failed"})
191
234
  data = await resp.json()
192
- login_response = Response[LoginResponseData].from_dict(data)
235
+ login_response = response_factory(Response[LoginResponseData], data)
193
236
  if login_response.data is None:
194
237
  print(login_response)
195
238
  return Response.from_dict({"code": resp.status, "msg": "Login failed"})
196
- self.login_info = LoginResponseData.from_dict(login_response.data)
239
+ self.login_info = login_response.data
197
240
  self._headers["Authorization"] = (
198
241
  f"Bearer {self.login_info.access_token}" if login_response.data else None
199
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
@@ -202,6 +202,7 @@ class MessageSystem(AbstractMessage, ABC):
202
202
  # return self.send_order_msg_sys(build2)
203
203
 
204
204
  def send_sys_set_date_time(self) -> bytes:
205
+ # TODO get HA timezone
205
206
  calendar = datetime.datetime.now()
206
207
  i = calendar.year
207
208
  i2 = calendar.month