pymammotion 0.5.69__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.
Files changed (154) hide show
  1. pymammotion/__init__.py +53 -0
  2. pymammotion/agora/__init__.py +0 -0
  3. pymammotion/agora/agora_api.py +755 -0
  4. pymammotion/agora/agora_rtc_capabilities.py +748 -0
  5. pymammotion/agora/agora_websockets.py +1175 -0
  6. pymammotion/aliyun/__init__.py +1 -0
  7. pymammotion/aliyun/client.py +235 -0
  8. pymammotion/aliyun/cloud_gateway.py +982 -0
  9. pymammotion/aliyun/model/aep_response.py +21 -0
  10. pymammotion/aliyun/model/connect_response.py +51 -0
  11. pymammotion/aliyun/model/dev_by_account_response.py +195 -0
  12. pymammotion/aliyun/model/login_by_oauth_response.py +64 -0
  13. pymammotion/aliyun/model/regions_response.py +29 -0
  14. pymammotion/aliyun/model/session_by_authcode_response.py +19 -0
  15. pymammotion/aliyun/model/thing_response.py +12 -0
  16. pymammotion/aliyun/regions.py +62 -0
  17. pymammotion/aliyun/tea/core.py +297 -0
  18. pymammotion/aliyun/tmp_constant.py +171 -0
  19. pymammotion/bluetooth/__init__.py +1 -0
  20. pymammotion/bluetooth/ble.py +62 -0
  21. pymammotion/bluetooth/ble_message.py +676 -0
  22. pymammotion/bluetooth/const.py +27 -0
  23. pymammotion/bluetooth/data/__init__.py +0 -0
  24. pymammotion/bluetooth/data/convert.py +25 -0
  25. pymammotion/bluetooth/data/framectrldata.py +40 -0
  26. pymammotion/bluetooth/data/notifydata.py +62 -0
  27. pymammotion/bluetooth/model/__init__.py +0 -0
  28. pymammotion/bluetooth/model/atomic_integer.py +54 -0
  29. pymammotion/const.py +13 -0
  30. pymammotion/data/__init__.py +0 -0
  31. pymammotion/data/model/__init__.py +8 -0
  32. pymammotion/data/model/account.py +8 -0
  33. pymammotion/data/model/device.py +192 -0
  34. pymammotion/data/model/device_config.py +72 -0
  35. pymammotion/data/model/device_info.py +60 -0
  36. pymammotion/data/model/device_limits.py +49 -0
  37. pymammotion/data/model/enums.py +77 -0
  38. pymammotion/data/model/errors.py +12 -0
  39. pymammotion/data/model/events.py +14 -0
  40. pymammotion/data/model/generate_geojson.py +565 -0
  41. pymammotion/data/model/generate_route_information.py +26 -0
  42. pymammotion/data/model/hash_list.py +475 -0
  43. pymammotion/data/model/location.py +36 -0
  44. pymammotion/data/model/mowing_modes.py +77 -0
  45. pymammotion/data/model/rapid_state.py +45 -0
  46. pymammotion/data/model/raw_data.py +215 -0
  47. pymammotion/data/model/region_data.py +102 -0
  48. pymammotion/data/model/report_info.py +182 -0
  49. pymammotion/data/model/work.py +27 -0
  50. pymammotion/data/mower_state_manager.py +369 -0
  51. pymammotion/data/mqtt/__init__.py +1 -0
  52. pymammotion/data/mqtt/event.py +227 -0
  53. pymammotion/data/mqtt/mammotion_properties.py +276 -0
  54. pymammotion/data/mqtt/properties.py +203 -0
  55. pymammotion/data/mqtt/status.py +57 -0
  56. pymammotion/event/__init__.py +6 -0
  57. pymammotion/event/event.py +96 -0
  58. pymammotion/homeassistant/__init__.py +3 -0
  59. pymammotion/homeassistant/mower_api.py +514 -0
  60. pymammotion/homeassistant/rtk_api.py +54 -0
  61. pymammotion/http/__init__.py +0 -0
  62. pymammotion/http/encryption.py +220 -0
  63. pymammotion/http/http.py +673 -0
  64. pymammotion/http/model/__init__.py +0 -0
  65. pymammotion/http/model/camera_stream.py +31 -0
  66. pymammotion/http/model/http.py +249 -0
  67. pymammotion/http/model/response_factory.py +61 -0
  68. pymammotion/http/model/rtk.py +16 -0
  69. pymammotion/mammotion/__init__.py +0 -0
  70. pymammotion/mammotion/commands/__init__.py +0 -0
  71. pymammotion/mammotion/commands/abstract_message.py +24 -0
  72. pymammotion/mammotion/commands/mammotion_command.py +81 -0
  73. pymammotion/mammotion/commands/messages/__init__.py +0 -0
  74. pymammotion/mammotion/commands/messages/basestation.py +43 -0
  75. pymammotion/mammotion/commands/messages/driver.py +122 -0
  76. pymammotion/mammotion/commands/messages/media.py +87 -0
  77. pymammotion/mammotion/commands/messages/navigation.py +564 -0
  78. pymammotion/mammotion/commands/messages/network.py +205 -0
  79. pymammotion/mammotion/commands/messages/ota.py +38 -0
  80. pymammotion/mammotion/commands/messages/system.py +330 -0
  81. pymammotion/mammotion/commands/messages/video.py +33 -0
  82. pymammotion/mammotion/control/__init__.py +0 -0
  83. pymammotion/mammotion/control/joystick.py +145 -0
  84. pymammotion/mammotion/devices/__init__.py +29 -0
  85. pymammotion/mammotion/devices/base.py +163 -0
  86. pymammotion/mammotion/devices/mammotion.py +571 -0
  87. pymammotion/mammotion/devices/mammotion_bluetooth.py +496 -0
  88. pymammotion/mammotion/devices/mammotion_cloud.py +355 -0
  89. pymammotion/mammotion/devices/mammotion_mower_ble.py +48 -0
  90. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  91. pymammotion/mammotion/devices/managers/managers.py +81 -0
  92. pymammotion/mammotion/devices/mower_device.py +120 -0
  93. pymammotion/mammotion/devices/mower_manager.py +107 -0
  94. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  95. pymammotion/mammotion/devices/rtk_cloud.py +115 -0
  96. pymammotion/mammotion/devices/rtk_device.py +50 -0
  97. pymammotion/mammotion/devices/rtk_manager.py +125 -0
  98. pymammotion/mqtt/__init__.py +6 -0
  99. pymammotion/mqtt/aliyun_mqtt.py +237 -0
  100. pymammotion/mqtt/linkkit/__init__.py +5 -0
  101. pymammotion/mqtt/linkkit/h2client.py +585 -0
  102. pymammotion/mqtt/linkkit/linkkit.py +3025 -0
  103. pymammotion/mqtt/mammotion_future.py +26 -0
  104. pymammotion/mqtt/mammotion_mqtt.py +214 -0
  105. pymammotion/mqtt/mqtt_models.py +66 -0
  106. pymammotion/proto/__init__.py +4841 -0
  107. pymammotion/proto/basestation.proto +51 -0
  108. pymammotion/proto/basestation_pb2.py +35 -0
  109. pymammotion/proto/basestation_pb2.pyi +89 -0
  110. pymammotion/proto/common.proto +7 -0
  111. pymammotion/proto/common_pb2.py +25 -0
  112. pymammotion/proto/common_pb2.pyi +13 -0
  113. pymammotion/proto/dev_net.proto +321 -0
  114. pymammotion/proto/dev_net_pb2.py +111 -0
  115. pymammotion/proto/dev_net_pb2.pyi +515 -0
  116. pymammotion/proto/luba_msg.proto +76 -0
  117. pymammotion/proto/luba_msg_pb2.py +41 -0
  118. pymammotion/proto/luba_msg_pb2.pyi +97 -0
  119. pymammotion/proto/luba_mul.proto +129 -0
  120. pymammotion/proto/luba_mul_pb2.py +61 -0
  121. pymammotion/proto/luba_mul_pb2.pyi +178 -0
  122. pymammotion/proto/mctrl_driver.proto +107 -0
  123. pymammotion/proto/mctrl_driver_pb2.py +57 -0
  124. pymammotion/proto/mctrl_driver_pb2.pyi +167 -0
  125. pymammotion/proto/mctrl_nav.proto +591 -0
  126. pymammotion/proto/mctrl_nav_pb2.py +136 -0
  127. pymammotion/proto/mctrl_nav_pb2.pyi +1067 -0
  128. pymammotion/proto/mctrl_ota.proto +80 -0
  129. pymammotion/proto/mctrl_ota_pb2.py +45 -0
  130. pymammotion/proto/mctrl_ota_pb2.pyi +128 -0
  131. pymammotion/proto/mctrl_pept.proto +34 -0
  132. pymammotion/proto/mctrl_pept_pb2.py +33 -0
  133. pymammotion/proto/mctrl_pept_pb2.pyi +58 -0
  134. pymammotion/proto/mctrl_sys.proto +741 -0
  135. pymammotion/proto/mctrl_sys_pb2.py +206 -0
  136. pymammotion/proto/mctrl_sys_pb2.pyi +1213 -0
  137. pymammotion/proto/message_pool.py +3 -0
  138. pymammotion/proto/py.typed +0 -0
  139. pymammotion/py.typed +0 -0
  140. pymammotion/utility/constant/__init__.py +3 -0
  141. pymammotion/utility/constant/device_constant.py +315 -0
  142. pymammotion/utility/conversions.py +5 -0
  143. pymammotion/utility/datatype_converter.py +124 -0
  144. pymammotion/utility/device_config.py +755 -0
  145. pymammotion/utility/device_type.py +489 -0
  146. pymammotion/utility/map.py +259 -0
  147. pymammotion/utility/movement.py +18 -0
  148. pymammotion/utility/mur_mur_hash.py +159 -0
  149. pymammotion/utility/periodic.py +106 -0
  150. pymammotion/utility/rocker_util.py +194 -0
  151. pymammotion-0.5.69.dist-info/METADATA +93 -0
  152. pymammotion-0.5.69.dist-info/RECORD +154 -0
  153. pymammotion-0.5.69.dist-info/WHEEL +4 -0
  154. pymammotion-0.5.69.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,26 @@
