pymammotion 0.4.0a2__py3-none-any.whl → 0.5.51__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 (133) hide show
  1. pymammotion/__init__.py +5 -4
  2. pymammotion/aliyun/client.py +235 -0
  3. pymammotion/aliyun/cloud_gateway.py +312 -64
  4. pymammotion/aliyun/model/aep_response.py +1 -2
  5. pymammotion/aliyun/model/dev_by_account_response.py +170 -23
  6. pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
  7. pymammotion/aliyun/model/regions_response.py +3 -3
  8. pymammotion/aliyun/model/session_by_authcode_response.py +2 -2
  9. pymammotion/aliyun/model/thing_response.py +12 -0
  10. pymammotion/aliyun/regions.py +62 -0
  11. pymammotion/aliyun/tea/core.py +297 -0
  12. pymammotion/bluetooth/ble.py +7 -9
  13. pymammotion/bluetooth/ble_message.py +10 -14
  14. pymammotion/const.py +3 -0
  15. pymammotion/data/model/__init__.py +1 -2
  16. pymammotion/data/model/device.py +95 -27
  17. pymammotion/data/model/device_config.py +4 -4
  18. pymammotion/data/model/device_info.py +35 -0
  19. pymammotion/data/model/device_limits.py +10 -10
  20. pymammotion/data/model/enums.py +12 -2
  21. pymammotion/data/model/errors.py +12 -0
  22. pymammotion/data/model/events.py +14 -0
  23. pymammotion/data/model/generate_geojson.py +521 -0
  24. pymammotion/data/model/generate_route_information.py +2 -2
  25. pymammotion/data/model/hash_list.py +370 -57
  26. pymammotion/data/model/location.py +4 -4
  27. pymammotion/data/model/mowing_modes.py +17 -1
  28. pymammotion/data/model/raw_data.py +2 -10
  29. pymammotion/data/model/region_data.py +10 -11
  30. pymammotion/data/model/report_info.py +31 -5
  31. pymammotion/data/model/work.py +27 -0
  32. pymammotion/data/mower_state_manager.py +316 -0
  33. pymammotion/data/mqtt/event.py +73 -28
  34. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  35. pymammotion/data/mqtt/properties.py +93 -78
  36. pymammotion/data/mqtt/status.py +18 -17
  37. pymammotion/event/event.py +27 -6
  38. pymammotion/homeassistant/__init__.py +3 -0
  39. pymammotion/homeassistant/mower_api.py +484 -0
  40. pymammotion/homeassistant/rtk_api.py +54 -0
  41. pymammotion/http/encryption.py +5 -6
  42. pymammotion/http/http.py +574 -28
  43. pymammotion/http/model/__init__.py +0 -0
  44. pymammotion/{aliyun/model/stream_subscription_response.py → http/model/camera_stream.py} +14 -2
  45. pymammotion/http/model/http.py +129 -4
  46. pymammotion/http/model/response_factory.py +61 -0
  47. pymammotion/http/model/rtk.py +16 -0
  48. pymammotion/mammotion/commands/abstract_message.py +7 -5
  49. pymammotion/mammotion/commands/mammotion_command.py +30 -1
  50. pymammotion/mammotion/commands/messages/basestation.py +43 -0
  51. pymammotion/mammotion/commands/messages/driver.py +61 -29
  52. pymammotion/mammotion/commands/messages/media.py +68 -15
  53. pymammotion/mammotion/commands/messages/navigation.py +61 -25
  54. pymammotion/mammotion/commands/messages/network.py +17 -23
  55. pymammotion/mammotion/commands/messages/ota.py +18 -18
  56. pymammotion/mammotion/commands/messages/system.py +32 -49
  57. pymammotion/mammotion/commands/messages/video.py +15 -16
  58. pymammotion/mammotion/devices/__init__.py +27 -3
  59. pymammotion/mammotion/devices/base.py +40 -131
  60. pymammotion/mammotion/devices/mammotion.py +436 -201
  61. pymammotion/mammotion/devices/mammotion_bluetooth.py +57 -47
  62. pymammotion/mammotion/devices/mammotion_cloud.py +134 -105
  63. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  64. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  65. pymammotion/mammotion/devices/managers/managers.py +81 -0
  66. pymammotion/mammotion/devices/mower_device.py +124 -0
  67. pymammotion/mammotion/devices/mower_manager.py +107 -0
  68. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  69. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  70. pymammotion/mammotion/devices/rtk_device.py +50 -0
  71. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  72. pymammotion/mqtt/__init__.py +2 -1
  73. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  74. pymammotion/mqtt/linkkit/__init__.py +5 -0
  75. pymammotion/mqtt/linkkit/h2client.py +585 -0
  76. pymammotion/mqtt/linkkit/linkkit.py +3023 -0
  77. pymammotion/mqtt/mammotion_mqtt.py +176 -169
  78. pymammotion/mqtt/mqtt_models.py +66 -0
  79. pymammotion/proto/__init__.py +4839 -4
  80. pymammotion/proto/basestation.proto +8 -0
  81. pymammotion/proto/basestation_pb2.py +11 -9
  82. pymammotion/proto/basestation_pb2.pyi +16 -2
  83. pymammotion/proto/dev_net.proto +79 -55
  84. pymammotion/proto/dev_net_pb2.py +60 -56
  85. pymammotion/proto/dev_net_pb2.pyi +49 -6
  86. pymammotion/proto/luba_msg.proto +2 -1
  87. pymammotion/proto/luba_msg_pb2.py +6 -6
  88. pymammotion/proto/luba_msg_pb2.pyi +1 -0
  89. pymammotion/proto/luba_mul.proto +62 -1
  90. pymammotion/proto/luba_mul_pb2.py +38 -22
  91. pymammotion/proto/luba_mul_pb2.pyi +94 -7
  92. pymammotion/proto/mctrl_driver.proto +44 -4
  93. pymammotion/proto/mctrl_driver_pb2.py +26 -14
  94. pymammotion/proto/mctrl_driver_pb2.pyi +66 -11
  95. pymammotion/proto/mctrl_nav.proto +93 -52
  96. pymammotion/proto/mctrl_nav_pb2.py +75 -67
  97. pymammotion/proto/mctrl_nav_pb2.pyi +142 -56
  98. pymammotion/proto/mctrl_ota.proto +40 -2
  99. pymammotion/proto/mctrl_ota_pb2.py +23 -13
  100. pymammotion/proto/mctrl_ota_pb2.pyi +67 -4
  101. pymammotion/proto/mctrl_pept.proto +8 -3
  102. pymammotion/proto/mctrl_pept_pb2.py +8 -6
  103. pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
  104. pymammotion/proto/mctrl_sys.proto +325 -86
  105. pymammotion/proto/mctrl_sys_pb2.py +162 -98
  106. pymammotion/proto/mctrl_sys_pb2.pyi +451 -25
  107. pymammotion/proto/message_pool.py +3 -0
  108. pymammotion/proto/py.typed +0 -0
  109. pymammotion/utility/constant/device_constant.py +29 -5
  110. pymammotion/utility/datatype_converter.py +13 -12
  111. pymammotion/utility/device_config.py +522 -130
  112. pymammotion/utility/device_type.py +218 -21
  113. pymammotion/utility/map.py +238 -51
  114. pymammotion/utility/mur_mur_hash.py +159 -0
  115. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/METADATA +26 -31
  116. pymammotion-0.5.51.dist-info/RECORD +152 -0
  117. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/WHEEL +1 -1
  118. pymammotion/aliyun/cloud_service.py +0 -65
  119. pymammotion/data/model/plan.py +0 -58
  120. pymammotion/data/state_manager.py +0 -129
  121. pymammotion/proto/basestation.py +0 -59
  122. pymammotion/proto/common.py +0 -12
  123. pymammotion/proto/dev_net.py +0 -381
  124. pymammotion/proto/luba_msg.py +0 -81
  125. pymammotion/proto/luba_mul.py +0 -76
  126. pymammotion/proto/mctrl_driver.py +0 -100
  127. pymammotion/proto/mctrl_nav.py +0 -664
  128. pymammotion/proto/mctrl_ota.py +0 -48
  129. pymammotion/proto/mctrl_pept.py +0 -41
  130. pymammotion/proto/mctrl_sys.py +0 -574
  131. pymammotion-0.4.0a2.dist-info/RECORD +0 -131
  132. /pymammotion/http/{_init_.py → __init__.py} +0 -0
  133. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info/licenses}/LICENSE +0 -0
