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.
- pymammotion/aliyun/client.py +2 -1
- pymammotion/aliyun/cloud_gateway.py +13 -3
- pymammotion/data/model/device.py +2 -1
- pymammotion/data/mqtt/properties.py +56 -44
- pymammotion/data/state_manager.py +19 -6
- pymammotion/http/http.py +54 -11
- pymammotion/http/model/http.py +46 -1
- pymammotion/http/model/response_factory.py +39 -0
- pymammotion/mammotion/commands/abstract_message.py +1 -4
- pymammotion/mammotion/commands/messages/system.py +1 -0
- pymammotion/mammotion/devices/mammotion.py +30 -9
- pymammotion/mammotion/devices/mammotion_bluetooth.py +5 -4
- pymammotion/mammotion/devices/mammotion_cloud.py +9 -3
- pymammotion/proto/__init__.py +2 -6
- pymammotion/proto/basestation.proto +8 -0
- pymammotion/proto/basestation_pb2.py +11 -9
- pymammotion/proto/basestation_pb2.pyi +16 -2
- pymammotion/proto/dev_net.proto +2 -0
- pymammotion/proto/dev_net_pb2.py +60 -60
- pymammotion/proto/dev_net_pb2.pyi +8 -4
- pymammotion/proto/luba_mul.proto +2 -2
- pymammotion/proto/luba_mul_pb2.py +15 -15
- pymammotion/proto/luba_mul_pb2.pyi +1 -1
- pymammotion/proto/mctrl_driver.proto +23 -4
- pymammotion/proto/mctrl_driver_pb2.py +26 -20
- pymammotion/proto/mctrl_driver_pb2.pyi +38 -10
- pymammotion/proto/mctrl_nav.proto +18 -1
- pymammotion/proto/mctrl_nav_pb2.py +5 -3
- pymammotion/proto/mctrl_nav_pb2.pyi +34 -2
- pymammotion/proto/mctrl_pept.proto +6 -1
- pymammotion/proto/mctrl_pept_pb2.py +8 -6
- pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
- pymammotion/proto/mctrl_sys.proto +82 -9
- pymammotion/proto/mctrl_sys_pb2.py +162 -146
- pymammotion/proto/mctrl_sys_pb2.pyi +151 -34
- pymammotion/utility/device_type.py +3 -0
- {pymammotion-0.4.55.dist-info → pymammotion-0.4.57.dist-info}/METADATA +1 -1
- {pymammotion-0.4.55.dist-info → pymammotion-0.4.57.dist-info}/RECORD +40 -39
- {pymammotion-0.4.55.dist-info → pymammotion-0.4.57.dist-info}/LICENSE +0 -0
- {pymammotion-0.4.55.dist-info → pymammotion-0.4.57.dist-info}/WHEEL +0 -0
pymammotion/aliyun/client.py
CHANGED
|
@@ -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":
|
|
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)
|
pymammotion/data/model/device.py
CHANGED
|
@@ -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:
|
|
18
|
+
batteryPercentage: int
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@dataclass
|
|
22
22
|
class BMSHardwareVersionItems(DataClassORJSONMixin):
|
|
23
|
-
bmsHardwareVersion:
|
|
23
|
+
bmsHardwareVersion: str
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
@dataclass
|
|
27
27
|
class CoordinateItems(DataClassORJSONMixin):
|
|
28
|
-
coordinate:
|
|
28
|
+
coordinate: str # '{"lon":0.303903,"lat":1.051868}'
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
@dataclass
|
|
32
32
|
class DeviceStateItems(DataClassORJSONMixin):
|
|
33
|
-
deviceState:
|
|
33
|
+
deviceState: int
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
@dataclass
|
|
37
37
|
class DeviceVersionItems(DataClassORJSONMixin):
|
|
38
|
-
deviceVersion:
|
|
38
|
+
deviceVersion: str
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
@dataclass
|
|
42
42
|
class DeviceVersionInfoItems(DataClassORJSONMixin):
|
|
43
|
-
deviceVersionInfo:
|
|
43
|
+
deviceVersionInfo: str
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
@dataclass
|
|
47
47
|
class ESP32VersionItems(DataClassORJSONMixin):
|
|
48
|
-
esp32Version:
|
|
48
|
+
esp32Version: str
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
@dataclass
|
|
52
52
|
class LeftMotorBootVersionItems(DataClassORJSONMixin):
|
|
53
|
-
leftMotorBootVersion:
|
|
53
|
+
leftMotorBootVersion: str
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
@dataclass
|
|
57
57
|
class LeftMotorVersionItems(DataClassORJSONMixin):
|
|
58
|
-
leftMotorVersion:
|
|
58
|
+
leftMotorVersion: str
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
@dataclass
|
|
62
62
|
class MCBootVersionItems(DataClassORJSONMixin):
|
|
63
|
-
mcBootVersion:
|
|
63
|
+
mcBootVersion: str
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
@dataclass
|
|
67
67
|
class NetworkInfoItems(DataClassORJSONMixin):
|
|
68
|
-
networkInfo:
|
|
68
|
+
networkInfo: str
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
@dataclass
|
|
72
72
|
class RightMotorBootVersionItems(DataClassORJSONMixin):
|
|
73
|
-
rightMotorBootVersion:
|
|
73
|
+
rightMotorBootVersion: str
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
@dataclass
|
|
77
77
|
class RightMotorVersionItems(DataClassORJSONMixin):
|
|
78
|
-
rightMotorVersion:
|
|
78
|
+
rightMotorVersion: str
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
@dataclass
|
|
82
82
|
class RTKVersionItems(DataClassORJSONMixin):
|
|
83
|
-
rtkVersion:
|
|
83
|
+
rtkVersion: str
|
|
84
84
|
|
|
85
85
|
|
|
86
86
|
@dataclass
|
|
87
87
|
class StationRTKVersionItems(DataClassORJSONMixin):
|
|
88
|
-
stationRtkVersion:
|
|
88
|
+
stationRtkVersion: str
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
@dataclass
|
|
92
92
|
class STM32H7VersionItems(DataClassORJSONMixin):
|
|
93
|
-
stm32H7Version:
|
|
93
|
+
stm32H7Version: str
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
|
|
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.
|
|
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/
|
|
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/
|
|
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.
|
|
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/
|
|
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.
|
|
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/
|
|
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
|
-
|
|
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/
|
|
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]
|
|
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 =
|
|
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
|
)
|
pymammotion/http/model/http.py
CHANGED
|
@@ -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
|