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.
- pymammotion/__init__.py +5 -4
- pymammotion/aliyun/client.py +235 -0
- pymammotion/aliyun/cloud_gateway.py +312 -64
- pymammotion/aliyun/model/aep_response.py +1 -2
- pymammotion/aliyun/model/dev_by_account_response.py +170 -23
- pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
- pymammotion/aliyun/model/regions_response.py +3 -3
- pymammotion/aliyun/model/session_by_authcode_response.py +2 -2
- pymammotion/aliyun/model/thing_response.py +12 -0
- pymammotion/aliyun/regions.py +62 -0
- pymammotion/aliyun/tea/core.py +297 -0
- pymammotion/bluetooth/ble.py +7 -9
- pymammotion/bluetooth/ble_message.py +10 -14
- pymammotion/const.py +3 -0
- pymammotion/data/model/__init__.py +1 -2
- pymammotion/data/model/device.py +95 -27
- pymammotion/data/model/device_config.py +4 -4
- pymammotion/data/model/device_info.py +35 -0
- pymammotion/data/model/device_limits.py +10 -10
- pymammotion/data/model/enums.py +12 -2
- pymammotion/data/model/errors.py +12 -0
- pymammotion/data/model/events.py +14 -0
- pymammotion/data/model/generate_geojson.py +521 -0
- pymammotion/data/model/generate_route_information.py +2 -2
- pymammotion/data/model/hash_list.py +370 -57
- pymammotion/data/model/location.py +4 -4
- pymammotion/data/model/mowing_modes.py +17 -1
- pymammotion/data/model/raw_data.py +2 -10
- pymammotion/data/model/region_data.py +10 -11
- pymammotion/data/model/report_info.py +31 -5
- pymammotion/data/model/work.py +27 -0
- pymammotion/data/mower_state_manager.py +316 -0
- pymammotion/data/mqtt/event.py +73 -28
- pymammotion/data/mqtt/mammotion_properties.py +257 -0
- pymammotion/data/mqtt/properties.py +93 -78
- pymammotion/data/mqtt/status.py +18 -17
- pymammotion/event/event.py +27 -6
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +484 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/encryption.py +5 -6
- pymammotion/http/http.py +574 -28
- pymammotion/http/model/__init__.py +0 -0
- pymammotion/{aliyun/model/stream_subscription_response.py → http/model/camera_stream.py} +14 -2
- pymammotion/http/model/http.py +129 -4
- pymammotion/http/model/response_factory.py +61 -0
- pymammotion/http/model/rtk.py +16 -0
- pymammotion/mammotion/commands/abstract_message.py +7 -5
- pymammotion/mammotion/commands/mammotion_command.py +30 -1
- pymammotion/mammotion/commands/messages/basestation.py +43 -0
- pymammotion/mammotion/commands/messages/driver.py +61 -29
- pymammotion/mammotion/commands/messages/media.py +68 -15
- pymammotion/mammotion/commands/messages/navigation.py +61 -25
- pymammotion/mammotion/commands/messages/network.py +17 -23
- pymammotion/mammotion/commands/messages/ota.py +18 -18
- pymammotion/mammotion/commands/messages/system.py +32 -49
- pymammotion/mammotion/commands/messages/video.py +15 -16
- pymammotion/mammotion/devices/__init__.py +27 -3
- pymammotion/mammotion/devices/base.py +40 -131
- pymammotion/mammotion/devices/mammotion.py +436 -201
- pymammotion/mammotion/devices/mammotion_bluetooth.py +57 -47
- pymammotion/mammotion/devices/mammotion_cloud.py +134 -105
- pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
- pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
- pymammotion/mammotion/devices/managers/managers.py +81 -0
- pymammotion/mammotion/devices/mower_device.py +124 -0
- pymammotion/mammotion/devices/mower_manager.py +107 -0
- pymammotion/mammotion/devices/rtk_ble.py +89 -0
- pymammotion/mammotion/devices/rtk_cloud.py +113 -0
- pymammotion/mammotion/devices/rtk_device.py +50 -0
- pymammotion/mammotion/devices/rtk_manager.py +122 -0
- pymammotion/mqtt/__init__.py +2 -1
- pymammotion/mqtt/aliyun_mqtt.py +232 -0
- pymammotion/mqtt/linkkit/__init__.py +5 -0
- pymammotion/mqtt/linkkit/h2client.py +585 -0
- pymammotion/mqtt/linkkit/linkkit.py +3023 -0
- pymammotion/mqtt/mammotion_mqtt.py +176 -169
- pymammotion/mqtt/mqtt_models.py +66 -0
- pymammotion/proto/__init__.py +4839 -4
- pymammotion/proto/basestation.proto +8 -0
- pymammotion/proto/basestation_pb2.py +11 -9
- pymammotion/proto/basestation_pb2.pyi +16 -2
- pymammotion/proto/dev_net.proto +79 -55
- pymammotion/proto/dev_net_pb2.py +60 -56
- pymammotion/proto/dev_net_pb2.pyi +49 -6
- pymammotion/proto/luba_msg.proto +2 -1
- pymammotion/proto/luba_msg_pb2.py +6 -6
- pymammotion/proto/luba_msg_pb2.pyi +1 -0
- pymammotion/proto/luba_mul.proto +62 -1
- pymammotion/proto/luba_mul_pb2.py +38 -22
- pymammotion/proto/luba_mul_pb2.pyi +94 -7
- pymammotion/proto/mctrl_driver.proto +44 -4
- pymammotion/proto/mctrl_driver_pb2.py +26 -14
- pymammotion/proto/mctrl_driver_pb2.pyi +66 -11
- pymammotion/proto/mctrl_nav.proto +93 -52
- pymammotion/proto/mctrl_nav_pb2.py +75 -67
- pymammotion/proto/mctrl_nav_pb2.pyi +142 -56
- pymammotion/proto/mctrl_ota.proto +40 -2
- pymammotion/proto/mctrl_ota_pb2.py +23 -13
- pymammotion/proto/mctrl_ota_pb2.pyi +67 -4
- pymammotion/proto/mctrl_pept.proto +8 -3
- pymammotion/proto/mctrl_pept_pb2.py +8 -6
- pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
- pymammotion/proto/mctrl_sys.proto +325 -86
- pymammotion/proto/mctrl_sys_pb2.py +162 -98
- pymammotion/proto/mctrl_sys_pb2.pyi +451 -25
- pymammotion/proto/message_pool.py +3 -0
- pymammotion/proto/py.typed +0 -0
- pymammotion/utility/constant/device_constant.py +29 -5
- pymammotion/utility/datatype_converter.py +13 -12
- pymammotion/utility/device_config.py +522 -130
- pymammotion/utility/device_type.py +218 -21
- pymammotion/utility/map.py +238 -51
- pymammotion/utility/mur_mur_hash.py +159 -0
- {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/METADATA +26 -31
- pymammotion-0.5.51.dist-info/RECORD +152 -0
- {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/WHEEL +1 -1
- pymammotion/aliyun/cloud_service.py +0 -65
- pymammotion/data/model/plan.py +0 -58
- pymammotion/data/state_manager.py +0 -129
- pymammotion/proto/basestation.py +0 -59
- pymammotion/proto/common.py +0 -12
- pymammotion/proto/dev_net.py +0 -381
- pymammotion/proto/luba_msg.py +0 -81
- pymammotion/proto/luba_mul.py +0 -76
- pymammotion/proto/mctrl_driver.py +0 -100
- pymammotion/proto/mctrl_nav.py +0 -664
- pymammotion/proto/mctrl_ota.py +0 -48
- pymammotion/proto/mctrl_pept.py +0 -41
- pymammotion/proto/mctrl_sys.py +0 -574
- pymammotion-0.4.0a2.dist-info/RECORD +0 -131
- /pymammotion/http/{_init_.py → __init__.py} +0 -0
- {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")
|