pymammotion 0.5.34__py3-none-any.whl → 0.5.53__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 (63) hide show
  1. pymammotion/__init__.py +3 -3
  2. pymammotion/aliyun/cloud_gateway.py +106 -18
  3. pymammotion/aliyun/model/dev_by_account_response.py +169 -21
  4. pymammotion/const.py +3 -0
  5. pymammotion/data/model/device.py +22 -9
  6. pymammotion/data/model/device_config.py +1 -1
  7. pymammotion/data/model/device_info.py +1 -0
  8. pymammotion/data/model/enums.py +5 -3
  9. pymammotion/data/model/events.py +14 -0
  10. pymammotion/data/model/generate_geojson.py +551 -0
  11. pymammotion/data/model/generate_route_information.py +2 -2
  12. pymammotion/data/model/hash_list.py +129 -33
  13. pymammotion/data/model/location.py +4 -4
  14. pymammotion/data/model/region_data.py +4 -4
  15. pymammotion/data/model/report_info.py +7 -0
  16. pymammotion/data/{state_manager.py → mower_state_manager.py} +75 -11
  17. pymammotion/data/mqtt/event.py +47 -22
  18. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  19. pymammotion/data/mqtt/properties.py +32 -29
  20. pymammotion/data/mqtt/status.py +17 -16
  21. pymammotion/event/event.py +5 -2
  22. pymammotion/homeassistant/__init__.py +3 -0
  23. pymammotion/homeassistant/mower_api.py +484 -0
  24. pymammotion/homeassistant/rtk_api.py +54 -0
  25. pymammotion/http/http.py +394 -14
  26. pymammotion/http/model/http.py +82 -2
  27. pymammotion/http/model/response_factory.py +10 -4
  28. pymammotion/mammotion/commands/mammotion_command.py +6 -0
  29. pymammotion/mammotion/commands/messages/navigation.py +39 -6
  30. pymammotion/mammotion/devices/__init__.py +27 -3
  31. pymammotion/mammotion/devices/base.py +16 -138
  32. pymammotion/mammotion/devices/mammotion.py +369 -200
  33. pymammotion/mammotion/devices/mammotion_bluetooth.py +7 -5
  34. pymammotion/mammotion/devices/mammotion_cloud.py +42 -83
  35. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  36. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  37. pymammotion/mammotion/devices/managers/managers.py +81 -0
  38. pymammotion/mammotion/devices/mower_device.py +124 -0
  39. pymammotion/mammotion/devices/mower_manager.py +107 -0
  40. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  41. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  42. pymammotion/mammotion/devices/rtk_device.py +50 -0
  43. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  44. pymammotion/mqtt/__init__.py +2 -1
  45. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  46. pymammotion/mqtt/mammotion_mqtt.py +174 -192
  47. pymammotion/mqtt/mqtt_models.py +66 -0
  48. pymammotion/proto/__init__.py +3 -3
  49. pymammotion/proto/mctrl_nav.proto +1 -1
  50. pymammotion/proto/mctrl_nav_pb2.py +1 -1
  51. pymammotion/proto/mctrl_nav_pb2.pyi +4 -4
  52. pymammotion/proto/mctrl_sys.proto +1 -1
  53. pymammotion/proto/mctrl_sys_pb2.py +1 -1
  54. pymammotion/utility/constant/device_constant.py +1 -1
  55. pymammotion/utility/datatype_converter.py +13 -12
  56. pymammotion/utility/device_type.py +88 -3
  57. pymammotion/utility/map.py +238 -51
  58. pymammotion/utility/mur_mur_hash.py +132 -87
  59. {pymammotion-0.5.34.dist-info → pymammotion-0.5.53.dist-info}/METADATA +26 -31
  60. {pymammotion-0.5.34.dist-info → pymammotion-0.5.53.dist-info}/RECORD +67 -51
  61. {pymammotion-0.5.34.dist-info → pymammotion-0.5.53.dist-info}/WHEEL +1 -1
  62. pymammotion/http/_init_.py +0 -0
  63. {pymammotion-0.5.34.dist-info → pymammotion-0.5.53.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 "",
@@ -22,7 +22,7 @@ from Tea.exceptions import UnretryableException
22
22
  from pymammotion.aliyun.client import Client
23
23
  from pymammotion.aliyun.model.aep_response import AepResponse
24
24
  from pymammotion.aliyun.model.connect_response import ConnectResponse
25
- from pymammotion.aliyun.model.dev_by_account_response import ListingDevByAccountResponse
25
+ from pymammotion.aliyun.model.dev_by_account_response import ListingDevAccountResponse
26
26
  from pymammotion.aliyun.model.login_by_oauth_response import LoginByOAuthResponse
27
27
  from pymammotion.aliyun.model.regions_response import RegionResponse
28
28
  from pymammotion.aliyun.model.session_by_authcode_response import SessionByAuthCodeResponse
@@ -103,6 +103,9 @@ class CheckSessionException(Exception):
103
103
  """Raise exception when checking session results in a failure."""
104
104
 
105
105
 
106
+ EXPIRED_CREDENTIAL_EXCEPTIONS = (CheckSessionException, SetupException)
107
+
108
+
106
109
  class CloudIOTGateway:
107
110
  """Class for interacting with Aliyun Cloud IoT Gateway."""
108
111
 
@@ -120,7 +123,7 @@ class CloudIOTGateway:
120
123
  aep_response: AepResponse | None = None,
121
124
  session_by_authcode_response: SessionByAuthCodeResponse | None = None,
122
125
  region_response: RegionResponse | None = None,
123
- dev_by_account: ListingDevByAccountResponse | None = None,
126
+ dev_by_account: ListingDevAccountResponse | None = None,
124
127
  ) -> None:
125
128
  """Initialize the CloudIOTGateway."""
126
129
  self.mammotion_http: MammotionHTTP = mammotion_http
@@ -146,7 +149,7 @@ class CloudIOTGateway:
146
149
  )
147
150
 
148
151
  @staticmethod
149
- def generate_random_string(length: int):
152
+ def generate_random_string(length: int) -> str:
150
153
  """Generate a random string of specified length."""
151
154
  characters = string.ascii_letters + string.digits
152
155
  return "".join(random.choice(characters) for _ in range(length))
@@ -165,7 +168,7 @@ class CloudIOTGateway:
165
168
  logger.error("Couldn't decode message %s", response_body_str)
166
169
  return {"code": 22000}
167
170
 
168
- def sign(self, data):
171
+ def sign(self, data: dict) -> str:
169
172
  """Generate signature for the given data."""
170
173
  keys = ["appKey", "clientId", "deviceSn", "timestamp"]
171
174
  concatenated_str = ""
@@ -180,7 +183,7 @@ class CloudIOTGateway:
180
183
  hashlib.sha1,
181
184
  ).hexdigest()
182
185
 
183
- async def get_region(self, country_code: str):
186
+ async def get_region(self, country_code: str) -> RegionResponse:
184
187
  """Get the region based on country code and auth code."""
185
188
  auth_code = self.mammotion_http.login_info.authorization_code
186
189
 
@@ -244,7 +247,7 @@ class CloudIOTGateway:
244
247
 
245
248
  return response.body
246
249
 
247
- async def aep_handle(self):
250
+ async def aep_handle(self) -> AepResponse:
248
251
  """Handle AEP authentication."""
249
252
  aep_domain = self.domain
250
253
 
@@ -300,9 +303,9 @@ class CloudIOTGateway:
300
303
 
301
304
  logger.debug(response_body_dict)
302
305
 
303
- return response.body
306
+ return self._aep_response
304
307
 
305
- async def connect(self):
308
+ async def connect(self) -> ConnectResponse:
306
309
  """Connect to the Aliyun Cloud IoT Gateway."""
307
310
  region_url = "sdk.openaccount.aliyun.com"
308
311
  time_now = time.time()
@@ -449,7 +452,7 @@ class CloudIOTGateway:
449
452
  return self._login_by_oauth_response
450
453
  raise LoginException(data)
451
454
 
452
- async def session_by_auth_code(self):
455
+ async def session_by_auth_code(self) -> SessionByAuthCodeResponse:
453
456
  """Create a session by auth code."""