@@ -1,207 +1,214 @@
1
- """MammotionMQTT."""
2
-
3
1
  import asyncio
4
- import base64
5
- import hashlib
6
- import hmac
2
+ from collections.abc import Awaitable, Callable
7
3
  import json
8
4
  import logging
9
- from logging import getLogger
10
- from typing import Awaitable, Callable, Optional
5
+ import ssl
6
+ from typing import Any
7
+ from urllib.parse import urlparse
11
8
 
12
- import betterproto
13
- from linkkit.linkkit import LinkKit
14
- from paho.mqtt.client import MQTTMessage
9
+ import paho.mqtt.client as mqtt
10
+ from paho.mqtt.properties import Properties
11
+ from paho.mqtt.reasoncodes import ReasonCode
15
12
 
16
- from pymammotion.aliyun.cloud_gateway import CloudIOTGateway
17
- from pymammotion.data.mqtt.event import ThingEventMessage
18
- from pymammotion.data.mqtt.properties import ThingPropertiesMessage
19
- from pymammotion.data.mqtt.status import ThingStatusMessage
20
- from pymammotion.proto.luba_msg import LubaMsg
13
+ from pymammotion import MammotionHTTP
14
+ from pymammotion.http.model.http import DeviceRecord, MQTTConnection, Response, UnauthorizedException
15
+ from pymammotion.utility.datatype_converter import DatatypeConverter
21
16
 