1
+ from asyncio import Future
2
+
3
+ import async_timeout
4
+
5
+
6
+ class MammotionFuture:
7
+ """Create futures for each MQTT Message."""
8
+
9
+ def __init__(self, iot_id) -> None:
10
+ self.iot_id = iot_id
11
+ self.fut: Future = Future()
12
+ self.loop = self.fut.get_loop()
13
+
14
+ def _resolve(self, item: bytes) -> None:
15
+ if not self.fut.cancelled():
16
+ self.fut.set_result(item)
17
+
18
+ def resolve(self, item: bytes) -> None:
19
+ self.loop.call_soon_threadsafe(self._resolve, item)
20
+
21
+ async def async_get(self, timeout: float | int) -> bytes:
22
+ try:
23
+ async with async_timeout.timeout(timeout):
24
+ return await self.fut
25
+ finally:
26
+ self.fut.cancel()
@@ -0,0 +1,214 @@
1
+ import asyncio
2
+ from collections.abc import Awaitable, Callable
3
+ import json
4
+ import logging
5
+ import ssl
6
+ from typing import Any
7
+ from urllib.parse import urlparse
8
+
9
+ import paho.mqtt.client as mqtt
10
+ from paho.mqtt.properties import Properties
11
+ from paho.mqtt.reasoncodes import ReasonCode
12
+
13
+ from pymammotion import MammotionHTTP
14
+ from pymammotion.http.model.http import DeviceRecord, MQTTConnection, Response, UnauthorizedException
15
+ from pymammotion.utility.datatype_converter import DatatypeConverter
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class MammotionMQTT:
21
+ """Mammotion MQTT Client."""
22
+
23
+ converter = DatatypeConverter()
24
+
25
+ def __init__(
26
+ self, mqtt_connection: MQTTConnection, mammotion_http: MammotionHTTP, records: list[DeviceRecord]
27
+ ) -> None:
28
+ self.on_connected: Callable[[], Awaitable[None]] | None = None
29
+ self.on_ready: Callable[[], Awaitable[None]] | None = None
30
+ self.on_error: Callable[[str], Awaitable[None]] | None = None
31
+ self.on_disconnected: Callable[[], Awaitable[None]] | None = None
32
+ self.on_message: Callable[[str, bytes, str], Awaitable[None]] | None = None
33
+ self.loop = asyncio.get_running_loop()
34
+ self.mammotion_http = mammotion_http
35
+ self.mqtt_connection = mqtt_connection
36
+ self.client = self.build(mqtt_connection)
37
+
38
+ self.records = records
39
+
40
+ # wire callbacks from the service object if present
41
+ self.client.on_connect = self._on_connect
42
+ self.client.on_message = self._on_message
43
+ self.client.on_disconnect = self._on_disconnect
44
+ # client.on_subscribe = getattr(mqtt_service_obj, "on_subscribe", None)
45
+ # client.on_publish = getattr(mqtt_service_obj, "on_publish", None)
46
+
47
+ def __del__(self) -> None:
48
+ if self.client.is_connected():
49
+ for record in self.records:
50
+ self.unsubscribe_all(record.product_key, record.device_name)
51
+ self.client.disconnect()
52
+
53
+ def connect_async(self) -> None:
54
+ """Connect async to MQTT Server."""
55
+ if not self.client.is_connected():
56
+ logger.info("Connecting...")
57
+ self.client.connect_async(host=self.client.host, port=self.client.port, keepalive=self.client.keepalive)
58
+ self.client.loop_start()
59
+
60
+ def disconnect(self) -> None:
61
+ """Disconnect from MQTT Server."""
62
+ logger.info("Disconnecting...")
63
+ self.client.disconnect()
64
+
65
+ @staticmethod
66
+ def build(mqtt_connection: MQTTConnection, keepalive: int = 60, timeout: int = 30) -> mqtt.Client:
67
+ """get_jwt_response: object with attributes .client_id, .username, .jwt (password), .host (e.g. 'mqtts://broker:8883' or 'broker:1883' or 'broker').
68
+ mqtt_service_obj: object that exposes callback methods (on_connect, on_message, on_disconnect, etc.)
69
+ Returns: (client, connected_bool, rc)
70
+ """
71
+ host = mqtt_connection.host
72
+ # Ensure urlparse can parse plain hosts
73
+ parsed = urlparse(host if "://" in host else "tcp://" + host)
74
+ scheme = parsed.scheme
75
+ hostname = parsed.hostname
76
+ port = parsed.port
77
+
78
+ # decide TLS/ssl and default port
79
+ use_ssl = scheme in ("mqtts", "ssl")
80
+ if port is None:
81
+ port = 8883 if use_ssl else 1883
82
+
83
+ client = mqtt.Client(
84
+ callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
85
+ client_id=mqtt_connection.client_id,
86
+ clean_session=True,
87
+ protocol=mqtt.MQTTv311,
88
+ )
89
+
90
+ client.username_pw_set(mqtt_connection.username, mqtt_connection.jwt)
91
+
92
+ if use_ssl:
93
+ # use system default CA certs; adjust tls_set() params if custom CA/client certs required
94
+ client.tls_set(cert_reqs=ssl.CERT_REQUIRED)
95
+ client.tls_insecure_set(False)
96
+
97
+ # automatic reconnect backoff
98
+ client.reconnect_delay_set(min_delay=1, max_delay=120)
99
+
100
+ # connect (synchronous connect attempt) and start background loop
101
+ if hostname:
102
+ client.host = hostname
103
+ client.port = port
104
+ client.keepalive = keepalive
105
+
106
+ return client
107
+
108
+ def _on_message(self, _client: mqtt.Client, _userdata: Any, message: mqtt.MQTTMessage) -> None:
109
+ """Is called when message is received."""
110
+ logger.debug("Message on topic %s", message.topic)
111
+ logger.debug(message)
112
+
113
+ if self.on_message is not None:
114
+ iot_id = None
115
+ # Parse the topic path to get product_key and device_name
116
+ topic_parts = message.topic.split("/")
117
+ if len(topic_parts) >= 4:
118
+ product_key = topic_parts[2]
119
+ device_name = topic_parts[3]
120
+
121
+ # Filter records to find matching device
122
+ filtered_records = [
123
+ record
124
+ for record in self.records
125
+ if record.product_key == product_key and record.device_name == device_name
126
+ ]
127
+
128
+ if filtered_records:
129
+ iot_id = filtered_records[0].iot_id
130
+ payload = json.loads(message.payload.decode("utf-8"))
131
+ payload["iot_id"] = iot_id
132
+ payload["product_key"] = product_key
133
+ payload["device_name"] = device_name
134
+ message.payload = json.dumps(payload).encode("utf-8")
135
+
136
+ if iot_id:
137
+ future = asyncio.run_coroutine_threadsafe(
138
+ self.on_message(message.topic, message.payload, iot_id), self.loop
139
+ )
140
+ asyncio.wrap_future(future, loop=self.loop)
141
+
142
+ def _on_connect(
143
+ self,
144
+ _client: mqtt.Client,
145
+ user_data: Any,
146
+ session_flag: mqtt.ConnectFlags,
147
+ rc: ReasonCode,
148
+ properties: Properties | None,
149
+ ) -> None:
150
+ """Handle connection event and execute callback if set."""
151
+ self.is_connected = True
152
+ for record in self.records:
153
+ self.subscribe_all(record.product_key, record.device_name)
154
+ if self.on_connected is not None:
155
+ future = asyncio.run_coroutine_threadsafe(self.on_connected(), self.loop)
156
+ asyncio.wrap_future(future, loop=self.loop)
157
+
158
+ if self.on_ready:
159
+ self.is_ready = True
160
+ future = asyncio.run_coroutine_threadsafe(self.on_ready(), self.loop)
161
+ asyncio.wrap_future(future, loop=self.loop)
162
+
163
+ logger.debug("on_connect, session_flag:%s, rc:%s", session_flag, rc)
164
+
165
+ def _on_disconnect(
166
+ self,
167
+ _client: mqtt.Client,
168
+ user_data: Any | None,
169
+ disconnect_flags: mqtt.DisconnectFlags,
170
+ rc: ReasonCode,
171
+ properties: Properties | None,
172
+ **kwargs: Any,
173
+ ) -> None:
174
+ """Handle disconnection event and execute callback if set."""
175
+ self.is_connected = False
176
+ if self.on_disconnected is not None:
177
+ for record in self.records:
178
+ self.unsubscribe_all(record.product_key, record.device_name)
179
+ future = asyncio.run_coroutine_threadsafe(self.on_disconnected(), self.loop)
180
+ asyncio.wrap_future(future, loop=self.loop)
181
+
182
+ logger.debug("on_disconnect, rc:%s", rc)
183
+
184
+ def subscribe_all(self, product_key: str, device_name: str) -> None:
185
+ """Subscribe to all topics for the given device."""
186
+
187
+ # "/sys/" + this.$productKey + "/" + this.$deviceName + "/thing/event/+/post"
188
+ # "/sys/proto/" + this.$productKey + "/" + this.$deviceName + "/thing/event/+/post"
189
+ # "/sys/" + this.$productKey + "/" + this.$deviceName + "/app/down/thing/status"
190
+ self.client.subscribe(f"/sys/{product_key}/{device_name}/app/down/thing/status")
191
+ self.client.subscribe(f"/sys/{product_key}/{device_name}/thing/event/+/post")
192
+ self.client.subscribe(f"/sys/proto/{product_key}/{device_name}/thing/event/+/post")
193
+
194
+ def unsubscribe_all(self, product_key: str, device_name: str) -> None:
195
+ """Unsubscribe from all topics for the given device."""
196
+ self.client.unsubscribe(f"/sys/{product_key}/{device_name}/app/down/thing/status")
197
+ self.client.unsubscribe(f"/sys/{product_key}/{device_name}/thing/event/+/post")
198
+ self.client.unsubscribe(f"/sys/proto/{product_key}/{device_name}/thing/event/+/post")
199
+
200
+ async def send_cloud_command(self, iot_id: str, command: bytes) -> str:
201
+ """Send command to cloud."""
202
+ res: Response[dict] = await self.mammotion_http.mqtt_invoke(
203
+ self.converter.printBase64Binary(command), "", iot_id
204
+ )
205
+
206
+ logger.debug("send_cloud_command: %s", res)
207
+
208
+ if res.code == 500:
209
+ return res.msg
210
+
211
+ if res.code == 401:
212
+ raise UnauthorizedException(res.msg)
213
+
214
+ return str(res.data["result"])
@@ -0,0 +1,66 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any
3
+
4
+ from mashumaro.mixins.orjson import DataClassORJSONMixin
5
+
6
+
7
+ @dataclass
8
+ class TopicProperty(DataClassORJSONMixin):
9
+ """TopicProperty."""
10
+
11
+ id: str = ""
12
+ method: str = ""
13
+ params: dict[str, Any] | None = None
14
+ sys: dict[str, Any] | None = None
15
+ time: int = 0
16
+ version: str = ""
17
+
18
+
19
+ @dataclass
20
+ class TopicDeviceStatus(DataClassORJSONMixin):
21
+ """TopicDeviceStatus."""
22
+
23
+ gmt_create: int = 0
24
+ action: str = ""
25
+ product_key: str = ""
26
+ device_name: str = ""
27
+ iot_id: str = ""
28
+
29
+
30
+ class TopicUtils:
31
+ """Utility helpers ported from the Java TopicUtils."""
32
+
33
+ @staticmethod
34
+ def _split_topic(topic: str) -> list[str]:
35
+ if topic is None:
36
+ raise ValueError("topic must not be None")
37
+ # preserve empty segments (leading/trailing slashes)
38
+ return topic.split("/")
39
+
40
+ @staticmethod
41
+ def get_device_name(topic: str) -> str:
42
+ parts = TopicUtils._split_topic(topic)
43
+ # original code expects the device name at index 3 when topic is like:
44
+ # /sys/{productKey}/{deviceName}/...
45
+ try:
46
+ return parts[3]
47
+ except IndexError:
48
+ return ""
49
+
50
+ @staticmethod
51
+ def get_identifier(topic: str) -> str:
52
+ if "property" in (topic or ""):
53
+ return ""
54
+ parts = TopicUtils._split_topic(topic)
55
+ if len(parts) < 2:
56
+ return ""
57
+ # second-last element (may be empty if trailing slash)
58
+ return parts[-2] or ""
59
+
60
+ @staticmethod
61
+ def get_method(topic: str) -> str:
62
+ """Get the method from a topic."""
63
+ parts = TopicUtils._split_topic(topic)
64
+ if len(parts) >= 2:
65
+ return f"/{parts[-2]}/{parts[-1]}"
66
+ return ""