454
457
  config = Config(
455
458
  app_key=self._app_key,
@@ -608,7 +611,8 @@ class CloudIOTGateway:
608
611
  session_data = session.data
609
612
 
610
613
  if (
611
- session_data.identityId is None
614
+ session_data is None
615
+ or session_data.identityId is None
612
616
  or session_data.refreshTokenExpire is None
613
617
  or session_data.iotToken is None
614
618
  or session_data.iotTokenExpire is None
@@ -619,7 +623,7 @@ class CloudIOTGateway:
619
623
  self._session_by_authcode_response = session
620
624
  self._iot_token_issued_at = int(time.time())
621
625
 
622
- async def list_binding_by_account(self) -> ListingDevByAccountResponse:
626
+ async def list_binding_by_account(self) -> ListingDevAccountResponse:
623
627
  """List bindings by account."""
624
628
  config = Config(
625
629
  app_key=self._app_key,
@@ -658,9 +662,9 @@ class CloudIOTGateway:
658
662
  response_body_dict = self.parse_json_response(response_body_str)
659
663
 
660
664
  if int(response_body_dict.get("code")) != 200:
661
- raise Exception("Error in creating session: " + response_body_dict["msg"])
665
+ raise Exception("Error in creating session: " + response_body_dict["message"])
662
666
 
663
- self._devices_by_account_response = ListingDevByAccountResponse.from_dict(response_body_dict)
667
+ self._devices_by_account_response = ListingDevAccountResponse.from_dict(response_body_dict)
664
668
  return self._devices_by_account_response
665
669
 
666
670
  async def list_binding_by_dev(self, iot_id: str):
@@ -698,10 +702,94 @@ class CloudIOTGateway:
698
702
  # Load the JSON string into a dictionary
699
703
  response_body_dict = self.parse_json_response(response_body_str)
700
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
+
701
789
  if int(response_body_dict.get("code")) != 200:
702
790
  raise Exception("Error in creating session: " + response_body_dict["msg"])
703
791
 
704
- self._devices_by_account_response = ListingDevByAccountResponse.from_dict(response_body_dict)
792
+ self._devices_by_account_response = ListingDevAccountResponse.from_dict(response_body_dict)
705
793
  return self._devices_by_account_response
706
794
 
707
795
  async def send_cloud_command(self, iot_id: str, command: bytes) -> str:
@@ -867,11 +955,11 @@ class CloudIOTGateway:
867
955
  self.mammotion_http = mammotion_http
868
956
 
869
957
  @property
870
- def region_response(self) -> RegionResponse:
958
+ def region_response(self) -> RegionResponse | None:
871
959
  return self._region_response
872
960
 
873
961
  @property
874
- def aep_response(self) -> AepResponse:
962
+ def aep_response(self) -> AepResponse | None:
875
963
  return self._aep_response
876
964
 
877
965
  @property
@@ -883,9 +971,9 @@ class CloudIOTGateway:
883
971
  return self._client_id
884
972
 
885
973
  @property
886
- def login_by_oauth_response(self) -> LoginByOAuthResponse:
974
+ def login_by_oauth_response(self) -> LoginByOAuthResponse | None:
887
975
  return self._login_by_oauth_response
888
976
 
889
977
  @property
890
- def connect_response(self) -> ConnectResponse:
978
+ def connect_response(self) -> ConnectResponse | None:
891
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"
@@ -8,6 +8,7 @@ from mashumaro.mixins.orjson import DataClassORJSONMixin
8
8
  from pymammotion.data.model import HashList, RapidState
9
9
  from pymammotion.data.model.device_info import DeviceFirmwares, DeviceNonWorkingHours, MowerInfo
10
10
  from pymammotion.data.model.errors import DeviceErrors
11
+ from pymammotion.data.model.events import Events
11
12
  from pymammotion.data.model.location import Location
12
13
  from pymammotion.data.model.report_info import ReportData
13
14
  from pymammotion.data.model.work import CurrentTaskSettings
@@ -41,6 +42,7 @@ class MowingDevice(DataClassORJSONMixin):
41
42
  device_firmwares: DeviceFirmwares = field(default_factory=DeviceFirmwares)
42
43
  errors: DeviceErrors = field(default_factory=DeviceErrors)
43
44
  non_work_hours: DeviceNonWorkingHours = field(default_factory=DeviceNonWorkingHours)
45
+ events: Events = field(default_factory=Events)
44
46
 
45
47
  def buffer(self, buffer_list: SystemUpdateBufMsg) -> None:
46
48
  """Update the device based on which buffer we are reading from."""
@@ -51,9 +53,11 @@ class MowingDevice(DataClassORJSONMixin):
51
53
  self.location.RTK.latitude = parse_double(buffer_list.update_buf_data[5], 8.0)
52
54
  self.location.RTK.longitude = parse_double(buffer_list.update_buf_data[6], 8.0)
53
55
  if buffer_list.update_buf_data[7] != 0:
54
- self.location.dock.latitude = parse_double(buffer_list.update_buf_data[7], 4.0)
55
- self.location.dock.longitude = parse_double(buffer_list.update_buf_data[8], 4.0)
56
- self.location.dock.rotation = buffer_list.update_buf_data[3] + 180
56
+ # latitude Y longitude X
57
+ self.location.dock.longitude = parse_double(buffer_list.update_buf_data[7], 4.0)
58
+ self.location.dock.latitude = parse_double(buffer_list.update_buf_data[8], 4.0)
59
+ self.location.dock.rotation = buffer_list.update_buf_data[3]
60
+
57
61
  case 2:
58
62
  self.errors.err_code_list.clear()
59
63
  self.errors.err_code_list_time.clear()
@@ -85,6 +89,20 @@ class MowingDevice(DataClassORJSONMixin):
85
89
  buffer_list.update_buf_data[22],
86
90
  ]
87
91
  )
92
+ case 3:
93
+ # task state event
94
+ task_area_map: dict[int, int] = {}
95
+ task_area_ids = []
96
+
97
+ for i in range(3, len(buffer_list.update_buf_data), 2):
98
+ area_id = buffer_list.update_buf_data[i]
99
+
100
+ if area_id != 0:
101
+ area_value = int(buffer_list.update_buf_data[i + 1])
102
+ task_area_map[area_id] = area_value
103
+ task_area_ids.append(area_id)
104
+ self.events.work_tasks_event.hash_list = task_area_map
105
+ self.events.work_tasks_event.ids = task_area_ids
88
106
 
89
107
  def update_report_data(self, toapp_report_data: ReportInfoData) -> None:
90
108
  """Set report data for the mower."""
@@ -96,16 +114,11 @@ class MowingDevice(DataClassORJSONMixin):
96
114
  self.location.device = coordinate_converter.enu_to_lla(
97
115
  parse_double(location.real_pos_y, 4.0), parse_double(location.real_pos_x, 4.0)
98
116
  )
117
+ self.map.invalidate_maps(location.bol_hash)
99
118
  if location.zone_hash:
100
119
  self.location.work_zone = (
101
120
  location.zone_hash if self.report_data.dev.sys_status == WorkMode.MODE_WORKING else 0
102
121
  )
103
- # if location.bol_hash:
104
- # for loc in toapp_report_data.locations:
105
- # if loc.bol_hash:
106
- # if loc.bol_hash != location.bol_hash:
107
- # self.map = HashList()
108
- # MurMurHashUtil.hash_unsigned_list(list(self.map.area.keys()))
109
122
 
110
123
  if toapp_report_data.fw_info:
111
124
  self.update_device_firmwares(toapp_report_data.fw_info)
@@ -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:
@@ -16,6 +16,7 @@ class SideLight(DataClassORJSONMixin):
16
16
 
17
17
  @dataclass
18
18
  class DeviceNonWorkingHours(DataClassORJSONMixin):
19
+ sub_cmd: int = 0
19
20
  start_time: str = ""
20
21
  end_time: str = ""
21
22
 
@@ -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:
@@ -0,0 +1,14 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from mashumaro.mixins.orjson import DataClassORJSONMixin
4
+
5
+
6
+ @dataclass
7
+ class WorkTaskEvent(DataClassORJSONMixin):
8
+ hash_area_map: dict[int, int] = field(default_factory=dict)
9
+ ids: list[int] = field(default_factory=list)
10
+
11
+
12
+ @dataclass
13
+ class Events(DataClassORJSONMixin):
14
+ work_tasks_event: WorkTaskEvent = field(default_factory=WorkTaskEvent)