22
- logger = getLogger(__name__)
17
+ logger = logging.getLogger(__name__)
23
18
 
24
19
 
25
20
  class MammotionMQTT:
26
- """MQTT client for pymammotion."""
21
+ """Mammotion MQTT Client."""
22
+
23
+ converter = DatatypeConverter()
27
24
 
28
25
  def __init__(
29
- self,
30
- region_id: str,
31
- product_key: str,
32
- device_name: str,
33
- device_secret: str,
34
- iot_token: str,
35
- cloud_client: CloudIOTGateway,
36
- client_id: Optional[str] = None,
26
+ self, mqtt_connection: MQTTConnection, mammotion_http: MammotionHTTP, records: list[DeviceRecord]
37
27
  ) -> None:
38
- """Create instance of MammotionMQTT."""
39
- super().__init__()
40
- self._cloud_client = cloud_client
41
- self.is_connected = False
42
- self.is_ready = False
43
- self.on_connected: Optional[Callable[[], Awaitable[None]]] = None
44
- self.on_ready: Optional[Callable[[], Awaitable[None]]] = None
45
- self.on_error: Optional[Callable[[str], Awaitable[None]]] = None
46
- self.on_disconnected: Optional[Callable[[], Awaitable[None]]] = None
47
- self.on_message: Optional[Callable[[str, str, str], Awaitable[None]]] = None
48
-
49
- self._product_key = product_key
50
- self._device_name = device_name
51
- self._device_secret = device_secret
52
- self._iot_token = iot_token
53
- self._mqtt_username = f"{device_name}&{product_key}"
54
- # linkkit provides the correct MQTT service for all of this and uses paho under the hood
55
- if client_id is None:
56
- client_id = f"python-{device_name}"
57
- self._mqtt_client_id = f"{client_id}|securemode=2,signmethod=hmacsha1|"
58
- sign_content = f"clientId{client_id}deviceName{device_name}productKey{product_key}"
59
- self._mqtt_password = hmac.new(
60
- device_secret.encode("utf-8"), sign_content.encode("utf-8"), hashlib.sha1
61
- ).hexdigest()
62
-
63
- self._client_id = client_id
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
64
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)
65
37
 
