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.

Files changed (59) hide show
  1. pymammotion/__init__.py +3 -3
  2. pymammotion/aliyun/client.py +5 -2
  3. pymammotion/aliyun/cloud_gateway.py +137 -20
  4. pymammotion/aliyun/model/dev_by_account_response.py +169 -21
  5. pymammotion/const.py +3 -0
  6. pymammotion/data/model/device.py +1 -0
  7. pymammotion/data/model/device_config.py +1 -1
  8. pymammotion/data/model/device_info.py +4 -0
  9. pymammotion/data/model/enums.py +5 -3
  10. pymammotion/data/model/generate_route_information.py +2 -2
  11. pymammotion/data/model/hash_list.py +113 -33
  12. pymammotion/data/model/mowing_modes.py +8 -0
  13. pymammotion/data/model/region_data.py +4 -4
  14. pymammotion/data/{state_manager.py → mower_state_manager.py} +50 -13
  15. pymammotion/data/mqtt/event.py +47 -22
  16. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  17. pymammotion/data/mqtt/properties.py +32 -29
  18. pymammotion/data/mqtt/status.py +17 -16
  19. pymammotion/homeassistant/__init__.py +3 -0
  20. pymammotion/homeassistant/mower_api.py +446 -0
  21. pymammotion/homeassistant/rtk_api.py +54 -0
  22. pymammotion/http/http.py +433 -18
  23. pymammotion/http/model/http.py +82 -2
  24. pymammotion/http/model/response_factory.py +10 -4
  25. pymammotion/mammotion/commands/mammotion_command.py +20 -0
  26. pymammotion/mammotion/commands/messages/driver.py +25 -0
  27. pymammotion/mammotion/commands/messages/navigation.py +10 -6
  28. pymammotion/mammotion/commands/messages/system.py +0 -14
  29. pymammotion/mammotion/devices/__init__.py +27 -3
  30. pymammotion/mammotion/devices/base.py +22 -146
  31. pymammotion/mammotion/devices/mammotion.py +364 -205
  32. pymammotion/mammotion/devices/mammotion_bluetooth.py +11 -8
  33. pymammotion/mammotion/devices/mammotion_cloud.py +49 -85
  34. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  35. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  36. pymammotion/mammotion/devices/managers/managers.py +81 -0
  37. pymammotion/mammotion/devices/mower_device.py +121 -0
  38. pymammotion/mammotion/devices/mower_manager.py +107 -0
  39. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  40. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  41. pymammotion/mammotion/devices/rtk_device.py +50 -0
  42. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  43. pymammotion/mqtt/__init__.py +2 -1
  44. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  45. pymammotion/mqtt/mammotion_mqtt.py +174 -192
  46. pymammotion/mqtt/mqtt_models.py +66 -0
  47. pymammotion/proto/__init__.py +2 -2
  48. pymammotion/proto/mctrl_nav.proto +2 -2
  49. pymammotion/proto/mctrl_nav_pb2.py +1 -1
  50. pymammotion/proto/mctrl_nav_pb2.pyi +4 -4
  51. pymammotion/proto/mctrl_sys.proto +1 -1
  52. pymammotion/utility/datatype_converter.py +13 -12
  53. pymammotion/utility/device_type.py +88 -3
  54. pymammotion/utility/mur_mur_hash.py +132 -87
  55. {pymammotion-0.5.21.dist-info → pymammotion-0.5.45.dist-info}/METADATA +25 -30
  56. {pymammotion-0.5.21.dist-info → pymammotion-0.5.45.dist-info}/RECORD +64 -50
  57. {pymammotion-0.5.21.dist-info → pymammotion-0.5.45.dist-info}/WHEEL +1 -1
  58. pymammotion/http/_init_.py +0 -0
  59. {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 = MammotionMQTT(
40
+ luba = AliyunMQTT(
41
41
  iot_token=IOT_TOKEN or "",
42
42
  region_id=REGION or "",
43
43
  product_key=PRODUCT_KEY or "",
@@ -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, "no"),
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, "no"),
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 ListingDevByAccountResponse
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: ListingDevByAccountResponse | None = None,
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 response.body
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.identityId is None
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) -> ListingDevByAccountResponse:
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["msg"])
665
+ raise Exception("Error in creating session: " + response_body_dict["message"])
649
666
 
650
- self._devices_by_account_response = ListingDevByAccountResponse.from_dict(response_body_dict)
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 = ListingDevByAccountResponse.from_dict(response_body_dict)
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
- gmtModified: int
10
- netType: str
11
- categoryKey: str
12
- productKey: str
13
- nodeType: str
14
- isEdgeGateway: bool
15
- deviceName: str
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
- nickName: str | None = None
26
- description: str | None = None
27
- productImage: str | None = None
28
- categoryImage: str | None = None
29
- productModel: str | None = None
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 ListingDevByAccountResponse(DataClassORJSONMixin):
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"
@@ -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: list[int] = field(default_factory=list)
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 = ""
@@ -4,9 +4,11 @@ from enum import Enum
4
4
  class ConnectionPreference(Enum):
5
5
  """Enum for connection preference."""
6
6
 
7
- EITHER = 0
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