pymammotion 0.5.33__py3-none-any.whl → 0.5.40__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 (47) 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 +198 -20
  4. pymammotion/data/model/device.py +1 -0
  5. pymammotion/data/model/device_config.py +1 -1
  6. pymammotion/data/model/enums.py +3 -1
  7. pymammotion/data/model/generate_route_information.py +2 -2
  8. pymammotion/data/model/hash_list.py +105 -33
  9. pymammotion/data/model/region_data.py +4 -4
  10. pymammotion/data/{state_manager.py → mower_state_manager.py} +17 -7
  11. pymammotion/homeassistant/__init__.py +3 -0
  12. pymammotion/homeassistant/mower_api.py +446 -0
  13. pymammotion/homeassistant/rtk_api.py +54 -0
  14. pymammotion/http/http.py +118 -7
  15. pymammotion/http/model/http.py +77 -2
  16. pymammotion/http/model/response_factory.py +10 -4
  17. pymammotion/mammotion/commands/mammotion_command.py +6 -0
  18. pymammotion/mammotion/commands/messages/navigation.py +10 -6
  19. pymammotion/mammotion/devices/__init__.py +27 -3
  20. pymammotion/mammotion/devices/base.py +16 -138
  21. pymammotion/mammotion/devices/mammotion.py +361 -204
  22. pymammotion/mammotion/devices/mammotion_bluetooth.py +7 -5
  23. pymammotion/mammotion/devices/mammotion_cloud.py +22 -74
  24. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  25. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  26. pymammotion/mammotion/devices/managers/managers.py +81 -0
  27. pymammotion/mammotion/devices/mower_device.py +121 -0
  28. pymammotion/mammotion/devices/mower_manager.py +107 -0
  29. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  30. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  31. pymammotion/mammotion/devices/rtk_device.py +50 -0
  32. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  33. pymammotion/mqtt/__init__.py +2 -1
  34. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  35. pymammotion/mqtt/mammotion_mqtt.py +132 -194
  36. pymammotion/mqtt/mqtt_models.py +66 -0
  37. pymammotion/proto/__init__.py +1 -1
  38. pymammotion/proto/mctrl_nav.proto +1 -1
  39. pymammotion/proto/mctrl_nav_pb2.py +1 -1
  40. pymammotion/proto/mctrl_nav_pb2.pyi +4 -4
  41. pymammotion/proto/mctrl_sys.proto +1 -1
  42. pymammotion/utility/device_type.py +88 -3
  43. pymammotion/utility/mur_mur_hash.py +132 -87
  44. {pymammotion-0.5.33.dist-info → pymammotion-0.5.40.dist-info}/METADATA +25 -31
  45. {pymammotion-0.5.33.dist-info → pymammotion-0.5.40.dist-info}/RECORD +54 -40
  46. {pymammotion-0.5.33.dist-info → pymammotion-0.5.40.dist-info}/WHEEL +1 -1
  47. {pymammotion-0.5.33.dist-info → pymammotion-0.5.40.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,89 @@
1
+ """RTK device with Bluetooth LE connectivity."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import Any
6
+ from uuid import UUID
7
+
8
+ from bleak import BleakGATTCharacteristic, BLEDevice
9
+ from bleak_retry_connector import BleakClientWithServiceCache
10
+
11
+ from pymammotion.aliyun.model.dev_by_account_response import Device
12
+ from pymammotion.bluetooth import BleMessage
13
+ from pymammotion.data.model.device import RTKDevice
14
+ from pymammotion.mammotion.commands.mammotion_command import MammotionCommand
15
+ from pymammotion.mammotion.devices.rtk_device import MammotionRTKDevice
16
+
17
+ _LOGGER = logging.getLogger(__name__)
18
+
19
+
20
+ class MammotionRTKBLEDevice(MammotionRTKDevice):
21
+ """RTK device with BLE connectivity - simpler than mowers, no map sync."""
22
+
23
+ def __init__(
24
+ self, cloud_device: Device, rtk_state: RTKDevice, device: BLEDevice, interface: int = 0, **kwargs: Any
25
+ ) -> None:
26
+ """Initialize MammotionRTKBLEDevice."""
27
+ super().__init__(cloud_device, rtk_state)
28
+ self.command_sent_time = 0
29
+ self._disconnect_strategy = True
30
+ self._interface = f"hci{interface}"
31
+ self.ble_device = device
32
+ self._client: BleakClientWithServiceCache | None = None
33
+ self._read_char: BleakGATTCharacteristic | int | str | UUID = 0
34
+ self._write_char: BleakGATTCharacteristic | int | str | UUID = 0
35
+ self._disconnect_timer: asyncio.TimerHandle | None = None
36
+ self._message: BleMessage | None = None
37
+ self._commands: MammotionCommand = MammotionCommand(device.name or "", 1)
38
+ self.command_queue = asyncio.Queue()
39
+ self._expected_disconnect = False
40
+ self._connect_lock = asyncio.Lock()
41
+ self._operation_lock = asyncio.Lock()
42
+ self._key: str | None = None
43
+ loop = asyncio.get_event_loop()
44
+ loop.create_task(self.process_queue())
45
+
46
+ def __del__(self) -> None:
47
+ """Cleanup."""
48
+ if self._disconnect_timer:
49
+ self._disconnect_timer.cancel()
50
+
51
+ @property
52
+ def client(self) -> BleakClientWithServiceCache:
53
+ """Return the BLE client."""
54
+ return self._client
55
+
56
+ def set_disconnect_strategy(self, *, disconnect: bool) -> None:
57
+ """Set disconnect strategy."""
58
+ self._disconnect_strategy = disconnect
59
+
60
+ async def process_queue(self) -> None:
61
+ """Process queued commands - simplified for RTK."""
62
+ while True:
63
+ key, kwargs = await self.command_queue.get()
64
+ try:
65
+ _LOGGER.debug("Processing RTK BLE command: %s", key)
66
+ command_bytes = getattr(self._commands, key)(**kwargs)
67
+ # Send command via BLE (implementation depends on BLE infrastructure)
68
+ # For now, this is a placeholder
69
+ _LOGGER.debug("RTK BLE command sent: %s", key)
70
+ except Exception as ex:
71
+ _LOGGER.exception("Error processing RTK BLE command: %s", ex)
72
+ finally:
73
+ self.command_queue.task_done()
74
+
75
+ async def queue_command(self, key: str, **kwargs: Any) -> None:
76
+ """Queue a command to the RTK device."""
77
+ await self.command_queue.put((key, kwargs))
78
+
79
+ async def command(self, key: str, **kwargs):
80
+ """Send a command to the RTK device."""
81
+ return await self.queue_command(key, **kwargs)
82
+
83
+ async def _ble_sync(self) -> None:
84
+ """RTK devices don't use BLE sync in the same way as mowers."""
85
+
86
+ async def stop(self) -> None:
87
+ """Stop everything ready for destroying."""
88
+ if self._client is not None and self._client.is_connected:
89
+ await self._client.disconnect()
@@ -0,0 +1,113 @@
1
+ """RTK device with cloud MQTT connectivity."""
2
+
3
+ import asyncio
4
+ from collections.abc import Awaitable, Callable
5
+ import logging
6
+ from typing import Any
7
+
8
+ from pymammotion.aliyun.model.dev_by_account_response import Device
9
+ from pymammotion.data.model.device import RTKDevice
10
+ from pymammotion.data.mqtt.properties import ThingPropertiesMessage
11
+ from pymammotion.data.mqtt.status import ThingStatusMessage
12
+ from pymammotion.mammotion.commands.mammotion_command import MammotionCommand
13
+ from pymammotion.mammotion.devices.mammotion_cloud import MammotionCloud
14
+ from pymammotion.mammotion.devices.rtk_device import MammotionRTKDevice
15
+
16
+ _LOGGER = logging.getLogger(__name__)
17
+
18
+
19
+ class MammotionRTKCloudDevice(MammotionRTKDevice):
20
+ """RTK device with cloud connectivity - simpler than mowers, no map sync."""
21
+
22
+ def __init__(self, mqtt: MammotionCloud, cloud_device: Device, rtk_state: RTKDevice) -> None:
23
+ """Initialize MammotionRTKCloudDevice."""
24
+ super().__init__(cloud_device, rtk_state)
25
+ self.stopped = False
26
+ self.on_ready_callback: Callable[[], Awaitable[None]] | None = None
27
+ self.loop = asyncio.get_event_loop()
28
+ self._mqtt = mqtt
29
+ self.iot_id = cloud_device.iot_id
30
+ self.device = cloud_device
31
+ self._commands: MammotionCommand = MammotionCommand(
32
+ cloud_device.device_name,
33
+ int(mqtt.cloud_client.mammotion_http.response.data.userInformation.userAccount),
34
+ )
35
+ # Subscribe to MQTT events for this device
36
+ self._mqtt.mqtt_properties_event.add_subscribers(self._parse_message_properties_for_device)
37
+ self._mqtt.mqtt_status_event.add_subscribers(self._parse_message_status_for_device)
38
+ self._mqtt.on_ready_event.add_subscribers(self.on_ready)
39
+ self._mqtt.on_disconnected_event.add_subscribers(self.on_disconnect)
40
+ self._mqtt.on_connected_event.add_subscribers(self.on_connect)
41
+
42
+ def __del__(self) -> None:
43
+ """Cleanup subscriptions."""
44
+ if hasattr(self, "_mqtt"):
45
+ self._mqtt.on_ready_event.remove_subscribers(self.on_ready)
46
+ self._mqtt.on_disconnected_event.remove_subscribers(self.on_disconnect)
47
+ self._mqtt.on_connected_event.remove_subscribers(self.on_connect)
48
+ self._mqtt.mqtt_properties_event.remove_subscribers(self._parse_message_properties_for_device)
49
+ self._mqtt.mqtt_status_event.remove_subscribers(self._parse_message_status_for_device)
50
+
51
+ @property
52
+ def command_sent_time(self) -> float:
53
+ return self._mqtt.command_sent_time
54
+
55
+ @property
56
+ def mqtt(self):
57
+ return self._mqtt
58
+
59
+ async def on_ready(self) -> None:
60
+ """Callback for when MQTT is subscribed to events."""
61
+ if self.stopped:
62
+ return
63
+ if self.on_ready_callback:
64
+ await self.on_ready_callback()
65
+
66
+ async def on_disconnect(self) -> None:
67
+ """Callback for when MQTT disconnects."""
68
+ self._mqtt.disconnect()
69
+
70
+ async def on_connect(self) -> None:
71
+ """Callback for when MQTT connects."""
72
+
73
+ async def stop(self) -> None:
74
+ """Stop all tasks and disconnect."""
75
+ self.stopped = True
76
+
77
+ async def start(self) -> None:
78
+ """Start the device connection."""
79
+ self.stopped = False
80
+ if not self.mqtt.is_connected():
81
+ loop = asyncio.get_running_loop()
82
+ await loop.run_in_executor(None, self.mqtt.connect_async)
83
+
84
+ async def queue_command(self, key: str, **kwargs: Any) -> None:
85
+ """Queue a command to the RTK device."""
86
+ _LOGGER.debug("Queueing command: %s", key)
87
+ future = asyncio.Future()
88
+ command_bytes = getattr(self._commands, key)(**kwargs)
89
+ await self._mqtt.command_queue.put((self.iot_id, key, command_bytes, future))
90
+ try:
91
+ return await future
92
+ except asyncio.CancelledError:
93
+ """Try again once."""
94
+ future = asyncio.Future()
95
+ await self._mqtt.command_queue.put((self.iot_id, key, command_bytes, future))
96
+
97
+ async def _parse_message_properties_for_device(self, event: ThingPropertiesMessage) -> None:
98
+ """Parse property messages for this RTK device."""
99
+ if event.params.iotId != self.iot_id:
100
+ return
101
+ # RTK devices have simpler properties - update as needed
102
+ _LOGGER.debug("RTK properties update: %s", event)
103
+
104
+ async def _parse_message_status_for_device(self, status: ThingStatusMessage) -> None:
105
+ """Parse status messages for this RTK device."""
106
+ if status.params.iotId != self.iot_id:
107
+ return
108
+ # Update online status
109
+ self._rtk_device.online = True
110
+ _LOGGER.debug("RTK status update: %s", status)
111
+
112
+ async def _ble_sync(self) -> None:
113
+ """RTK devices don't use BLE sync in the same way as mowers."""
@@ -0,0 +1,50 @@
1
+ """RTK device class without map synchronization callbacks."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import Any
6
+
7
+ from pymammotion.aliyun.model.dev_by_account_response import Device
8
+ from pymammotion.data.model.device import RTKDevice
9
+ from pymammotion.data.model.raw_data import RawMowerData
10
+
11
+ _LOGGER = logging.getLogger(__name__)
12
+
13
+
14
+ class MammotionRTKDevice:
15
+ """RTK device without map synchronization - simpler than mowers."""
16
+
17
+ def __init__(self, cloud_device: Device, rtk_state: RTKDevice) -> None:
18
+ """Initialize MammotionRTKDevice."""
19
+ self.loop = asyncio.get_event_loop()
20
+ self._rtk_device = rtk_state
21
+ self._raw_data = dict()
22
+ self._raw_mower_data: RawMowerData = RawMowerData()
23
+ self._notify_future: asyncio.Future[bytes] | None = None
24
+ self._cloud_device = cloud_device
25
+
26
+ @property
27
+ def rtk(self) -> RTKDevice:
28
+ """Get the RTK device state."""
29
+ return self._rtk_device
30
+
31
+ @property
32
+ def raw_data(self) -> dict[str, Any]:
33
+ """Get the raw data of the device."""
34
+ return self._raw_data
35
+
36
+ async def command(self, key: str, **kwargs: Any) -> bytes | None:
37
+ """Send a command to the device."""
38
+ return await self.queue_command(key, **kwargs)
39
+
40
+ async def queue_command(self, key: str, **kwargs: Any) -> bytes | None:
41
+ """Queue commands to RTK device - to be implemented by connection-specific subclasses."""
42
+ raise NotImplementedError("Subclasses must implement queue_command")
43
+
44
+ async def _ble_sync(self) -> None:
45
+ """Send ble sync command - to be implemented by connection-specific subclasses."""
46
+ raise NotImplementedError("Subclasses must implement _ble_sync")
47
+
48
+ def stop(self) -> None:
49
+ """Stop everything ready for destroying - to be implemented by connection-specific subclasses."""
50
+ raise NotImplementedError("Subclasses must implement stop")
@@ -0,0 +1,122 @@
1
+ """RTK Device Manager - manages RTK devices with cloud and BLE connectivity."""
2
+
3
+ from typing import override
4
+
5
+ from bleak import BLEDevice
6
+
7
+ from pymammotion.aliyun.cloud_gateway import CloudIOTGateway
8
+ from pymammotion.aliyun.model.dev_by_account_response import Device
9
+ from pymammotion.data.model.device import RTKDevice
10
+ from pymammotion.data.model.enums import ConnectionPreference
11
+ from pymammotion.mammotion.devices.mammotion_cloud import MammotionCloud
12
+ from pymammotion.mammotion.devices.managers.managers import AbstractDeviceManager
13
+ from pymammotion.mammotion.devices.rtk_ble import MammotionRTKBLEDevice
14
+ from pymammotion.mammotion.devices.rtk_cloud import MammotionRTKCloudDevice
15
+
16
+
17
+ class MammotionRTKDeviceManager(AbstractDeviceManager):
18
+ """Manages an RTK device with both cloud and BLE connectivity options."""
19
+
20
+ def __init__(
21
+ self,
22
+ name: str,
23
+ iot_id: str,
24
+ cloud_client: CloudIOTGateway,
25
+ cloud_device: Device,
26
+ ble_device: BLEDevice | None = None,
27
+ mqtt: MammotionCloud | None = None,
28
+ preference: ConnectionPreference = ConnectionPreference.WIFI,
29
+ ) -> None:
30
+ """Initialize RTK device manager."""
31
+ super().__init__(name, iot_id, cloud_client, cloud_device, preference)
32
+ self._ble_device: MammotionRTKBLEDevice | None = None
33
+ self._cloud_device: MammotionRTKCloudDevice | None = None
34
+ self.name = name
35
+ self.iot_id = iot_id
36
+ self.cloud_client = cloud_client
37
+ self._device: Device = cloud_device
38
+ self.mammotion_http = cloud_client.mammotion_http
39
+ self.preference = preference
40
+
41
+ # Initialize RTK state
42
+ self._rtk_state = RTKDevice(
43
+ name=name,
44
+ iot_id=iot_id,
45
+ product_key=cloud_device.product_key,
46
+ )
47
+
48
+ # Add connection types if provided
49
+ if ble_device:
50
+ self.add_ble(ble_device)
51
+ if mqtt:
52
+ self.add_cloud(mqtt)
53
+
54
+ @property
55
+ def state(self) -> RTKDevice:
56
+ """Return the RTK device state."""
57
+ return self._rtk_state
58
+
59
+ @state.setter
60
+ def state(self, value: RTKDevice) -> None:
61
+ """Set the RTK device state."""
62
+ self._rtk_state = value
63
+
64
+ @property
65
+ def ble(self) -> MammotionRTKBLEDevice | None:
66
+ """Return BLE device interface."""
67
+ return self._ble_device
68
+
69
+ @property
70
+ def cloud(self) -> MammotionRTKCloudDevice | None:
71
+ """Return cloud device interface."""
72
+ return self._cloud_device
73
+
74
+ def has_queued_commands(self) -> bool:
75
+ """Check if there are queued commands."""
76
+ if self.cloud and self.preference == ConnectionPreference.WIFI:
77
+ return not self.cloud.mqtt.command_queue.empty()
78
+ elif self.ble:
79
+ return not self.ble.command_queue.empty()
80
+ return False
81
+
82
+ def add_ble(self, ble_device: BLEDevice) -> MammotionRTKBLEDevice:
83
+ """Add BLE device."""
84
+ self._ble_device = MammotionRTKBLEDevice(
85
+ cloud_device=self._device, rtk_state=self._rtk_state, device=ble_device
86
+ )
87
+ return self._ble_device
88
+
89
+ @override
90
+ def add_cloud(self, mqtt: MammotionCloud) -> MammotionRTKCloudDevice:
91
+ """Add cloud device."""
92
+ self._cloud_device = MammotionRTKCloudDevice(mqtt, cloud_device=self._device, rtk_state=self._rtk_state)
93
+ return self._cloud_device
94
+
95
+ def replace_cloud(self, cloud_device: MammotionRTKCloudDevice) -> None:
96
+ """Replace cloud device."""
97
+ self._cloud_device = cloud_device
98
+
99
+ def remove_cloud(self) -> None:
100
+ """Remove cloud device."""
101
+ self._cloud_device = None
102
+
103
+ def replace_ble(self, ble_device: MammotionRTKBLEDevice) -> None:
104
+ """Replace BLE device."""
105
+ self._ble_device = ble_device
106
+
107
+ def remove_ble(self) -> None:
108
+ """Remove BLE device."""
109
+ self._ble_device = None
110
+
111
+ def replace_mqtt(self, mqtt: MammotionCloud) -> None:
112
+ """Replace MQTT connection."""
113
+ device = self._cloud_device.device
114
+ self._cloud_device = MammotionRTKCloudDevice(mqtt, cloud_device=device, rtk_state=self._rtk_state)
115
+
116
+ def has_cloud(self) -> bool:
117
+ """Check if cloud connection is available."""
118
+ return self._cloud_device is not None
119
+
120
+ def has_ble(self) -> bool:
121
+ """Check if BLE connection is available."""
122
+ return self._ble_device is not None
@@ -1,5 +1,6 @@
1
1
  """Package for MammotionMQTT."""
2
2
 
3
+ from .aliyun_mqtt import AliyunMQTT
3
4
  from .mammotion_mqtt import MammotionMQTT
4
5
 
5
- __all__ = ["MammotionMQTT"]
6
+ __all__ = ["AliyunMQTT", "MammotionMQTT"]
@@ -0,0 +1,232 @@
1
+ """MammotionMQTT."""
2
+
3
+ import asyncio
4
+ import base64
5
+ from collections.abc import Awaitable, Callable
6
+ import hashlib
7
+ import hmac
8
+ import json
9
+ import logging
10
+ from logging import getLogger
11
+
12
+ import betterproto2
13
+ from paho.mqtt.client import MQTTMessage
14
+
15
+ from pymammotion.aliyun.cloud_gateway import CloudIOTGateway
16
+ from pymammotion.data.mqtt.event import ThingEventMessage
17
+ from pymammotion.data.mqtt.properties import ThingPropertiesMessage
18
+ from pymammotion.data.mqtt.status import ThingStatusMessage
19
+ from pymammotion.mqtt.linkkit.linkkit import LinkKit
20
+ from pymammotion.proto import LubaMsg
21
+
22
+ logger = getLogger(__name__)
23
+
24
+
25
+ class AliyunMQTT:
26
+ """MQTT client for pymammotion."""
27
+
28
+ 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: str | None = None,
37
+ ) -> 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: Callable[[], Awaitable[None]] | None = None
44
+ self.on_ready: Callable[[], Awaitable[None]] | None = None
45
+ self.on_error: Callable[[str], Awaitable[None]] | None = None
46
+ self.on_disconnected: Callable[[], Awaitable[None]] | None = None
47
+ self.on_message: Callable[[str, str, str], Awaitable[None]] | 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
64
+ self.loop = asyncio.get_running_loop()
65
+
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
+ )
76
+
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"
83
+
84
+ def connect_async(self) -> None:
85
+ """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()
90
+
91
+ def disconnect(self) -> None:
92
+ """Disconnect from MQTT Server."""
93
+ logger.info("Disconnecting...")
94
+
95
+ self._linkkit_client.disconnect()
96
+
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
+ )
124
+
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
+ )
136
+
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
+
142
+ def unsubscribe(self) -> None:
143
+ self._linkkit_client.unsubscribe_topic(
144
+ f"/sys/{self._product_key}/{self._device_name}/app/down/account/bind_reply"
145
+ )
146
+ self._linkkit_client.unsubscribe_topic(
147
+ f"/sys/{self._product_key}/{self._device_name}/app/down/thing/event/property/post_reply"
148
+ )
149
+ self._linkkit_client.unsubscribe_topic(
150
+ f"/sys/{self._product_key}/{self._device_name}/app/down/thing/wifi/status/notify"
151
+ )
152
+ self._linkkit_client.unsubscribe_topic(
153
+ f"/sys/{self._product_key}/{self._device_name}/app/down/thing/wifi/connect/event/notify"
154
+ )
155
+ self._linkkit_client.unsubscribe_topic(
156
+ f"/sys/{self._product_key}/{self._device_name}/app/down/_thing/event/notify"
157
+ )
158
+ self._linkkit_client.unsubscribe_topic(f"/sys/{self._product_key}/{self._device_name}/app/down/thing/events")
159
+ self._linkkit_client.unsubscribe_topic(f"/sys/{self._product_key}/{self._device_name}/app/down/thing/status")
160
+ self._linkkit_client.unsubscribe_topic(
161
+ f"/sys/{self._product_key}/{self._device_name}/app/down/thing/properties"
162
+ )
163
+ self._linkkit_client.unsubscribe_topic(
164
+ f"/sys/{self._product_key}/{self._device_name}/app/down/thing/model/down_raw"
165
+ )
166
+
167
+ def _thing_on_topic_message(self, topic: str, payload: str, qos, user_data) -> None:
168
+ """Is called when thing topic comes in."""
169
+ logger.debug(
170
+ "on_topic_message, receive message, topic:%s, payload:%s, qos:%d",
171
+ topic,
172
+ payload,
173
+ qos,
174
+ )
175
+ json_payload = json.loads(payload)
176
+ iot_id = json_payload.get("params", {}).get("iotId", "")
177
+ if iot_id != "" and self.on_message is not None:
178
+ future = asyncio.run_coroutine_threadsafe(self.on_message(topic, payload, iot_id), self.loop)
179
+ asyncio.wrap_future(future, loop=self.loop)
180
+
181
+ def _thing_on_connect(self, session_flag, rc, user_data) -> None:
182
+ """Handle connection event and execute callback if set."""
183
+ self.is_connected = True
184
+ if self.on_connected is not None:
185
+ future = asyncio.run_coroutine_threadsafe(self.on_connected(), self.loop)
186
+ asyncio.wrap_future(future, loop=self.loop)
187
+
188
+ logger.debug("on_connect, session_flag:%d, rc:%d", session_flag, rc)
189
+
190
+ def _on_disconnect(self, _client, _userdata) -> None:
191
+ """Is called on disconnect."""
192
+ if self._linkkit_client.check_state() is LinkKit.LinkKitState.DISCONNECTED:
193
+ logger.info("Disconnected")
194
+ self.is_connected = False
195
+ self.is_ready = False
196
+ if self.on_disconnected:
197
+ future = asyncio.run_coroutine_threadsafe(self.on_disconnected(), self.loop)
198
+ asyncio.wrap_future(future, loop=self.loop)
199
+
200
+ def _on_message(self, _client, _userdata, message: MQTTMessage) -> None:
201
+ """Is called when message is received."""
202
+ logger.debug("Message on topic %s", message.topic)
203
+
204
+ payload = json.loads(message.payload)
205
+ if message.topic.endswith("/app/down/thing/events"):
206
+ event = ThingEventMessage(**payload)
207
+ params = event.params
208
+ if params.identifier == "device_protobuf_msg_event":
209
+ content = LubaMsg().parse(base64.b64decode(params.value.content))
210
+
211
+ logger.info("Unhandled protobuf event: %s", betterproto2.which_one_of(content, "LubaSubMsg"))
212
+ elif params.identifier == "device_warning_event":
213
+ logger.debug("identifier event: %s", params.identifier)
214
+ else:
215
+ logger.info("Unhandled event: %s", params.identifier)
216
+ elif message.topic.endswith("/app/down/thing/status"):
217
+ # the tell if a device has come back online
218
+ # lastStatus
219
+ # 1 online?
220
+ # 3 offline?
221
+ status = ThingStatusMessage(**payload)
222
+ logger.debug(status.params.status.value)
223
+ elif message.topic.endswith("/app/down/thing/properties"):
224
+ properties = ThingPropertiesMessage(**payload)
225
+ logger.debug("properties: %s", properties)
226
+ else:
227
+ logger.debug("Unhandled topic: %s", message.topic)
228
+ logger.debug(payload)
229
+
230
+ async def send_cloud_command(self, iot_id: str, command: bytes) -> str:
231
+ """Return internal cloud client."""
232
+ return await self._cloud_client.send_cloud_command(iot_id, command)