66
- self._linkkit_client = LinkKit(
67
- region_id,
68
- product_key,
69
- device_name,
70
- device_secret,
71
- auth_type="",
72
- client_id=client_id,
73
- password=self._mqtt_password,
74
- username=self._mqtt_username,
75
- )
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)
76
46
 
77
- self._linkkit_client.enable_logger(level=logging.ERROR)
78
- self._linkkit_client.on_connect = self._thing_on_connect
79
- self._linkkit_client.on_disconnect = self._on_disconnect
80
- self._linkkit_client.on_thing_enable = self._thing_on_thing_enable
81
- self._linkkit_client.on_topic_message = self._thing_on_topic_message
82
- self._mqtt_host = f"{self._product_key}.iot-as-mqtt.{region_id}.aliyuncs.com"
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()
83
52
 
84
53
  def connect_async(self) -> None:
85
54
  """Connect async to MQTT Server."""
86
- logger.info("Connecting...")
87
- if self._linkkit_client.check_state() is LinkKit.LinkKitState.INITIALIZED:
88
- self._linkkit_client.thing_setup()
89
- self._linkkit_client.connect_async()
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()
90
59
 
91
60
  def disconnect(self) -> None:
92
61
  """Disconnect from MQTT Server."""
93
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
+ )
94
89
 
95
- self._linkkit_client.disconnect()
90
+ client.username_pw_set(mqtt_connection.username, mqtt_connection.jwt)
96
91
 
97
- def _thing_on_thing_enable(self, user_data) -> None:
98
- """Is called when Thing is enabled."""
99
- logger.debug("on_thing_enable")
100
- self.is_connected = True
101
- # logger.debug('subscribe_topic, topic:%s' % echo_topic)
102
- # self._linkkit_client.subscribe_topic(echo_topic, 0)
103
- self._linkkit_client.subscribe_topic(
104
- f"/sys/{self._product_key}/{self._device_name}/app/down/account/bind_reply"
105
- )
106
- self._linkkit_client.subscribe_topic(
107
- f"/sys/{self._product_key}/{self._device_name}/app/down/thing/event/property/post_reply"
108
- )
109
- self._linkkit_client.subscribe_topic(
110
- f"/sys/{self._product_key}/{self._device_name}/app/down/thing/wifi/status/notify"
111
- )
112
- self._linkkit_client.subscribe_topic(
113
- f"/sys/{self._product_key}/{self._device_name}/app/down/thing/wifi/connect/event/notify"
114
- )
115
- self._linkkit_client.subscribe_topic(
116
- f"/sys/{self._product_key}/{self._device_name}/app/down/_thing/event/notify"
117
- )
118
- self._linkkit_client.subscribe_topic(f"/sys/{self._product_key}/{self._device_name}/app/down/thing/events")
119
- self._linkkit_client.subscribe_topic(f"/sys/{self._product_key}/{self._device_name}/app/down/thing/status")
120
- self._linkkit_client.subscribe_topic(f"/sys/{self._product_key}/{self._device_name}/app/down/thing/properties")
121
- self._linkkit_client.subscribe_topic(
122
- f"/sys/{self._product_key}/{self._device_name}/app/down/thing/model/down_raw"
123
- )
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)
124
96
 
125
- self._linkkit_client.publish_topic(
126
- f"/sys/{self._product_key}/{self._device_name}/app/up/account/bind",
127
- json.dumps(
128
- {
129
- "id": "msgid1",
130
- "version": "1.0",
131
- "request": {"clientId": self._mqtt_username},
132
- "params": {"iotToken": self._iot_token},
133
- }
134
- ),
135
- )
97
+ # automatic reconnect backoff
98
+ client.reconnect_delay_set(min_delay=1, max_delay=120)
136
99
 
