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
@@ -0,0 +1,39 @@
1
+ """Mower device with cloud MQTT connectivity."""
2
+
3
+ import logging
4
+
5
+ from pymammotion.aliyun.model.dev_by_account_response import Device
6
+ from pymammotion.data.mower_state_manager import MowerStateManager
7
+ from pymammotion.mammotion.devices.mammotion_cloud import MammotionBaseCloudDevice, MammotionCloud
8
+ from pymammotion.mammotion.devices.mower_device import MammotionMowerDevice
9
+
10
+ _LOGGER = logging.getLogger(__name__)
11
+
12
+
13
+ class MammotionMowerCloudDevice(MammotionBaseCloudDevice, MammotionMowerDevice):
14
+ """Mower device with cloud connectivity and map synchronization."""
15
+
16
+ def __init__(self, mqtt: MammotionCloud, cloud_device: Device, state_manager: MowerStateManager) -> None:
17
+ """Initialize MammotionMowerCloudDevice.
18
+
19
+ Uses multiple inheritance to combine:
20
+ - MammotionBaseCloudDevice: MQTT communication
21
+ - MammotionMowerDevice: Map sync callbacks
22
+ """
23
+ # Initialize base cloud device (which also initializes MammotionBaseDevice)
24
+ super().__init__(mqtt, cloud_device, state_manager)
25
+ # Initialize mower device callbacks (but skip base device init as it's already done)
26
+ # We manually set the callbacks that MammotionMowerDevice would set
27
+ self._state_manager.cloud_gethash_ack_callback = self.datahash_response
28
+ self._state_manager.cloud_get_commondata_ack_callback = self.commdata_response
29
+ self._state_manager.cloud_get_plan_callback = self.plan_callback
30
+
31
+ def __del__(self) -> None:
32
+ """Cleanup subscriptions and callbacks."""
33
+ # Clean up mower-specific callbacks
34
+ if hasattr(self, "_state_manager"):
35
+ self._state_manager.cloud_gethash_ack_callback = None
36
+ self._state_manager.cloud_get_commondata_ack_callback = None
37
+ self._state_manager.cloud_get_plan_callback = None
38
+ # Call parent cleanup
39
+ super().__del__()
@@ -0,0 +1,81 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from bleak import BLEDevice
4
+
5
+ from pymammotion import CloudIOTGateway
6
+ from pymammotion.aliyun.model.dev_by_account_response import Device
7
+ from pymammotion.data.model.device import MowingDevice, RTKDevice
8
+ from pymammotion.data.model.enums import ConnectionPreference
9
+ from pymammotion.mammotion.devices.mammotion_bluetooth import MammotionBaseBLEDevice
10
+ from pymammotion.mammotion.devices.mammotion_cloud import MammotionBaseCloudDevice, MammotionCloud
11
+
12
+
13
+ class AbstractDeviceManager(ABC):
14
+ """Abstract base class for device managers."""
15
+
16
+ def __init__(
17
+ self,
18
+ name: str,
19
+ iot_id: str,
20
+ cloud_client: CloudIOTGateway,
21
+ cloud_device: Device,
22
+ preference: ConnectionPreference = ConnectionPreference.BLUETOOTH,
23
+ ) -> None:
24
+ self.name = name
25
+ self.iot_id = iot_id
26
+ self.cloud_client = cloud_client
27
+ self._device: Device = cloud_device
28
+ self.mammotion_http = cloud_client.mammotion_http
29
+ self.preference = preference
30
+
31
+ @property
32
+ @abstractmethod
33
+ def state(self) -> MowingDevice | RTKDevice:
34
+ """Return the state of the device."""
35
+
36
+ @state.setter
37
+ @abstractmethod
38
+ def state(self, value: MowingDevice | RTKDevice) -> None:
39
+ """Set the device state."""
40
+
41
+ @property
42
+ @abstractmethod
43
+ def ble(self) -> MammotionBaseBLEDevice | None:
44
+ """Return BLE device interface."""
45
+
46
+ @property
47
+ @abstractmethod
48
+ def cloud(self) -> MammotionBaseCloudDevice | None:
49
+ """Return cloud device interface."""
50
+
51
+ @abstractmethod
52
+ def has_queued_commands(self) -> bool:
53
+ """Check if there are queued commands."""
54
+
55
+ @abstractmethod
56
+ def add_ble(self, ble_device: BLEDevice) -> MammotionBaseBLEDevice:
57
+ """Add BLE device."""
58
+
59
+ @abstractmethod
60
+ def add_cloud(self, mqtt: MammotionCloud) -> MammotionBaseCloudDevice:
61
+ """Add cloud device."""
62
+
63
+ @abstractmethod
64
+ def replace_cloud(self, cloud_device: MammotionBaseCloudDevice) -> None:
65
+ """Replace cloud device."""
66
+
67
+ @abstractmethod
68
+ def remove_cloud(self) -> None:
69
+ """Remove cloud device."""
70
+
71
+ @abstractmethod
72
+ def replace_ble(self, ble_device: MammotionBaseBLEDevice) -> None:
73
+ """Replace BLE device."""
74
+
75
+ @abstractmethod
76
+ def remove_ble(self) -> None:
77
+ """Remove BLE device."""
78
+
79
+ @abstractmethod
80
+ def replace_mqtt(self, mqtt: MammotionCloud) -> None:
81
+ """Replace MQTT connection."""
@@ -0,0 +1,124 @@
1
+ """Mower-specific device class with map synchronization callbacks."""
2
+
3
+ from abc import ABC
4
+ import logging
5
+
6
+ from pymammotion.aliyun.model.dev_by_account_response import Device
7
+ from pymammotion.data.model import RegionData
8
+ from pymammotion.data.mower_state_manager import MowerStateManager
9
+ from pymammotion.mammotion.devices.base import MammotionBaseDevice
10
+ from pymammotion.proto import NavGetCommDataAck, NavGetHashListAck, NavPlanJobSet, SvgMessageAckT
11
+ from pymammotion.utility.device_type import DeviceType
12
+
13
+ _LOGGER = logging.getLogger(__name__)
14
+
15
+
16
+ def find_next_integer(lst: list[int], current_hash: int) -> int | None:
17
+ """Find the next integer in a list after the current hash."""
18
+ try:
19
+ current_index = lst.index(current_hash)
20
+ if current_index + 1 < len(lst):
21
+ return lst[current_index + 1]
22
+ else:
23
+ return None
24
+ except ValueError:
25
+ return None
26
+
27
+
28
+ class MammotionMowerDevice(MammotionBaseDevice, ABC):
29
+ """Mower device with map synchronization support."""
30
+
31
+ def __init__(self, state_manager: MowerStateManager, cloud_device: Device) -> None:
32
+ """Initialize MammotionMowerDevice."""
33
+ super().__init__(state_manager, cloud_device)
34
+ # Register mower-specific callbacks
35
+ self._state_manager.cloud_gethash_ack_callback = self.datahash_response
36
+ self._state_manager.cloud_get_commondata_ack_callback = self.commdata_response
37
+ self._state_manager.cloud_get_plan_callback = self.plan_callback
38
+
39
+ async def datahash_response(self, hash_ack: NavGetHashListAck) -> None:
40
+ """Handle datahash responses for root level hashs."""
41
+ current_frame = hash_ack.current_frame
42
+
43
+ missing_frames = self.mower.map.missing_root_hash_frame(hash_ack)
44
+ if len(missing_frames) == 0:
45
+ if len(self.mower.map.missing_hashlist(hash_ack.sub_cmd)) > 0:
46
+ data_hash = self.mower.map.missing_hashlist(hash_ack.sub_cmd).pop(0)
47
+ await self.queue_command("synchronize_hash_data", hash_num=data_hash)
48
+ return
49
+
50
+ if current_frame != missing_frames[0] - 1:
51
+ current_frame = missing_frames[0] - 1
52
+ await self.queue_command("get_hash_response", total_frame=hash_ack.total_frame, current_frame=current_frame)
53
+
54
+ async def commdata_response(self, common_data: NavGetCommDataAck | SvgMessageAckT) -> None:
55
+ """Handle common data responses."""
56
+ total_frame = common_data.total_frame
57
+ current_frame = common_data.current_frame
58
+
59
+ missing_frames = self.mower.map.missing_frame(common_data)
60
+ if len(missing_frames) == 0:
61
+ # get next in hash ack list
62
+ data_hash = (
63
+ self.mower.map.missing_hashlist(common_data.sub_cmd).pop(0)
64
+ if len(self.mower.map.missing_hashlist(common_data.sub_cmd)) > 0
65
+ else None
66
+ )
67
+ if data_hash is None:
68
+ return
69
+
70
+ await self.queue_command("synchronize_hash_data", hash_num=data_hash)
71
+ else:
72
+ if current_frame != missing_frames[0] - 1:
73
+ current_frame = missing_frames[0] - 1
74
+
75
+ region_data = RegionData()
76
+ region_data.hash = common_data.data_hash if isinstance(common_data, SvgMessageAckT) else common_data.hash
77
+ region_data.action = common_data.action if isinstance(common_data, NavGetCommDataAck) else 0
78
+ region_data.type = common_data.type
79
+ region_data.sub_cmd = common_data.sub_cmd
80
+ region_data.total_frame = total_frame
81
+ region_data.current_frame = current_frame
82
+ await self.queue_command("get_regional_data", regional_data=region_data)
83
+
84
+ async def plan_callback(self, plan: NavPlanJobSet) -> None:
85
+ """Handle plan job responses."""
86
+ if plan.plan_index < plan.total_plan_num - 1:
87
+ index = plan.plan_index + 1
88
+ await self.queue_command("read_plan", sub_cmd=2, plan_index=index)
89
+
90
+ async def start_schedule_sync(self) -> None:
91
+ """Start sync of schedule data."""
92
+ if len(self.mower.map.plan) == 0 or list(self.mower.map.plan.values())[0].total_plan_num != len(
93
+ self.mower.map.plan
94
+ ):
95
+ await self.queue_command("read_plan", sub_cmd=2, plan_index=0)
96
+
97
+ async def start_map_sync(self) -> None:
98
+ """Start sync of map data."""
99
+ if location := next((loc for loc in self.mower.report_data.locations if loc.pos_type == 5), None):
100
+ self.mower.map.update_hash_lists(self.mower.map.hashlist, location.bol_hash)
101
+
102
+ await self.queue_command("send_todev_ble_sync", sync_type=3)
103
+
104
+ # TODO correctly check if area names exist for a zone.
105
+ if self._cloud_device and len(self.mower.map.area_name) == 0 and not DeviceType.is_luba1(self.mower.name):
106
+ await self.queue_command("get_area_name_list", device_id=self._cloud_device.iot_id)
107
+
108
+ if len(self.mower.map.root_hash_lists) == 0 or len(self.mower.map.missing_hashlist()) > 0:
109
+ await self.queue_command("get_all_boundary_hash_list", sub_cmd=0)
110
+
111
+ for hash_id, frame in list(self.mower.map.area.items()):
112
+ missing_frames = self.mower.map.find_missing_frames(frame)
113
+ if len(missing_frames) > 0:
114
+ del self.mower.map.area[hash_id]
115
+
116
+ for hash_id, frame in list(self.mower.map.path.items()):
117
+ missing_frames = self.mower.map.find_missing_frames(frame)
118
+ if len(missing_frames) > 0:
119
+ del self.mower.map.path[hash_id]
120
+
121
+ for hash_id, frame in list(self.mower.map.obstacle.items()):
122
+ missing_frames = self.mower.map.find_missing_frames(frame)
123
+ if len(missing_frames) > 0:
124
+ del self.mower.map.obstacle[hash_id]
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ from bleak import BLEDevice
4
+
5
+ from pymammotion import CloudIOTGateway
6
+ from pymammotion.aliyun.model.dev_by_account_response import Device
7
+ from pymammotion.data.model.device import MowingDevice
8
+ from pymammotion.data.model.enums import ConnectionPreference
9
+ from pymammotion.data.mower_state_manager import MowerStateManager
10
+ from pymammotion.mammotion.devices.mammotion_cloud import MammotionCloud
11
+ from pymammotion.mammotion.devices.mammotion_mower_ble import MammotionMowerBLEDevice
12
+ from pymammotion.mammotion.devices.mammotion_mower_cloud import MammotionMowerCloudDevice
13
+ from pymammotion.mammotion.devices.managers.managers import AbstractDeviceManager
14
+
15
+
16
+ class MammotionMowerDeviceManager(AbstractDeviceManager):
17
+ def __init__(
18
+ self,
19
+ name: str,
20
+ iot_id: str,
21
+ cloud_client: CloudIOTGateway,
22
+ cloud_device: Device,
23
+ ble_device: BLEDevice | None = None,
24
+ mqtt: MammotionCloud | None = None,
25
+ preference: ConnectionPreference = ConnectionPreference.BLUETOOTH,
26
+ ) -> None:
27
+ super().__init__(name, iot_id, cloud_client, cloud_device, preference)
28
+ self._ble_device: MammotionMowerBLEDevice | None = None
29
+ self._cloud_device: MammotionMowerCloudDevice | None = None
30
+
31
+ self._state_manager = MowerStateManager(MowingDevice())
32
+ self._state_manager.get_device().name = name
33
+ self.add_ble(ble_device) if ble_device else None
34
+ self.add_cloud(mqtt) if mqtt else None
35
+
36
+ @property
37
+ def state_manager(self) -> MowerStateManager:
38
+ """Return the state manager."""
39
+ return self._state_manager
40
+
41
+ @property
42
+ def state(self) -> MowingDevice:
43
+ """Return the state of the device."""
44
+ return self._state_manager.get_device()
45
+
46
+ @state.setter
47
+ def state(self, value: MowingDevice) -> None:
48
+ self._state_manager.set_device(value)
49
+
50
+ @property
51
+ def ble(self) -> MammotionMowerBLEDevice | None:
52
+ return self._ble_device
53
+
54
+ @property
55
+ def cloud(self) -> MammotionMowerCloudDevice | None:
56
+ return self._cloud_device
57
+
58
+ def has_queued_commands(self) -> bool:
59
+ if self.cloud and self.preference == ConnectionPreference.WIFI:
60
+ return not self.cloud.mqtt.command_queue.empty()
61
+ elif self.ble:
62
+ return not self.ble.command_queue.empty()
63
+ return False
64
+
65
+ def add_ble(self, ble_device: BLEDevice) -> MammotionMowerBLEDevice:
66
+ self._ble_device = MammotionMowerBLEDevice(
67
+ state_manager=self._state_manager, cloud_device=self._device, device=ble_device
68
+ )
69
+ return self._ble_device
70
+
71
+ def add_cloud(self, mqtt: MammotionCloud) -> MammotionMowerCloudDevice:
72
+ self._cloud_device = MammotionMowerCloudDevice(
73
+ mqtt, cloud_device=self._device, state_manager=self._state_manager
74
+ )
75
+ return self._cloud_device
76
+
77
+ def replace_cloud(self, cloud_device: MammotionMowerCloudDevice) -> None:
78
+ self._cloud_device = cloud_device
79
+
80
+ def remove_cloud(self) -> None:
81
+ self._state_manager.cloud_get_commondata_ack_callback = None
82
+ self._state_manager.cloud_get_hashlist_ack_callback = None
83
+ self._state_manager.cloud_get_plan_callback = None
84
+ self._state_manager.cloud_on_notification_callback = None
85
+ self._state_manager.cloud_gethash_ack_callback = None
86
+ self._cloud_device = None
87
+
88
+ def replace_ble(self, ble_device: MammotionMowerBLEDevice) -> None:
89
+ self._ble_device = ble_device
90
+
91
+ def remove_ble(self) -> None:
92
+ self._state_manager.ble_get_commondata_ack_callback = None
93
+ self._state_manager.ble_get_hashlist_ack_callback = None
94
+ self._state_manager.ble_get_plan_callback = None
95
+ self._state_manager.ble_on_notification_callback = None
96
+ self._state_manager.ble_gethash_ack_callback = None
97
+ self._ble_device = None
98
+
99
+ def replace_mqtt(self, mqtt: MammotionCloud) -> None:
100
+ device = self._cloud_device.device
101
+ self._cloud_device = MammotionMowerCloudDevice(mqtt, cloud_device=device, state_manager=self._state_manager)
102
+
103
+ def has_cloud(self) -> bool:
104
+ return self._cloud_device is not None
105
+
106
+ def has_ble(self) -> bool:
107
+ return self._ble_device is not None
@@ -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.iot_id != 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.iot_id != 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")