pymammotion 0.5.21__py3-none-any.whl → 0.5.45__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/__init__.py +3 -3
- pymammotion/aliyun/client.py +5 -2
- pymammotion/aliyun/cloud_gateway.py +137 -20
- pymammotion/aliyun/model/dev_by_account_response.py +169 -21
- pymammotion/const.py +3 -0
- pymammotion/data/model/device.py +1 -0
- pymammotion/data/model/device_config.py +1 -1
- pymammotion/data/model/device_info.py +4 -0
- pymammotion/data/model/enums.py +5 -3
- pymammotion/data/model/generate_route_information.py +2 -2
- pymammotion/data/model/hash_list.py +113 -33
- pymammotion/data/model/mowing_modes.py +8 -0
- pymammotion/data/model/region_data.py +4 -4
- pymammotion/data/{state_manager.py → mower_state_manager.py} +50 -13
- pymammotion/data/mqtt/event.py +47 -22
- pymammotion/data/mqtt/mammotion_properties.py +257 -0
- pymammotion/data/mqtt/properties.py +32 -29
- pymammotion/data/mqtt/status.py +17 -16
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +446 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/http.py +433 -18
- pymammotion/http/model/http.py +82 -2
- pymammotion/http/model/response_factory.py +10 -4
- pymammotion/mammotion/commands/mammotion_command.py +20 -0
- pymammotion/mammotion/commands/messages/driver.py +25 -0
- pymammotion/mammotion/commands/messages/navigation.py +10 -6
- pymammotion/mammotion/commands/messages/system.py +0 -14
- pymammotion/mammotion/devices/__init__.py +27 -3
- pymammotion/mammotion/devices/base.py +22 -146
- pymammotion/mammotion/devices/mammotion.py +364 -205
- pymammotion/mammotion/devices/mammotion_bluetooth.py +11 -8
- pymammotion/mammotion/devices/mammotion_cloud.py +49 -85
- pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
- pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
- pymammotion/mammotion/devices/managers/managers.py +81 -0
- pymammotion/mammotion/devices/mower_device.py +121 -0
- pymammotion/mammotion/devices/mower_manager.py +107 -0
- pymammotion/mammotion/devices/rtk_ble.py +89 -0
- pymammotion/mammotion/devices/rtk_cloud.py +113 -0
- pymammotion/mammotion/devices/rtk_device.py +50 -0
- pymammotion/mammotion/devices/rtk_manager.py +122 -0
- pymammotion/mqtt/__init__.py +2 -1
- pymammotion/mqtt/aliyun_mqtt.py +232 -0
- pymammotion/mqtt/mammotion_mqtt.py +174 -192
- pymammotion/mqtt/mqtt_models.py +66 -0
- pymammotion/proto/__init__.py +2 -2
- pymammotion/proto/mctrl_nav.proto +2 -2
- pymammotion/proto/mctrl_nav_pb2.py +1 -1
- pymammotion/proto/mctrl_nav_pb2.pyi +4 -4
- pymammotion/proto/mctrl_sys.proto +1 -1
- pymammotion/utility/datatype_converter.py +13 -12
- pymammotion/utility/device_type.py +88 -3
- pymammotion/utility/mur_mur_hash.py +132 -87
- {pymammotion-0.5.21.dist-info → pymammotion-0.5.45.dist-info}/METADATA +25 -30
- {pymammotion-0.5.21.dist-info → pymammotion-0.5.45.dist-info}/RECORD +64 -50
- {pymammotion-0.5.21.dist-info → pymammotion-0.5.45.dist-info}/WHEEL +1 -1
- pymammotion/http/_init_.py +0 -0
- {pymammotion-0.5.21.dist-info → pymammotion-0.5.45.dist-info/licenses}/LICENSE +0 -0
pymammotion/__init__.py
CHANGED
|
@@ -15,12 +15,12 @@ from pymammotion.bluetooth.ble import MammotionBLE
|
|
|
15
15
|
from pymammotion.http.http import MammotionHTTP
|
|
16
16
|
|
|
17
17
|
# TODO make a working device that will work outside HA too.
|
|
18
|
-
from pymammotion.mqtt import MammotionMQTT
|
|
18
|
+
from pymammotion.mqtt import AliyunMQTT, MammotionMQTT
|
|
19
19
|
|
|
20
20
|
logger = logging.getLogger(__name__)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
__all__ = ["MammotionBLE", "MammotionHTTP", "MammotionMQTT", "logger"]
|
|
23
|
+
__all__ = ["MammotionBLE", "MammotionHTTP", "AliyunMQTT", "MammotionMQTT", "logger"]
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
# TODO provide interface to pick between mqtt/cloud/bluetooth
|
|
@@ -37,7 +37,7 @@ if __name__ == "__main__":
|
|
|
37
37
|
REGION = os.environ.get("REGION")
|
|
38
38
|
mammotion_http = MammotionHTTP()
|
|
39
39
|
cloud_client = CloudIOTGateway(mammotion_http)
|
|
40
|
-
luba =
|
|
40
|
+
luba = AliyunMQTT(
|
|
41
41
|
iot_token=IOT_TOKEN or "",
|
|
42
42
|
region_id=REGION or "",
|
|
43
43
|
product_key=PRODUCT_KEY or "",
|
pymammotion/aliyun/client.py
CHANGED
|
@@ -81,7 +81,7 @@ class Client:
|
|
|
81
81
|
"maxAttempts": UtilClient.default_number(runtime.max_attempts, 3),
|
|
82
82
|
},
|
|
83
83
|
"backoff": {
|
|
84
|
-
"policy": UtilClient.default_string(runtime.backoff_policy, "
|
|
84
|
+
"policy": UtilClient.default_string(runtime.backoff_policy, "yes"),
|
|
85
85
|
"period": UtilClient.default_number(runtime.backoff_period, 1),
|
|
86
86
|
},
|
|
87
87
|
"ignoreSSL": runtime.ignore_ssl,
|
|
@@ -124,6 +124,9 @@ class Client:
|
|
|
124
124
|
_request.headers["x-ca-signature"] = APIGatewayUtilClient.get_signature(_request, self._app_secret)
|
|
125
125
|
_last_request = _request
|
|
126
126
|
_response = TeaCore.do_action(_request, _runtime)
|
|
127
|
+
if _response.body.get("code") == 20056:
|
|
128
|
+
raise Exception("Gateway timeout.")
|
|
129
|
+
|
|
127
130
|
return _response
|
|
128
131
|
except Exception as e:
|
|
129
132
|
if TeaCore.is_retryable(e):
|
|
@@ -171,7 +174,7 @@ class Client:
|
|
|
171
174
|
"maxAttempts": UtilClient.default_number(runtime.max_attempts, 3),
|
|
172
175
|
},
|
|
173
176
|
"backoff": {
|
|
174
|
-
"policy": UtilClient.default_string(runtime.backoff_policy, "
|
|
177
|
+
"policy": UtilClient.default_string(runtime.backoff_policy, "yes"),
|
|
175
178
|
"period": UtilClient.default_number(runtime.backoff_period, 1),
|
|
176
179
|
},
|
|
177
180
|
"ignoreSSL": runtime.ignore_ssl,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Module for interacting with Aliyun Cloud IoT Gateway."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import base64
|
|
4
5
|
import hashlib
|
|
5
6
|
import hmac
|
|
@@ -21,7 +22,7 @@ from Tea.exceptions import UnretryableException
|
|
|
21
22
|
from pymammotion.aliyun.client import Client
|
|
22
23
|
from pymammotion.aliyun.model.aep_response import AepResponse
|
|
23
24
|
from pymammotion.aliyun.model.connect_response import ConnectResponse
|
|
24
|
-
from pymammotion.aliyun.model.dev_by_account_response import
|
|
25
|
+
from pymammotion.aliyun.model.dev_by_account_response import ListingDevAccountResponse
|
|
25
26
|
from pymammotion.aliyun.model.login_by_oauth_response import LoginByOAuthResponse
|
|
26
27
|
from pymammotion.aliyun.model.regions_response import RegionResponse
|
|
27
28
|
from pymammotion.aliyun.model.session_by_authcode_response import SessionByAuthCodeResponse
|
|
@@ -86,6 +87,14 @@ class GatewayTimeoutException(Exception):
|
|
|
86
87
|
self.iot_id = args[1]
|
|
87
88
|
|
|
88
89
|
|
|
90
|
+
class TooManyRequestsException(Exception):
|
|
91
|
+
"""Raise exception when the gateway times out."""
|
|
92
|
+
|
|
93
|
+
def __init__(self, *args: object) -> None:
|
|
94
|
+
super().__init__(args)
|
|
95
|
+
self.iot_id = args[1]
|
|
96
|
+
|
|
97
|
+
|
|
89
98
|
class LoginException(Exception):
|
|
90
99
|
"""Raise exception when library cannot log in."""
|
|
91
100
|
|
|
@@ -94,6 +103,9 @@ class CheckSessionException(Exception):
|
|
|
94
103
|
"""Raise exception when checking session results in a failure."""
|
|
95
104
|
|
|
96
105
|
|
|
106
|
+
EXPIRED_CREDENTIAL_EXCEPTIONS = (CheckSessionException, SetupException)
|
|
107
|
+
|
|
108
|
+
|
|
97
109
|
class CloudIOTGateway:
|
|
98
110
|
"""Class for interacting with Aliyun Cloud IoT Gateway."""
|
|
99
111
|
|
|
@@ -111,14 +123,14 @@ class CloudIOTGateway:
|
|
|
111
123
|
aep_response: AepResponse | None = None,
|
|
112
124
|
session_by_authcode_response: SessionByAuthCodeResponse | None = None,
|
|
113
125
|
region_response: RegionResponse | None = None,
|
|
114
|
-
dev_by_account:
|
|
126
|
+
dev_by_account: ListingDevAccountResponse | None = None,
|
|
115
127
|
) -> None:
|
|
116
128
|
"""Initialize the CloudIOTGateway."""
|
|
117
129
|
self.mammotion_http: MammotionHTTP = mammotion_http
|
|
118
130
|
self._app_key = APP_KEY
|
|
119
131
|
self._app_secret = APP_SECRET
|
|
120
132
|
self.domain = ALIYUN_DOMAIN
|
|
121
|
-
|
|
133
|
+
self.message_delay = 1
|
|
122
134
|
self._client_id = self.generate_hardware_string(8) # 8 characters
|
|
123
135
|
self._device_sn = self.generate_hardware_string(32) # 32 characters
|
|
124
136
|
self._utdid = self.generate_hardware_string(32) # 32 characters
|
|
@@ -137,7 +149,7 @@ class CloudIOTGateway:
|
|
|
137
149
|
)
|
|
138
150
|
|
|
139
151
|
@staticmethod
|
|
140
|
-
def generate_random_string(length: int):
|
|
152
|
+
def generate_random_string(length: int) -> str:
|
|
141
153
|
"""Generate a random string of specified length."""
|
|
142
154
|
characters = string.ascii_letters + string.digits
|
|
143
155
|
return "".join(random.choice(characters) for _ in range(length))
|
|
@@ -156,7 +168,7 @@ class CloudIOTGateway:
|
|
|
156
168
|
logger.error("Couldn't decode message %s", response_body_str)
|
|
157
169
|
return {"code": 22000}
|
|
158
170
|
|
|
159
|
-
def sign(self, data):
|
|
171
|
+
def sign(self, data: dict) -> str:
|
|
160
172
|
"""Generate signature for the given data."""
|
|
161
173
|
keys = ["appKey", "clientId", "deviceSn", "timestamp"]
|
|
162
174
|
concatenated_str = ""
|
|
@@ -171,7 +183,7 @@ class CloudIOTGateway:
|
|
|
171
183
|
hashlib.sha1,
|
|
172
184
|
).hexdigest()
|
|
173
185
|
|
|
174
|
-
async def get_region(self, country_code: str):
|
|
186
|
+
async def get_region(self, country_code: str) -> RegionResponse:
|
|
175
187
|
"""Get the region based on country code and auth code."""
|
|
176
188
|
auth_code = self.mammotion_http.login_info.authorization_code
|
|
177
189
|
|
|
@@ -235,7 +247,7 @@ class CloudIOTGateway:
|
|
|
235
247
|
|
|
236
248
|
return response.body
|
|
237
249
|
|
|
238
|
-
async def aep_handle(self):
|
|
250
|
+
async def aep_handle(self) -> AepResponse:
|
|
239
251
|
"""Handle AEP authentication."""
|
|
240
252
|
aep_domain = self.domain
|
|
241
253
|
|
|
@@ -291,9 +303,9 @@ class CloudIOTGateway:
|
|
|
291
303
|
|
|
292
304
|
logger.debug(response_body_dict)
|
|
293
305
|
|
|
294
|
-
return
|
|
306
|
+
return self._aep_response
|
|
295
307
|
|
|
296
|
-
async def connect(self):
|
|
308
|
+
async def connect(self) -> ConnectResponse:
|
|
297
309
|
"""Connect to the Aliyun Cloud IoT Gateway."""
|
|
298
310
|
region_url = "sdk.openaccount.aliyun.com"
|
|
299
311
|
time_now = time.time()
|
|
@@ -440,7 +452,7 @@ class CloudIOTGateway:
|
|
|
440
452
|
return self._login_by_oauth_response
|
|
441
453
|
raise LoginException(data)
|
|
442
454
|
|
|
443
|
-
async def session_by_auth_code(self):
|
|
455
|
+
async def session_by_auth_code(self) -> SessionByAuthCodeResponse:
|
|
444
456
|
"""Create a session by auth code."""
|
|
445
457
|
config = Config(
|
|
446
458
|
app_key=self._app_key,
|
|
@@ -591,11 +603,16 @@ class CloudIOTGateway:
|
|
|
591
603
|
await self.sign_out()
|
|
592
604
|
raise CheckSessionException("Error check or refresh token: " + response_body_dict.__str__())
|
|
593
605
|
|
|
606
|
+
if response_body_dict.get("code") == 2401:
|
|
607
|
+
await self.sign_out()
|
|
608
|
+
raise CheckSessionException("Error check or refresh token: " + response_body_dict.__str__())
|
|
609
|
+
|
|
594
610
|
session = SessionByAuthCodeResponse.from_dict(response_body_dict)
|
|
595
611
|
session_data = session.data
|
|
596
612
|
|
|
597
613
|
if (
|
|
598
|
-
session_data
|
|
614
|
+
session_data is None
|
|
615
|
+
or session_data.identityId is None
|
|
599
616
|
or session_data.refreshTokenExpire is None
|
|
600
617
|
or session_data.iotToken is None
|
|
601
618
|
or session_data.iotTokenExpire is None
|
|
@@ -606,7 +623,7 @@ class CloudIOTGateway:
|
|
|
606
623
|
self._session_by_authcode_response = session
|
|
607
624
|
self._iot_token_issued_at = int(time.time())
|
|
608
625
|
|
|
609
|
-
async def list_binding_by_account(self) ->
|
|
626
|
+
async def list_binding_by_account(self) -> ListingDevAccountResponse:
|
|
610
627
|
"""List bindings by account."""
|
|
611
628
|
config = Config(
|
|
612
629
|
app_key=self._app_key,
|
|
@@ -645,9 +662,9 @@ class CloudIOTGateway:
|
|
|
645
662
|
response_body_dict = self.parse_json_response(response_body_str)
|
|
646
663
|
|
|
647
664
|
if int(response_body_dict.get("code")) != 200:
|
|
648
|
-
raise Exception("Error in creating session: " + response_body_dict["
|
|
665
|
+
raise Exception("Error in creating session: " + response_body_dict["message"])
|
|
649
666
|
|
|
650
|
-
self._devices_by_account_response =
|
|
667
|
+
self._devices_by_account_response = ListingDevAccountResponse.from_dict(response_body_dict)
|
|
651
668
|
return self._devices_by_account_response
|
|
652
669
|
|
|
653
670
|
async def list_binding_by_dev(self, iot_id: str):
|
|
@@ -685,10 +702,94 @@ class CloudIOTGateway:
|
|
|
685
702
|
# Load the JSON string into a dictionary
|
|
686
703
|
response_body_dict = self.parse_json_response(response_body_str)
|
|
687
704
|
|
|
705
|
+
if int(response_body_dict.get("code")) != 200:
|
|
706
|
+
raise Exception("Error in getting shared device list: " + response_body_dict["msg"])
|
|
707
|
+
|
|
708
|
+
self._devices_by_account_response = ListingDevAccountResponse.from_dict(response_body_dict)
|
|
709
|
+
return self._devices_by_account_response
|
|
710
|
+
|
|
711
|
+
async def confirm_share(self, record_list: list[str]) -> bool:
|
|
712
|
+
config = Config(
|
|
713
|
+
app_key=self._app_key,
|
|
714
|
+
app_secret=self._app_secret,
|
|
715
|
+
domain=self._region_response.data.apiGatewayEndpoint,
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
client = Client(config)
|
|
719
|
+
|
|
720
|
+
# build request
|
|
721
|
+
request = CommonParams(
|
|
722
|
+
api_ver="1.0.7",
|
|
723
|
+
language="en-US",
|
|
724
|
+
iot_token=self._session_by_authcode_response.data.iotToken,
|
|
725
|
+
)
|
|
726
|
+
body = IoTApiRequest(
|
|
727
|
+
id=str(uuid.uuid4()),
|
|
728
|
+
params={"agree": 1, "recordIdList": record_list},
|
|
729
|
+
request=request,
|
|
730
|
+
version="1.0",
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
# send request
|
|
734
|
+
response = await client.async_do_request("/uc/confirmShare", "https", "POST", None, body, RuntimeOptions())
|
|
735
|
+
logger.debug(response.status_message)
|
|
736
|
+
logger.debug(response.headers)
|
|
737
|
+
logger.debug(response.status_code)
|
|
738
|
+
logger.debug(response.body)
|
|
739
|
+
|
|
740
|
+
# Decode the response body
|
|
741
|
+
response_body_str = response.body.decode("utf-8")
|
|
742
|
+
|
|
743
|
+
# Load the JSON string into a dictionary
|
|
744
|
+
response_body_dict = self.parse_json_response(response_body_str)
|
|
745
|
+
|
|
746
|
+
if int(response_body_dict.get("code")) != 200:
|
|
747
|
+
raise Exception("Error in accepting share: " + response_body_dict["msg"])
|
|
748
|
+
|
|
749
|
+
return True
|
|
750
|
+
|
|
751
|
+
async def get_shared_notice_list(self):
|
|
752
|
+
### status 0 accepted status -1 ready to be accepted 3 expired
|
|
753
|
+
config = Config(
|
|
754
|
+
app_key=self._app_key,
|
|
755
|
+
app_secret=self._app_secret,
|
|
756
|
+
domain=self._region_response.data.apiGatewayEndpoint,
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
client = Client(config)
|
|
760
|
+
|
|
761
|
+
# build request
|
|
762
|
+
request = CommonParams(
|
|
763
|
+
api_ver="1.0.9",
|
|
764
|
+
language="en-US",
|
|
765
|
+
iot_token=self._session_by_authcode_response.data.iotToken,
|
|
766
|
+
)
|
|
767
|
+
body = IoTApiRequest(
|
|
768
|
+
id=str(uuid.uuid4()),
|
|
769
|
+
params={"pageSize": 100, "pageNo": 1},
|
|
770
|
+
request=request,
|
|
771
|
+
version="1.0",
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
# send request
|
|
775
|
+
response = await client.async_do_request(
|
|
776
|
+
"/uc/getShareNoticeList", "https", "POST", None, body, RuntimeOptions()
|
|
777
|
+
)
|
|
778
|
+
logger.debug(response.status_message)
|
|
779
|
+
logger.debug(response.headers)
|
|
780
|
+
logger.debug(response.status_code)
|
|
781
|
+
logger.debug(response.body)
|
|
782
|
+
|
|
783
|
+
# Decode the response body
|
|
784
|
+
response_body_str = response.body.decode("utf-8")
|
|
785
|
+
|
|
786
|
+
# Load the JSON string into a dictionary
|
|
787
|
+
response_body_dict = self.parse_json_response(response_body_str)
|
|
788
|
+
|
|
688
789
|
if int(response_body_dict.get("code")) != 200:
|
|
689
790
|
raise Exception("Error in creating session: " + response_body_dict["msg"])
|
|
690
791
|
|
|
691
|
-
self._devices_by_account_response =
|
|
792
|
+
self._devices_by_account_response = ListingDevAccountResponse.from_dict(response_body_dict)
|
|
692
793
|
return self._devices_by_account_response
|
|
693
794
|
|
|
694
795
|
async def send_cloud_command(self, iot_id: str, command: bytes) -> str:
|
|
@@ -730,7 +831,6 @@ class CloudIOTGateway:
|
|
|
730
831
|
)
|
|
731
832
|
|
|
732
833
|
client = Client(config)
|
|
733
|
-
|
|
734
834
|
# build request
|
|
735
835
|
request = CommonParams(
|
|
736
836
|
api_ver="1.0.5",
|
|
@@ -762,6 +862,16 @@ class CloudIOTGateway:
|
|
|
762
862
|
logger.debug(response.body)
|
|
763
863
|
logger.debug(iot_id)
|
|
764
864
|
|
|
865
|
+
if response.status_code == 429:
|
|
866
|
+
logger.debug("too many requests.")
|
|
867
|
+
if self.message_delay > 8:
|
|
868
|
+
raise TooManyRequestsException(response.status_message, iot_id)
|
|
869
|
+
asyncio.get_event_loop().call_later(
|
|
870
|
+
self.message_delay, lambda: asyncio.ensure_future(self.send_cloud_command(iot_id, command))
|
|
871
|
+
)
|
|
872
|
+
self.message_delay = self.message_delay * 2
|
|
873
|
+
return message_id
|
|
874
|
+
|
|
765
875
|
response_body_str = response.body.decode("utf-8")
|
|
766
876
|
response_body_dict = self.parse_json_response(response_body_str)
|
|
767
877
|
|
|
@@ -785,6 +895,13 @@ class CloudIOTGateway:
|
|
|
785
895
|
if response_body_dict.get("code") == 6205:
|
|
786
896
|
raise DeviceOfflineException(response_body_dict.get("code"), iot_id)
|
|
787
897
|
|
|
898
|
+
if response_body_dict.get("code") == 460:
|
|
899
|
+
logger.debug("iotToken expired, must re-login.")
|
|
900
|
+
raise CheckSessionException(response_body_dict.get("message"))
|
|
901
|
+
|
|
902
|
+
if self.message_delay != 1:
|
|
903
|
+
self.message_delay = 1
|
|
904
|
+
|
|
788
905
|
return message_id
|
|
789
906
|
|
|
790
907
|
async def get_device_properties(self, iot_id: str) -> ThingPropertiesResponse:
|
|
@@ -838,11 +955,11 @@ class CloudIOTGateway:
|
|
|
838
955
|
self.mammotion_http = mammotion_http
|
|
839
956
|
|
|
840
957
|
@property
|
|
841
|
-
def region_response(self) -> RegionResponse:
|
|
958
|
+
def region_response(self) -> RegionResponse | None:
|
|
842
959
|
return self._region_response
|
|
843
960
|
|
|
844
961
|
@property
|
|
845
|
-
def aep_response(self) -> AepResponse:
|
|
962
|
+
def aep_response(self) -> AepResponse | None:
|
|
846
963
|
return self._aep_response
|
|
847
964
|
|
|
848
965
|
@property
|
|
@@ -854,9 +971,9 @@ class CloudIOTGateway:
|
|
|
854
971
|
return self._client_id
|
|
855
972
|
|
|
856
973
|
@property
|
|
857
|
-
def login_by_oauth_response(self) -> LoginByOAuthResponse:
|
|
974
|
+
def login_by_oauth_response(self) -> LoginByOAuthResponse | None:
|
|
858
975
|
return self._login_by_oauth_response
|
|
859
976
|
|
|
860
977
|
@property
|
|
861
|
-
def connect_response(self) -> ConnectResponse:
|
|
978
|
+
def connect_response(self) -> ConnectResponse | None:
|
|
862
979
|
return self._connect_response
|
|
@@ -1,35 +1,183 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
+
from typing import Annotated, Optional
|
|
2
3
|
|
|
3
4
|
from mashumaro.config import BaseConfig
|
|
4
5
|
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
6
|
+
from mashumaro.types import Alias
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
@dataclass
|
|
8
10
|
class Device(DataClassORJSONMixin):
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
categoryName: str
|
|
17
|
-
identityAlias: str
|
|
18
|
-
productName: str
|
|
19
|
-
iotId: str
|
|
20
|
-
bindTime: int
|
|
21
|
-
owned: int
|
|
22
|
-
identityId: str
|
|
23
|
-
thingType: str
|
|
11
|
+
"""Unified device model supporting both Device and ShareNotification data"""
|
|
12
|
+
|
|
13
|
+
# Core device fields (from Device model)
|
|
14
|
+
gmt_modified: Annotated[int, Alias("gmtModified")]
|
|
15
|
+
node_type: Annotated[str, Alias("nodeType")]
|
|
16
|
+
device_name: Annotated[str, Alias("deviceName")]
|
|
17
|
+
product_name: Annotated[str, Alias("productName")]
|
|
24
18
|
status: int
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
identity_id: Annotated[str, Alias("identityId")]
|
|
20
|
+
|
|
21
|
+
# Required fields from original Device model
|
|
22
|
+
net_type: Annotated[str, Alias("netType")]
|
|
23
|
+
category_key: Annotated[str, Alias("categoryKey")]
|
|
24
|
+
product_key: Annotated[str, Alias("productKey")]
|
|
25
|
+
is_edge_gateway: Annotated[bool, Alias("isEdgeGateway")]
|
|
26
|
+
category_name: Annotated[str, Alias("categoryName")]
|
|
27
|
+
identity_alias: Annotated[str, Alias("identityAlias")]
|
|
28
|
+
iot_id: Annotated[str, Alias("iotId")]
|
|
29
|
+
bind_time: Annotated[int, Alias("bindTime")]
|
|
30
|
+
owned: int
|
|
31
|
+
thing_type: Annotated[str, Alias("thingType")]
|
|
32
|
+
|
|
33
|
+
# Optional fields (common to both or nullable)
|
|
34
|
+
nick_name: Annotated[Optional[str], Alias("nickName")] = None
|
|
35
|
+
description: Optional[str] = None
|
|
36
|
+
product_image: Annotated[Optional[str], Alias("productImage")] = None
|
|
37
|
+
category_image: Annotated[Optional[str], Alias("categoryImage")] = None
|
|
38
|
+
product_model: Annotated[Optional[str], Alias("productModel")] = None
|
|
39
|
+
|
|
40
|
+
# Optional fields from ShareNotification only
|
|
41
|
+
target_id: Annotated[Optional[str], Alias("targetId")] = None
|
|
42
|
+
receiver_identity_id: Annotated[Optional[str], Alias("receiverIdentityId")] = None
|
|
43
|
+
target_type: Annotated[Optional[str], Alias("targetType")] = None
|
|
44
|
+
gmt_create: Annotated[Optional[int], Alias("gmtCreate")] = None
|
|
45
|
+
batch_id: Annotated[Optional[str], Alias("batchId")] = None
|
|
46
|
+
record_id: Annotated[Optional[str], Alias("recordId")] = None
|
|
47
|
+
initiator_identity_id: Annotated[Optional[str], Alias("initiatorIdentityId")] = None
|
|
48
|
+
is_receiver: Annotated[Optional[int], Alias("isReceiver")] = None
|
|
49
|
+
initiator_alias: Annotated[Optional[str], Alias("initiatorAlias")] = None
|
|
50
|
+
receiver_alias: Annotated[Optional[str], Alias("receiverAlias")] = None
|
|
30
51
|
|
|
31
52
|
class Config(BaseConfig):
|
|
32
53
|
omit_default = True
|
|
54
|
+
allow_deserialization_not_by_alias = True
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# # Alternative: Keep them separate but with a common base class
|
|
58
|
+
# @dataclass
|
|
59
|
+
# class BaseDevice(DataClassORJSONMixin):
|
|
60
|
+
# """Base device model with common fields"""
|
|
61
|
+
#
|
|
62
|
+
# gmt_modified: int
|
|
63
|
+
# node_type: str
|
|
64
|
+
# device_name: str
|
|
65
|
+
# product_name: str
|
|
66
|
+
# status: int
|
|
67
|
+
# product_image: Optional[str] = None
|
|
68
|
+
# category_image: Optional[str] = None
|
|
69
|
+
# description: Optional[str] = None
|
|
70
|
+
#
|
|
71
|
+
# class Config(BaseConfig):
|
|
72
|
+
# omit_default = True
|
|
73
|
+
# serialize_by_alias = True
|
|
74
|
+
# aliases = {
|
|
75
|
+
# "gmt_modified": "gmtModified",
|
|
76
|
+
# "node_type": "nodeType",
|
|
77
|
+
# "device_name": "deviceName",
|
|
78
|
+
# "product_name": "productName",
|
|
79
|
+
# "product_image": "productImage",
|
|
80
|
+
# "category_image": "categoryImage",
|
|
81
|
+
# }
|
|
82
|
+
#
|
|
83
|
+
#
|
|
84
|
+
# @dataclass
|
|
85
|
+
# class Device(BaseDevice):
|
|
86
|
+
# """Full device model"""
|
|
87
|
+
#
|
|
88
|
+
# net_type: str
|
|
89
|
+
# category_key: str
|
|
90
|
+
# product_key: str
|
|
91
|
+
# is_edge_gateway: bool
|
|
92
|
+
# category_name: str
|
|
93
|
+
# identity_alias: str
|
|
94
|
+
# iot_id: str
|
|
95
|
+
# bind_time: int
|
|
96
|
+
# owned: int
|
|
97
|
+
# identity_id: str
|
|
98
|
+
# thing_type: str
|
|
99
|
+
# nick_name: Optional[str] = None
|
|
100
|
+
# product_model: Optional[str] = None
|
|
101
|
+
#
|
|
102
|
+
# class Config(BaseConfig):
|
|
103
|
+
# omit_default = True
|
|
104
|
+
# serialize_by_alias = True
|
|
105
|
+
# aliases = {
|
|
106
|
+
# **BaseDevice.Config.aliases,
|
|
107
|
+
# "net_type": "netType",
|
|
108
|
+
# "category_key": "categoryKey",
|
|
109
|
+
# "product_key": "productKey",
|
|
110
|
+
# "is_edge_gateway": "isEdgeGateway",
|
|
111
|
+
# "category_name": "categoryName",
|
|
112
|
+
# "identity_alias": "identityAlias",
|
|
113
|
+
# "iot_id": "iotId",
|
|
114
|
+
# "bind_time": "bindTime",
|
|
115
|
+
# "identity_id": "identityId",
|
|
116
|
+
# "thing_type": "thingType",
|
|
117
|
+
# "nick_name": "nickName",
|
|
118
|
+
# "product_model": "productModel",
|
|
119
|
+
# }
|
|
120
|
+
#
|
|
121
|
+
#
|
|
122
|
+
# @dataclass
|
|
123
|
+
# class ShareNotification(BaseDevice):
|
|
124
|
+
# """Share notification model extending base device"""
|
|
125
|
+
#
|
|
126
|
+
# target_id: str
|
|
127
|
+
# receiver_identity_id: str
|
|
128
|
+
# target_type: str
|
|
129
|
+
# gmt_create: int
|
|
130
|
+
# batch_id: str
|
|
131
|
+
# record_id: str
|
|
132
|
+
# initiator_identity_id: str
|
|
133
|
+
# is_receiver: int
|
|
134
|
+
# initiator_alias: str
|
|
135
|
+
# receiver_alias: str
|
|
136
|
+
#
|
|
137
|
+
# # Optional fields that Device has but ShareNotification might not
|
|
138
|
+
# net_type: Optional[str] = None
|
|
139
|
+
# category_key: Optional[str] = None
|
|
140
|
+
# product_key: Optional[str] = None
|
|
141
|
+
# is_edge_gateway: Optional[bool] = None
|
|
142
|
+
# category_name: Optional[str] = None
|
|
143
|
+
# identity_alias: Optional[str] = None
|
|
144
|
+
# iot_id: Optional[str] = None
|
|
145
|
+
# bind_time: Optional[int] = None
|
|
146
|
+
# owned: Optional[int] = None
|
|
147
|
+
# identity_id: Optional[str] = None
|
|
148
|
+
# thing_type: Optional[str] = None
|
|
149
|
+
# nick_name: Optional[str] = None
|
|
150
|
+
# product_model: Optional[str] = None
|
|
151
|
+
#
|
|
152
|
+
# class Config(BaseConfig):
|
|
153
|
+
# omit_default = True
|
|
154
|
+
# serialize_by_alias = True
|
|
155
|
+
# aliases = {
|
|
156
|
+
# **BaseDevice.Config.aliases,
|
|
157
|
+
# "target_id": "targetId",
|
|
158
|
+
# "receiver_identity_id": "receiverIdentityId",
|
|
159
|
+
# "target_type": "targetType",
|
|
160
|
+
# "gmt_create": "gmtCreate",
|
|
161
|
+
# "batch_id": "batchId",
|
|
162
|
+
# "record_id": "recordId",
|
|
163
|
+
# "initiator_identity_id": "initiatorIdentityId",
|
|
164
|
+
# "is_receiver": "isReceiver",
|
|
165
|
+
# "initiator_alias": "initiatorAlias",
|
|
166
|
+
# "receiver_alias": "receiverAlias",
|
|
167
|
+
# # Device fields that might be present
|
|
168
|
+
# "net_type": "netType",
|
|
169
|
+
# "category_key": "categoryKey",
|
|
170
|
+
# "product_key": "productKey",
|
|
171
|
+
# "is_edge_gateway": "isEdgeGateway",
|
|
172
|
+
# "category_name": "categoryName",
|
|
173
|
+
# "identity_alias": "identityAlias",
|
|
174
|
+
# "iot_id": "iotId",
|
|
175
|
+
# "bind_time": "bindTime",
|
|
176
|
+
# "identity_id": "identityId",
|
|
177
|
+
# "thing_type": "thingType",
|
|
178
|
+
# "nick_name": "nickName",
|
|
179
|
+
# "product_model": "productModel",
|
|
180
|
+
# }
|
|
33
181
|
|
|
34
182
|
|
|
35
183
|
@dataclass
|
|
@@ -41,7 +189,7 @@ class Data(DataClassORJSONMixin):
|
|
|
41
189
|
|
|
42
190
|
|
|
43
191
|
@dataclass
|
|
44
|
-
class
|
|
192
|
+
class ListingDevAccountResponse(DataClassORJSONMixin):
|
|
45
193
|
code: int
|
|
46
194
|
data: Data | None
|
|
47
195
|
id: str | None = None
|
pymammotion/const.py
CHANGED
|
@@ -8,3 +8,6 @@ MAMMOTION_DOMAIN = "https://id.mammotion.com"
|
|
|
8
8
|
MAMMOTION_API_DOMAIN = "https://domestic.mammotion.com"
|
|
9
9
|
MAMMOTION_CLIENT_ID = "MADKALUBAS"
|
|
10
10
|
MAMMOTION_CLIENT_SECRET = "GshzGRZJjuMUgd2sYHM7"
|
|
11
|
+
|
|
12
|
+
MAMMOTION_OUATH2_CLIENT_ID = "GxebgSt8si6pKqR"
|
|
13
|
+
MAMMOTION_OUATH2_CLIENT_SECRET = "JP0508SRJFa0A90ADpzLINDBxMa4Vj"
|
pymammotion/data/model/device.py
CHANGED
|
@@ -96,6 +96,7 @@ class MowingDevice(DataClassORJSONMixin):
|
|
|
96
96
|
self.location.device = coordinate_converter.enu_to_lla(
|
|
97
97
|
parse_double(location.real_pos_y, 4.0), parse_double(location.real_pos_x, 4.0)
|
|
98
98
|
)
|
|
99
|
+
self.map.invalidate_maps(location.bol_hash)
|
|
99
100
|
if location.zone_hash:
|
|
100
101
|
self.location.work_zone = (
|
|
101
102
|
location.zone_hash if self.report_data.dev.sys_status == WorkMode.MODE_WORKING else 0
|
|
@@ -30,7 +30,7 @@ class OperationSettings(DataClassORJSONMixin):
|
|
|
30
30
|
obstacle_laps: int = 1
|
|
31
31
|
mowing_laps: int = 1 # border laps
|
|
32
32
|
start_progress: int = 0
|
|
33
|
-
areas:
|
|
33
|
+
areas: set[int] = field(default_factory=set)
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def create_path_order(operation_mode: OperationSettings, device_name: str) -> str:
|
|
@@ -19,18 +19,22 @@ class DeviceNonWorkingHours(DataClassORJSONMixin):
|
|
|
19
19
|
start_time: str = ""
|
|
20
20
|
end_time: str = ""
|
|
21
21
|
|
|
22
|
+
|
|
22
23
|
@dataclass
|
|
23
24
|
class LampInfo(DataClassORJSONMixin):
|
|
24
25
|
lamp_bright: int = 0
|
|
25
26
|
manual_light: bool = False
|
|
26
27
|
night_light: bool = False
|
|
27
28
|
|
|
29
|
+
|
|
28
30
|
@dataclass
|
|
29
31
|
class MowerInfo(DataClassORJSONMixin):
|
|
30
32
|
blade_status: bool = False
|
|
31
33
|
rain_detection: bool = False
|
|
32
34
|
traversal_mode: int = 0
|
|
33
35
|
turning_mode: int = 0
|
|
36
|
+
blade_mode: int = 0
|
|
37
|
+
blade_rpm: int = 0
|
|
34
38
|
side_led: SideLight = field(default_factory=SideLight)
|
|
35
39
|
collector_installation_status: bool = False
|
|
36
40
|
model: str = ""
|
pymammotion/data/model/enums.py
CHANGED
|
@@ -4,9 +4,11 @@ from enum import Enum
|
|
|
4
4
|
class ConnectionPreference(Enum):
|
|
5
5
|
"""Enum for connection preference."""
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
ANY = 0
|
|
8
8
|
WIFI = 1
|
|
9
9
|
BLUETOOTH = 2
|
|
10
|
+
PREFER_WIFI = 3
|
|
11
|
+
PREFER_BLUETOOTH = 4
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
class PositionMode(Enum):
|
|
@@ -17,7 +19,7 @@ class PositionMode(Enum):
|
|
|
17
19
|
UNKNOWN = 4
|
|
18
20
|
|
|
19
21
|
@staticmethod
|
|
20
|
-
def from_value(value: int):
|
|
22
|
+
def from_value(value: int) -> "PositionMode":
|
|
21
23
|
if value == 0:
|
|
22
24
|
return PositionMode.FIX
|
|
23
25
|
elif value == 1:
|
|
@@ -50,7 +52,7 @@ class RTKStatus(Enum):
|
|
|
50
52
|
UNKNOWN = 6
|
|
51
53
|
|
|
52
54
|
@staticmethod
|
|
53
|
-
def from_value(value: int):
|
|
55
|
+
def from_value(value: int) -> "RTKStatus":
|
|
54
56
|
if value == 0:
|
|
55
57
|
return RTKStatus.NONE
|
|
56
58
|
elif value == 1 or value == 2:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
2
|
import logging
|
|
3
3
|
|
|
4
4
|
logger = logging.getLogger(__name__)
|
|
@@ -8,7 +8,7 @@ logger = logging.getLogger(__name__)
|
|
|
8
8
|
class GenerateRouteInformation:
|
|
9
9
|
"""Creates a model for generating route information and mowing plan before starting a job."""
|
|
10
10
|
|
|
11
|
-
one_hashs: list[int] = list
|
|
11
|
+
one_hashs: list[int] = field(default_factory=list)
|
|
12
12
|
job_mode: int = 4 # taskMode
|
|
13
13
|
job_version: int = 0
|
|
14
14
|
job_id: int = 0
|