137
- if self.on_ready:
138
- self.is_ready = True
139
- future = asyncio.run_coroutine_threadsafe(self.on_ready(), self.loop)
140
- asyncio.wrap_future(future, loop=self.loop)
141
- # self._linkkit_client.query_ota_firmware()
142
- # command = MammotionCommand(device_name="Luba")
143
- # self._cloud_client.send_cloud_command(command.get_report_cfg())
144
-
145
- def _thing_on_topic_message(self, topic, payload, qos, user_data) -> None:
146
- """Is called when thing topic comes in."""
147
- logger.debug(
148
- "on_topic_message, receive message, topic:%s, payload:%s, qos:%d",
149
- topic,
150
- payload,
151
- qos,
152
- )
153
- payload = json.loads(payload)
154
- iot_id = payload.get("params", {}).get("iotId", "")
155
- if iot_id != "" and self.on_message:
156
- future = asyncio.run_coroutine_threadsafe(self.on_message(topic, payload, iot_id), self.loop)
157
- asyncio.wrap_future(future, loop=self.loop)
100
+ # connect (synchronous connect attempt) and start background loop
101
+ if hostname:
102
+ client.host = hostname
103
+ client.port = port
104
+ client.keepalive = keepalive
158
105
 
159
- def _thing_on_connect(self, session_flag, rc, user_data) -> None:
160
- """Is called on thing connect."""
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."""
161
151
  self.is_connected = True
152
+ for record in self.records:
153
+ self.subscribe_all(record.product_key, record.device_name)
162
154
  if self.on_connected is not None:
163
155
  future = asyncio.run_coroutine_threadsafe(self.on_connected(), self.loop)
164
156
  asyncio.wrap_future(future, loop=self.loop)
165
157
 
166
- logger.debug("on_connect, session_flag:%d, rc:%d", session_flag, rc)
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)
167
162
 
168
- # self._linkkit_client.subscribe_topic(f"/sys/{self._product_key}/{self._device_name}/#")
163
+ logger.debug("on_connect, session_flag:%s, rc:%s", session_flag, rc)
169
164
 
170
- def _on_disconnect(self, _client, _userdata) -> None:
171
- """Is called on disconnect."""
172
- logger.info("Disconnected")
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."""
173
175
  self.is_connected = False
174
- self.is_ready = False
175
- if self.on_disconnected:
176
+ if self.on_disconnected is not None:
177
+ for record in self.records:
178
+ self.unsubscribe_all(record.product_key, record.device_name)
176
179
  future = asyncio.run_coroutine_threadsafe(self.on_disconnected(), self.loop)
177
180
  asyncio.wrap_future(future, loop=self.loop)
178
181
 
179
- def _on_message(self, _client, _userdata, message: MQTTMessage) -> None:
180
- """Is called when message is received."""
181
- logger.info("Message on topic %s", message.topic)
182
-
183
- payload = json.loads(message.payload)
184
- if message.topic.endswith("/app/down/thing/events"):
185
- event = ThingEventMessage(**payload)
186
- params = event.params
187
- if params.identifier == "device_protobuf_msg_event":
188
- content = LubaMsg().parse(base64.b64decode(params.value.content))
189
-
190
- logger.info("Unhandled protobuf event: %s", betterproto.which_one_of(content, "LubaSubMsg"))
191
- elif params.identifier == "device_warning_event":
192
- logger.debug("identifier event: %s", params.identifier)
193
- else:
194
- logger.info("Unhandled event: %s", params.identifier)
195
- elif message.topic.endswith("/app/down/thing/status"):
196
- status = ThingStatusMessage(**payload)
197
- logger.debug(status.params.status.value)
198
- elif message.topic.endswith("/app/down/thing/properties"):
199
- properties = ThingPropertiesMessage(**payload)
200
- logger.debug("properties: %s", properties)
201
- else:
202
- logger.debug("Unhandled topic: %s", message.topic)
203
- logger.debug(payload)
204
-
205
- def get_cloud_client(self) -> CloudIOTGateway:
206
- """Return internal cloud client."""
207
- return self._cloud_client
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 ""