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.
- pymammotion/__init__.py +53 -0
- pymammotion/agora/__init__.py +0 -0
- pymammotion/agora/agora_api.py +755 -0
- pymammotion/agora/agora_rtc_capabilities.py +748 -0
- pymammotion/agora/agora_websockets.py +1175 -0
- pymammotion/aliyun/__init__.py +1 -0
- pymammotion/aliyun/client.py +235 -0
- pymammotion/aliyun/cloud_gateway.py +982 -0
- pymammotion/aliyun/model/aep_response.py +21 -0
- pymammotion/aliyun/model/connect_response.py +51 -0
- pymammotion/aliyun/model/dev_by_account_response.py +195 -0
- pymammotion/aliyun/model/login_by_oauth_response.py +64 -0
- pymammotion/aliyun/model/regions_response.py +29 -0
- pymammotion/aliyun/model/session_by_authcode_response.py +19 -0
- pymammotion/aliyun/model/thing_response.py +12 -0
- pymammotion/aliyun/regions.py +62 -0
- pymammotion/aliyun/tea/core.py +297 -0
- pymammotion/aliyun/tmp_constant.py +171 -0
- pymammotion/bluetooth/__init__.py +1 -0
- pymammotion/bluetooth/ble.py +62 -0
- pymammotion/bluetooth/ble_message.py +676 -0
- pymammotion/bluetooth/const.py +27 -0
- pymammotion/bluetooth/data/__init__.py +0 -0
- pymammotion/bluetooth/data/convert.py +25 -0
- pymammotion/bluetooth/data/framectrldata.py +40 -0
- pymammotion/bluetooth/data/notifydata.py +62 -0
- pymammotion/bluetooth/model/__init__.py +0 -0
- pymammotion/bluetooth/model/atomic_integer.py +54 -0
- pymammotion/const.py +13 -0
- pymammotion/data/__init__.py +0 -0
- pymammotion/data/model/__init__.py +8 -0
- pymammotion/data/model/account.py +8 -0
- pymammotion/data/model/device.py +192 -0
- pymammotion/data/model/device_config.py +72 -0
- pymammotion/data/model/device_info.py +60 -0
- pymammotion/data/model/device_limits.py +49 -0
- pymammotion/data/model/enums.py +77 -0
- pymammotion/data/model/errors.py +12 -0
- pymammotion/data/model/events.py +14 -0
- pymammotion/data/model/generate_geojson.py +565 -0
- pymammotion/data/model/generate_route_information.py +26 -0
- pymammotion/data/model/hash_list.py +475 -0
- pymammotion/data/model/location.py +36 -0
- pymammotion/data/model/mowing_modes.py +77 -0
- pymammotion/data/model/rapid_state.py +45 -0
- pymammotion/data/model/raw_data.py +215 -0
- pymammotion/data/model/region_data.py +102 -0
- pymammotion/data/model/report_info.py +182 -0
- pymammotion/data/model/work.py +27 -0
- pymammotion/data/mower_state_manager.py +369 -0
- pymammotion/data/mqtt/__init__.py +1 -0
- pymammotion/data/mqtt/event.py +227 -0
- pymammotion/data/mqtt/mammotion_properties.py +276 -0
- pymammotion/data/mqtt/properties.py +203 -0
- pymammotion/data/mqtt/status.py +57 -0
- pymammotion/event/__init__.py +6 -0
- pymammotion/event/event.py +96 -0
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +514 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/__init__.py +0 -0
- pymammotion/http/encryption.py +220 -0
- pymammotion/http/http.py +673 -0
- pymammotion/http/model/__init__.py +0 -0
- pymammotion/http/model/camera_stream.py +31 -0
- pymammotion/http/model/http.py +249 -0
- pymammotion/http/model/response_factory.py +61 -0
- pymammotion/http/model/rtk.py +16 -0
- pymammotion/mammotion/__init__.py +0 -0
- pymammotion/mammotion/commands/__init__.py +0 -0
- pymammotion/mammotion/commands/abstract_message.py +24 -0
- pymammotion/mammotion/commands/mammotion_command.py +81 -0
- pymammotion/mammotion/commands/messages/__init__.py +0 -0
- pymammotion/mammotion/commands/messages/basestation.py +43 -0
- pymammotion/mammotion/commands/messages/driver.py +122 -0
- pymammotion/mammotion/commands/messages/media.py +87 -0
- pymammotion/mammotion/commands/messages/navigation.py +564 -0
- pymammotion/mammotion/commands/messages/network.py +205 -0
- pymammotion/mammotion/commands/messages/ota.py +38 -0
- pymammotion/mammotion/commands/messages/system.py +330 -0
- pymammotion/mammotion/commands/messages/video.py +33 -0
- pymammotion/mammotion/control/__init__.py +0 -0
- pymammotion/mammotion/control/joystick.py +145 -0
- pymammotion/mammotion/devices/__init__.py +29 -0
- pymammotion/mammotion/devices/base.py +163 -0
- pymammotion/mammotion/devices/mammotion.py +571 -0
- pymammotion/mammotion/devices/mammotion_bluetooth.py +496 -0
- pymammotion/mammotion/devices/mammotion_cloud.py +355 -0
- pymammotion/mammotion/devices/mammotion_mower_ble.py +48 -0
- pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
- pymammotion/mammotion/devices/managers/managers.py +81 -0
- pymammotion/mammotion/devices/mower_device.py +120 -0
- pymammotion/mammotion/devices/mower_manager.py +107 -0
- pymammotion/mammotion/devices/rtk_ble.py +89 -0
- pymammotion/mammotion/devices/rtk_cloud.py +115 -0
- pymammotion/mammotion/devices/rtk_device.py +50 -0
- pymammotion/mammotion/devices/rtk_manager.py +125 -0
- pymammotion/mqtt/__init__.py +6 -0
- pymammotion/mqtt/aliyun_mqtt.py +237 -0
- pymammotion/mqtt/linkkit/__init__.py +5 -0
- pymammotion/mqtt/linkkit/h2client.py +585 -0
- pymammotion/mqtt/linkkit/linkkit.py +3025 -0
- pymammotion/mqtt/mammotion_future.py +26 -0
- pymammotion/mqtt/mammotion_mqtt.py +214 -0
- pymammotion/mqtt/mqtt_models.py +66 -0
- pymammotion/proto/__init__.py +4841 -0
- pymammotion/proto/basestation.proto +51 -0
- pymammotion/proto/basestation_pb2.py +35 -0
- pymammotion/proto/basestation_pb2.pyi +89 -0
- pymammotion/proto/common.proto +7 -0
- pymammotion/proto/common_pb2.py +25 -0
- pymammotion/proto/common_pb2.pyi +13 -0
- pymammotion/proto/dev_net.proto +321 -0
- pymammotion/proto/dev_net_pb2.py +111 -0
- pymammotion/proto/dev_net_pb2.pyi +515 -0
- pymammotion/proto/luba_msg.proto +76 -0
- pymammotion/proto/luba_msg_pb2.py +41 -0
- pymammotion/proto/luba_msg_pb2.pyi +97 -0
- pymammotion/proto/luba_mul.proto +129 -0
- pymammotion/proto/luba_mul_pb2.py +61 -0
- pymammotion/proto/luba_mul_pb2.pyi +178 -0
- pymammotion/proto/mctrl_driver.proto +107 -0
- pymammotion/proto/mctrl_driver_pb2.py +57 -0
- pymammotion/proto/mctrl_driver_pb2.pyi +167 -0
- pymammotion/proto/mctrl_nav.proto +591 -0
- pymammotion/proto/mctrl_nav_pb2.py +136 -0
- pymammotion/proto/mctrl_nav_pb2.pyi +1067 -0
- pymammotion/proto/mctrl_ota.proto +80 -0
- pymammotion/proto/mctrl_ota_pb2.py +45 -0
- pymammotion/proto/mctrl_ota_pb2.pyi +128 -0
- pymammotion/proto/mctrl_pept.proto +34 -0
- pymammotion/proto/mctrl_pept_pb2.py +33 -0
- pymammotion/proto/mctrl_pept_pb2.pyi +58 -0
- pymammotion/proto/mctrl_sys.proto +741 -0
- pymammotion/proto/mctrl_sys_pb2.py +206 -0
- pymammotion/proto/mctrl_sys_pb2.pyi +1213 -0
- pymammotion/proto/message_pool.py +3 -0
- pymammotion/proto/py.typed +0 -0
- pymammotion/py.typed +0 -0
- pymammotion/utility/constant/__init__.py +3 -0
- pymammotion/utility/constant/device_constant.py +315 -0
- pymammotion/utility/conversions.py +5 -0
- pymammotion/utility/datatype_converter.py +124 -0
- pymammotion/utility/device_config.py +755 -0
- pymammotion/utility/device_type.py +489 -0
- pymammotion/utility/map.py +259 -0
- pymammotion/utility/movement.py +18 -0
- pymammotion/utility/mur_mur_hash.py +159 -0
- pymammotion/utility/periodic.py +106 -0
- pymammotion/utility/rocker_util.py +194 -0
- pymammotion-0.5.69.dist-info/METADATA +93 -0
- pymammotion-0.5.69.dist-info/RECORD +154 -0
- pymammotion-0.5.69.dist-info/WHEEL +4 -0
- 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 ""
|