pymammotion 0.2.34__py3-none-any.whl → 0.2.36__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.
@@ -586,6 +586,7 @@ class CloudIOTGateway:
586
586
  request=request,
587
587
  version="1.0",
588
588
  )
589
+ logger.debug(self.converter.printBase64Binary(command))
589
590
 
590
591
  # send request
591
592
  response = client.do_request("/thing/service/invoke", "https", "POST", None, body, RuntimeOptions())
@@ -1,5 +1,5 @@
1
- from dataclasses import dataclass, field
2
- from datetime import time
1
+ from dataclasses import dataclass
2
+ from typing import Optional
3
3
 
4
4
  from mashumaro.mixins.orjson import DataClassORJSONMixin
5
5
 
@@ -16,4 +16,4 @@ class SessionOauthToken(DataClassORJSONMixin):
16
16
  @dataclass
17
17
  class SessionByAuthCodeResponse(DataClassORJSONMixin):
18
18
  code: int
19
- data: SessionOauthToken
19
+ data: Optional[SessionOauthToken]
@@ -43,7 +43,7 @@ def create_path_order(operation_mode: OperationSettings, device_name: str) -> st
43
43
  bArr = bytearray(8)
44
44
  bArr[0] = operation_mode.border_mode
45
45
  bArr[1] = operation_mode.obstacle_laps
46
- bArr[3] = operation_mode.start_progress
46
+ bArr[3] = int(operation_mode.start_progress)
47
47
  bArr[2] = 0
48
48
 
49
49
  if not DeviceType.is_luba1(device_name):
@@ -54,7 +54,7 @@ def create_path_order(operation_mode: OperationSettings, device_name: str) -> st
54
54
  i = 0
55
55
  bArr[5] = i
56
56
  if operation_mode.is_dump:
57
- b = operation_mode.collect_grass_frequency
57
+ b = int(operation_mode.collect_grass_frequency)
58
58
  else:
59
59
  b = 10
60
60
  bArr[6] = b
@@ -1,7 +1,7 @@
1
1
  from dataclasses import dataclass, field
2
2
  from enum import IntEnum
3
3
 
4
- from pymammotion.proto.mctrl_nav import NavGetCommDataAck, AreaHashName
4
+ from pymammotion.proto.mctrl_nav import AreaHashName, NavGetCommDataAck
5
5
 
6
6
 
7
7
  class PathType(IntEnum):
@@ -24,6 +24,7 @@ class HashList:
24
24
  [hashID, FrameList].
25
25
  hashlist for all our hashIDs for verification
26
26
  """
27
+
27
28
  area: dict # type 0
28
29
  path: dict # type 2
29
30
  obstacle: dict # type 1
@@ -1,7 +1,6 @@
1
- import base64
2
1
  from base64 import b64decode
3
2
  from dataclasses import dataclass
4
- from typing import Any, Literal, Optional, Union, Mapping
3
+ from typing import Any, Literal, Optional, Union
5
4
 
6
5
  from google.protobuf import json_format
7
6
  from mashumaro.mixins.orjson import DataClassORJSONMixin
@@ -40,8 +39,6 @@ class DeviceProtobufMsgEventValue(DataClassORJSONMixin):
40
39
  content: str
41
40
 
42
41
 
43
-
44
-
45
42
  @dataclass
46
43
  class DeviceWarningEventValue(DataClassORJSONMixin):
47
44
  # TODO: enum for error codes
@@ -64,7 +61,7 @@ class DeviceNotificationEventCode(DataClassORJSONMixin):
64
61
 
65
62
  @dataclass
66
63
  class DeviceNotificationEventValue(DataClassORJSONMixin):
67
- data: str # parsed to DeviceNotificationEventCode
64
+ data: str # parsed to DeviceNotificationEventCode
68
65
 
69
66
 
70
67
  @dataclass
@@ -1,12 +1,12 @@
1
1
  """Manage state from notifications into MowingDevice."""
2
2
 
3
- from typing import Optional, Callable, Awaitable
3
+ from typing import Any, Awaitable, Callable, Optional
4
4
 
5
5
  import betterproto
6
6
 
7
7
  from pymammotion.data.model.device import MowingDevice
8
8
  from pymammotion.proto.luba_msg import LubaMsg
9
- from pymammotion.proto.mctrl_nav import NavGetCommDataAck, NavGetHashListAck, AppGetAllAreaHashName
9
+ from pymammotion.proto.mctrl_nav import AppGetAllAreaHashName, NavGetCommDataAck, NavGetHashListAck
10
10
 
11
11
 
12
12
  class StateManager:
@@ -19,6 +19,7 @@ class StateManager:
19
19
  self.gethash_ack_callback: Optional[Callable[[NavGetHashListAck], Awaitable[None]]] = None
20
20
  self.get_commondata_ack_callback: Optional[Callable[[NavGetCommDataAck], Awaitable[None]]] = None
21
21
  self.on_notification_callback: Optional[Callable[[], Awaitable[None]]] = None
22
+ self.queue_command_callback: Optional[Callable[[str, dict[str, Any]], Awaitable[bytes]]] = None
22
23
 
23
24
  def get_device(self) -> MowingDevice:
24
25
  """Get device."""
@@ -36,7 +37,7 @@ class StateManager:
36
37
  case "nav":
37
38
  await self._update_nav_data(message)
38
39
  case "sys":
39
- self._update_sys_data(message)
40
+ await self._update_sys_data(message)
40
41
  case "driver":
41
42
  self._update_driver_data(message)
42
43
  case "net":
@@ -66,8 +67,7 @@ class StateManager:
66
67
  hash_names: AppGetAllAreaHashName = nav_msg[1]
67
68
  self._device.map.area_name = hash_names.hashnames
68
69
 
69
-
70
- def _update_sys_data(self, message) -> None:
70
+ async def _update_sys_data(self, message) -> None:
71
71
  """Update system."""
72
72
  sys_msg = betterproto.which_one_of(message.sys, "SubSysMsg")
73
73
  match sys_msg[0]:
@@ -75,6 +75,8 @@ class StateManager:
75
75
  self._device.buffer(sys_msg[1])
76
76
  case "toapp_report_data":
77
77
  self._device.update_report_data(sys_msg[1])
78
+ if self.queue_command_callback:
79
+ await self.queue_command_callback("get_report_cfg", stop=True)
78
80
  case "mow_to_app_info":
79
81
  self._device.mow_info(sys_msg[1])
80
82
  case "system_tard_state_tunnel":
@@ -4,8 +4,20 @@ from abc import ABC
4
4
 
5
5
  from pymammotion.mammotion.commands.abstract_message import AbstractMessage
6
6
  from pymammotion.mammotion.commands.messages.navigation import MessageNavigation
7
- from pymammotion.proto import luba_msg_pb2, mctrl_sys, mctrl_sys_pb2
8
- from pymammotion.proto.mctrl_sys import RptInfoType
7
+ from pymammotion.proto.luba_msg import LubaMsg, MsgAttr, MsgCmdType, MsgDevice
8
+ from pymammotion.proto.mctrl_sys import (
9
+ DeviceProductTypeInfoT,
10
+ LoraCfgReq,
11
+ MctlSys,
12
+ MCtrlSimulationCmdData,
13
+ ReportInfoCfg,
14
+ RptAct,
15
+ RptInfoType,
16
+ SysCommCmd,
17
+ SysKnifeControl,
18
+ SysSetDateTime,
19
+ TimeCtrlLight,
20
+ )
9
21
  from pymammotion.utility.device_type import DeviceType
10
22
 
11
23
 
@@ -13,37 +25,35 @@ class MessageSystem(AbstractMessage, ABC):
13
25
  messageNavigation: MessageNavigation = MessageNavigation()
14
26
 
15
27
  def send_order_msg_sys(self, sys):
16
- luba_msg = luba_msg_pb2.LubaMsg(
17
- msgtype=luba_msg_pb2.MSG_CMD_TYPE_EMBED_SYS,
18
- sender=luba_msg_pb2.DEV_MOBILEAPP,
19
- rcver=luba_msg_pb2.DEV_MAINCTL,
28
+ luba_msg = LubaMsg(
29
+ msgtype=MsgCmdType.MSG_CMD_TYPE_EMBED_SYS,
30
+ sender=MsgDevice.DEV_MOBILEAPP,
31
+ rcver=MsgDevice.DEV_MAINCTL,
20
32
  sys=sys,
21
33
  )
22
34
 
23
35
  return luba_msg.SerializeToString()
24
36
 
25
37
  def reset_system(self):
26
- build = mctrl_sys_pb2.MctlSys(todev_reset_system=1)
38
+ build = MctlSys(todev_reset_system=1)
27
39
  print("Send command - send factory reset")
28
40
  return self.send_order_msg_sys(build)
29
41
 
30
42
  def set_blade_control(self, on_off: int):
31
- mctlsys = mctrl_sys_pb2.MctlSys()
32
- sys_knife_control = mctrl_sys_pb2.SysKnifeControl()
43
+ mctlsys = MctlSys()
44
+ sys_knife_control = SysKnifeControl()
33
45
  sys_knife_control.knife_status = on_off
34
- mctlsys.todev_knife_ctrl.CopyFrom(sys_knife_control)
46
+ mctlsys.todev_knife_ctrl = sys_knife_control
35
47
 
36
48
  return self.send_order_msg_sys(mctlsys)
37
49
 
38
50
  def get_device_product_model(self):
39
- return self.send_order_msg_sys(
40
- mctrl_sys_pb2.MctlSys(device_product_type_info=mctrl_sys_pb2.device_product_type_info_t())
41
- )
51
+ return self.send_order_msg_sys(MctlSys(device_product_type_info=DeviceProductTypeInfoT()))
42
52
 
43
53
  def read_and_set_sidelight(self, is_sidelight: bool, operate: int):
44
54
  """Read state of sidelight as well as set it."""
45
55
  if is_sidelight:
46
- build = mctrl_sys_pb2.TimeCtrlLight(
56
+ build = TimeCtrlLight(
47
57
  operate=operate,
48
58
  enable=0,
49
59
  action=0,
@@ -53,7 +63,7 @@ class MessageSystem(AbstractMessage, ABC):
53
63
  end_min=0,
54
64
  )
55
65
  else:
56
- build = mctrl_sys_pb2.TimeCtrlLight(
66
+ build = TimeCtrlLight(
57
67
  operate=operate,
58
68
  enable=1,
59
69
  action=0,
@@ -64,16 +74,16 @@ class MessageSystem(AbstractMessage, ABC):
64
74
  )
65
75
  print(f"Send read and write sidelight command is_sidelight:{
66
76
  is_sidelight}, operate:{operate}")
67
- build2 = mctrl_sys_pb2.MctlSys(todev_time_ctrl_light=build)
77
+ build2 = MctlSys(todev_time_ctrl_light=build)
68
78
  print(f"Send command - send read and write sidelight command is_sidelight:{
69
79
  is_sidelight}, operate:{operate}, timeCtrlLight:{build}")
70
80
  return self.send_order_msg_sys(build2)
71
81
 
72
82
  def test_tool_order_to_sys(self, sub_cmd: int, param_id: int, param_value: list[int]):
73
- build = mctrl_sys_pb2.mCtrlSimulationCmdData(sub_cmd=sub_cmd, param_id=param_id, param_value=param_value)
83
+ build = MCtrlSimulationCmdData(sub_cmd=sub_cmd, param_id=param_id, param_value=param_value)
74
84
  print(f"Send tool test command: subCmd={sub_cmd}, param_id:{
75
85
  param_id}, param_value={param_value}")
76
- build2 = mctrl_sys_pb2.MctlSys(simulation_cmd=build)
86
+ build2 = MctlSys(simulation_cmd=build)
77
87
  print(f"Send tool test command: subCmd={sub_cmd}, param_id:{
78
88
  param_id}, param_value={param_value}")
79
89
  return self.send_order_msg_sys(build2)
@@ -81,15 +91,13 @@ class MessageSystem(AbstractMessage, ABC):
81
91
  def read_and_set_rtk_paring_code(self, op: int, cgf: str):
82
92
  print(f"Send read and write base station configuration quality op:{
83
93
  op}, cgf:{cgf}")
84
- return self.send_order_msg_sys(
85
- mctrl_sys_pb2.MctlSys(todev_lora_cfg_req=mctrl_sys_pb2.LoraCfgReq(op=op, cfg=cgf))
86
- )
94
+ return self.send_order_msg_sys(MctlSys(todev_lora_cfg_req=LoraCfgReq(op=op, cfg=cgf)))
87
95
 
88
96
  def allpowerfull_rw(self, id: int, context: int, rw: int):
89
97
  if (id == 6 or id == 3 or id == 7) and DeviceType.is_luba_2(self.get_device_name()):
90
98
  self.messageNavigation.allpowerfull_rw_adapter_x3(id, context, rw)
91
99
  return
92
- build = mctrl_sys_pb2.MctlSys(bidire_comm_cmd=mctrl_sys_pb2.SysCommCmd(id=id, context=context, rw=rw))
100
+ build = MctlSys(bidire_comm_cmd=SysCommCmd(id=id, context=context, rw=rw))
93
101
  print(f"Send command - 9 general read and write command id={id}, context={context}, rw={rw}")
94
102
  if id == 5:
95
103
  # This logic doesnt make snese, but its what they had so..
@@ -98,7 +106,7 @@ class MessageSystem(AbstractMessage, ABC):
98
106
 
99
107
  # Commented out as not needed and too many refs to try fix up
100
108
  # def factory_test_order(self, test_id: int, test_duration: int, expect: str):
101
- # new_builder = mctrl_sys_pb2.mow_to_app_qctools_info_t.Builder()
109
+ # new_builder = mow_to_app_qctools_info_t.Builder()
102
110
  # print(f"Factory tool print, expect={expect}")
103
111
  # if not expect:
104
112
  # build = new_builder.set_type_value(
@@ -108,7 +116,7 @@ class MessageSystem(AbstractMessage, ABC):
108
116
  # json_array = json.loads(expect)
109
117
  # z2 = True
110
118
  # for i in range(len(json_array)):
111
- # new_builder2 = mctrl_sys_pb2.QCAppTestExcept.Builder()
119
+ # new_builder2 = QCAppTestExcept.Builder()
112
120
  # json_object = json_array[i]
113
121
  # if "except_type" in json_object:
114
122
  # string = json_object["except_type"]
@@ -116,7 +124,7 @@ class MessageSystem(AbstractMessage, ABC):
116
124
  # json_array2 = json_object["conditions"]
117
125
  # for i2 in range(len(json_array2)):
118
126
  # json_object2 = json_array2[i2]
119
- # new_builder3 = mctrl_sys_pb2.QCAppTestConditions.Builder()
127
+ # new_builder3 = QCAppTestConditions.Builder()
120
128
  # if "cond_type" in json_object2:
121
129
  # new_builder3.set_cond_type(
122
130
  # json_object2["cond_type"])
@@ -151,7 +159,7 @@ class MessageSystem(AbstractMessage, ABC):
151
159
  # test_id).set_time_of_duration(test_duration).build()
152
160
  # print(f"Factory tool print, mow_to_app_qctools_info_t={
153
161
  # build.except_count}, mow_to_app_qctools_info_t22={build.except_list}")
154
- # build2 = mctrl_sys_pb2.MctlSys(mow_to_app_qctools_info=build)
162
+ # build2 = MctlSys(mow_to_app_qctools_info=build)
155
163
  # print(f"Send command - factory tool test command testId={
156
164
  # test_id}, testDuration={test_duration}", "Factory tool print222", True)
157
165
  # return self.send_order_msg_sys(build2)
@@ -169,8 +177,8 @@ class MessageSystem(AbstractMessage, ABC):
169
177
  i9 = 1 if calendar.dst() else 0
170
178
  print(f"Print time zone, time zone={
171
179
  i8}, daylight saving time={i9} week={i4}")
172
- build = mctrl_sys.MctlSys(
173
- todev_data_time=mctrl_sys.SysSetDateTime(
180
+ build = MctlSys(
181
+ todev_data_time=SysSetDateTime(
174
182
  year=i,
175
183
  month=i2,
176
184
  date=i3,
@@ -191,21 +199,21 @@ class MessageSystem(AbstractMessage, ABC):
191
199
  return self.send_order_msg_sys(build)
192
200
 
193
201
  def get_device_version_info(self):
194
- return self.send_order_msg_sys(mctrl_sys_pb2.MctlSys(todev_get_dev_fw_info=1))
202
+ return self.send_order_msg_sys(MctlSys(todev_get_dev_fw_info=1))
195
203
 
196
204
  # === sendOrderMsg_Sys2 ===
197
205
 
198
206
  def request_iot_sys(
199
207
  self,
200
- rpt_act: mctrl_sys_pb2.rpt_act,
208
+ rpt_act: RptAct,
201
209
  rpt_info_type: list[RptInfoType | str] | None,
202
210
  timeout: int,
203
211
  period: int,
204
212
  no_change_period: int,
205
213
  count: int,
206
214
  ) -> bytes:
207
- build = mctrl_sys_pb2.MctlSys(
208
- todev_report_cfg=mctrl_sys_pb2.report_info_cfg(
215
+ build = MctlSys(
216
+ todev_report_cfg=ReportInfoCfg(
209
217
  act=rpt_act,
210
218
  sub=rpt_info_type,
211
219
  timeout=timeout,
@@ -218,9 +226,12 @@ class MessageSystem(AbstractMessage, ABC):
218
226
  build.todev_report_cfg.act} {build}")
219
227
  return self.send_order_msg_sys(build)
220
228
 
221
- def get_report_cfg(self, timeout: int = 10000, period: int = 1000, no_change_period: int = 2000):
222
- mctlsys = mctrl_sys_pb2.MctlSys(
223
- todev_report_cfg=mctrl_sys_pb2.report_info_cfg(
229
+ def get_report_cfg(
230
+ self, timeout: int = 10000, period: int = 1000, no_change_period: int = 1000, stop: bool = False
231
+ ):
232
+ mctlsys = MctlSys(
233
+ todev_report_cfg=ReportInfoCfg(
234
+ act=RptAct.RPT_STOP if stop else RptAct.RPT_START,
224
235
  timeout=timeout,
225
236
  period=period,
226
237
  no_change_period=no_change_period,
@@ -233,17 +244,18 @@ class MessageSystem(AbstractMessage, ABC):
233
244
  mctlsys.todev_report_cfg.sub.append(RptInfoType.RIT_DEV_LOCAL.value)
234
245
  mctlsys.todev_report_cfg.sub.append(RptInfoType.RIT_WORK.value)
235
246
  mctlsys.todev_report_cfg.sub.append(RptInfoType.RIT_DEV_STA.value)
247
+ mctlsys.todev_report_cfg.sub.append(RptInfoType.RIT_MAINTAIN.value)
236
248
  mctlsys.todev_report_cfg.sub.append(RptInfoType.RIT_VISION_POINT.value)
237
249
  mctlsys.todev_report_cfg.sub.append(RptInfoType.RIT_VIO.value)
238
250
  mctlsys.todev_report_cfg.sub.append(RptInfoType.RIT_VISION_STATISTIC.value)
239
251
 
240
- lubaMsg = luba_msg_pb2.LubaMsg()
241
- lubaMsg.msgtype = luba_msg_pb2.MSG_CMD_TYPE_EMBED_SYS
242
- lubaMsg.sender = luba_msg_pb2.DEV_MOBILEAPP
243
- lubaMsg.rcver = luba_msg_pb2.DEV_MAINCTL
244
- lubaMsg.msgattr = luba_msg_pb2.MSG_ATTR_REQ
252
+ lubaMsg = LubaMsg()
253
+ lubaMsg.msgtype = MsgCmdType.MSG_CMD_TYPE_EMBED_SYS
254
+ lubaMsg.sender = MsgDevice.DEV_MOBILEAPP
255
+ lubaMsg.rcver = MsgDevice.DEV_MAINCTL
256
+ lubaMsg.msgattr = MsgAttr.MSG_ATTR_REQ
245
257
  lubaMsg.seqs = 1
246
258
  lubaMsg.version = 1
247
259
  lubaMsg.subtype = 1
248
- lubaMsg.sys.CopyFrom(mctlsys)
260
+ lubaMsg.sys = mctlsys
249
261
  return lubaMsg.SerializeToString()
@@ -18,7 +18,7 @@ from pymammotion.utility.movement import get_percent, transform_both_speeds
18
18
  _LOGGER = logging.getLogger(__name__)
19
19
 
20
20
 
21
- def find_next_integer(lst: list[int], current_hash: float) -> int | None:
21
+ def find_next_integer(lst: list[int], current_hash: int) -> int | None:
22
22
  try:
23
23
  # Find the index of the current integer
24
24
  current_index = lst.index(current_hash)
@@ -54,6 +54,9 @@ class MammotionBaseDevice:
54
54
  def set_notification_callback(self, func: Callable[[], Awaitable[None]]) -> None:
55
55
  self._state_manager.on_notification_callback = func
56
56
 
57
+ def set_queue_callback(self, func: Callable[[str, dict[str, Any]], Awaitable[bytes]]) -> None:
58
+ self._state_manager.queue_command_callback = func
59
+
57
60
  async def datahash_response(self, hash_ack: NavGetHashListAck) -> None:
58
61
  """Handle datahash responses."""
59
62
  await self.queue_command("synchronize_hash_data", hash_num=hash_ack.data_couple[0])
@@ -206,8 +209,9 @@ class MammotionBaseDevice:
206
209
  await self.queue_command("get_area_name_list", device_id=self._cloud_device.deviceName)
207
210
  if has_field(self._mower.net.toapp_wifi_iot_status):
208
211
  if not DeviceType.is_luba1(self._mower.net.toapp_wifi_iot_status.devicename):
209
- await self.queue_command("get_area_name_list",
210
- device_id=self._mower.net.toapp_wifi_iot_status.devicename)
212
+ await self.queue_command(
213
+ "get_area_name_list", device_id=self._mower.net.toapp_wifi_iot_status.devicename
214
+ )
211
215
  except Exception:
212
216
  """Do nothing for now."""
213
217
 
@@ -220,9 +224,6 @@ class MammotionBaseDevice:
220
224
  for data_hash in self.mower.nav.toapp_gethash_ack.data_couple:
221
225
  await self.queue_command("synchronize_hash_data", hash_num=data_hash)
222
226
 
223
-
224
-
225
-
226
227
  # sub_cmd 3 is job hashes??
227
228
  # sub_cmd 4 is dump location (yuka)
228
229
  # jobs list
@@ -37,7 +37,8 @@ class ConnectionPreference(Enum):
37
37
  class MammotionMixedDeviceManager:
38
38
  _ble_device: MammotionBaseBLEDevice | None = None
39
39
  _cloud_device: MammotionBaseCloudDevice | None = None
40
- _mowing_state: MowingDevice = MowingDevice()
40
+ _mowing_state: MowingDevice
41
+ preference: ConnectionPreference
41
42
 
42
43
  def __init__(
43
44
  self,
@@ -45,10 +46,13 @@ class MammotionMixedDeviceManager:
45
46
  cloud_device: Device | None = None,
46
47
  ble_device: BLEDevice | None = None,
47
48
  mqtt: MammotionCloud | None = None,
49
+ preference: ConnectionPreference = ConnectionPreference.BLUETOOTH,
48
50
  ) -> None:
49
51
  self.name = name
52
+ self._mowing_state = MowingDevice()
50
53
  self.add_ble(ble_device)
51
54
  self.add_cloud(cloud_device, mqtt)
55
+ self.preference = preference
52
56
 
53
57
  def mower_state(self):
54
58
  return self._mowing_state
@@ -101,19 +105,30 @@ class MammotionDevices:
101
105
  def get_device(self, mammotion_device_name: str) -> MammotionMixedDeviceManager:
102
106
  return self.devices.get(mammotion_device_name)
103
107
 
108
+ def remove_device(self, name) -> None:
109
+ device_for_removal = self.devices.pop(name)
110
+ if device_for_removal.has_cloud():
111
+ should_disconnect = {
112
+ device
113
+ for key, device in self.devices.items()
114
+ if device.cloud() is not None and device.cloud()._mqtt == device_for_removal.cloud()._mqtt
115
+ }
116
+ if len(should_disconnect) == 0:
117
+ device_for_removal.cloud()._mqtt.disconnect()
118
+
104
119
 
105
120
  async def create_devices(
106
121
  ble_device: BLEDevice,
107
122
  cloud_credentials: Credentials | None = None,
108
123
  preference: ConnectionPreference = ConnectionPreference.BLUETOOTH,
109
124
  ):
110
- mammotion = Mammotion(ble_device, preference)
125
+ mammotion = Mammotion()
126
+ mammotion.add_ble_device(ble_device, preference)
111
127
 
112
128
  if cloud_credentials and preference == ConnectionPreference.EITHER or preference == ConnectionPreference.WIFI:
113
- cloud_client = await Mammotion.login(
129
+ await mammotion.login_and_initiate_cloud(
114
130
  cloud_credentials.account_id or cloud_credentials.email, cloud_credentials.password
115
131
  )
116
- await mammotion.initiate_cloud_connection(cloud_client)
117
132
 
118
133
  return mammotion
119
134
 
@@ -123,26 +138,44 @@ class Mammotion:
123
138
  """Represents a Mammotion account and its devices."""
124
139
 
125
140
  devices = MammotionDevices()
126
- cloud_client: CloudIOTGateway | None = None
127
- mqtt: MammotionCloud | None = None
141
+ mqtt_list: dict[str, MammotionCloud] = dict()
128
142
 
129
- def __init__(
130
- self, ble_device: BLEDevice, preference: ConnectionPreference = ConnectionPreference.BLUETOOTH
131
- ) -> None:
143
+ _instance = None
144
+
145
+ def __new__(cls, *args, **kwargs):
146
+ if not cls._instance:
147
+ cls._instance = super().__new__(cls)
148
+ return cls._instance
149
+
150
+ def __init__(self) -> None:
132
151
  """Initialize MammotionDevice."""
133
- if ble_device:
134
- self.devices.add_device(MammotionMixedDeviceManager(name=ble_device.name, ble_device=ble_device))
152
+ self._login_lock = asyncio.Lock()
135
153
 
136
- if preference:
137
- self._preference = preference
154
+ def add_ble_device(self, ble_device: BLEDevice, preference: ConnectionPreference = ConnectionPreference.BLUETOOTH) -> None:
155
+ if ble_device:
156
+ self.devices.add_device(
157
+ MammotionMixedDeviceManager(name=ble_device.name, ble_device=ble_device, preference=preference)
158
+ )
138
159
 
139
- async def initiate_cloud_connection(self, cloud_client: CloudIOTGateway) -> None:
140
- if self.mqtt is not None:
141
- if self.mqtt.is_connected:
160
+ async def login_and_initiate_cloud(self, account, password, force: bool = False) -> None:
161
+ async with self._login_lock:
162
+ exists: MammotionCloud | None = self.mqtt_list.get(account)
163
+ if not exists or force:
164
+ cloud_client = await self.login(account, password)
165
+ else:
166
+ cloud_client = exists.cloud_client
167
+
168
+ await self.initiate_cloud_connection(account, cloud_client)
169
+
170
+ async def initiate_cloud_connection(self, account: str, cloud_client: CloudIOTGateway) -> None:
171
+ if self.mqtt_list.get(account) is not None:
172
+ if self.mqtt_list.get(account).is_connected:
173
+ # we might have removed a device so readd
174
+ self.add_cloud_devices(self.mqtt_list.get(account))
142
175
  return
143
176
 
144
177
  self.cloud_client = cloud_client
145
- self.mqtt = MammotionCloud(
178
+ self.mqtt_list[account] = MammotionCloud(
146
179
  MammotionMQTT(
147
180
  region_id=cloud_client.region_response.data.regionId,
148
181
  product_key=cloud_client.aep_response.data.productKey,
@@ -151,24 +184,30 @@ class Mammotion:
151
184
  iot_token=cloud_client.session_by_authcode_response.data.iotToken,
152
185
  client_id=cloud_client.client_id,
153
186
  cloud_client=cloud_client,
154
- )
187
+ ),
188
+ cloud_client,
155
189
  )
156
190
 
157
191
  loop = asyncio.get_running_loop()
158
- await loop.run_in_executor(None, self.mqtt.connect_async)
192
+ await loop.run_in_executor(None, self.mqtt_list[account].connect_async)
159
193
 
160
- for device in cloud_client.devices_by_account_response.data.data:
194
+ def add_cloud_devices(self, mqtt_client: MammotionCloud) -> None:
195
+ for device in mqtt_client.cloud_client.devices_by_account_response.data.data:
161
196
  mower_device = self.devices.get_device(device.deviceName)
162
197
  if device.deviceName.startswith(("Luba-", "Yuka-")) and mower_device is None:
163
198
  self.devices.add_device(
164
- MammotionMixedDeviceManager(name=device.deviceName, cloud_device=device, mqtt=self.mqtt)
199
+ MammotionMixedDeviceManager(
200
+ name=device.deviceName,
201
+ cloud_device=device,
202
+ mqtt=mqtt_client,
203
+ preference=ConnectionPreference.WIFI,
204
+ )
165
205
  )
166
206
  elif device.deviceName.startswith(("Luba-", "Yuka-")) and mower_device:
167
207
  if mower_device.cloud() is None:
168
- mower_device.add_cloud(cloud_device=device, mqtt=self.mqtt)
169
- else:
170
- device.replace_mqtt(self.mqtt)
171
-
208
+ mower_device.add_cloud(cloud_device=device, mqtt=mqtt_client)
209
+ elif mqtt_client != mower_device.cloud().mqtt:
210
+ mower_device.replace_mqtt(mqtt_client)
172
211
 
173
212
  def set_disconnect_strategy(self, disconnect: bool) -> None:
174
213
  for device_name, device in self.devices.devices:
@@ -176,8 +215,7 @@ class Mammotion:
176
215
  ble_device: MammotionBaseBLEDevice = device.ble()
177
216
  ble_device.set_disconnect_strategy(disconnect)
178
217
 
179
- @staticmethod
180
- async def login(account: str, password: str) -> CloudIOTGateway:
218
+ async def login(self, account: str, password: str) -> CloudIOTGateway:
181
219
  """Login to mammotion cloud."""
182
220
  cloud_client = CloudIOTGateway()
183
221
  async with ClientSession(MAMMOTION_DOMAIN) as session:
@@ -198,6 +236,9 @@ class Mammotion:
198
236
  await loop.run_in_executor(None, cloud_client.list_binding_by_account)
199
237
  return cloud_client
200
238
 
239
+ def remove_device(self, name: str) -> None:
240
+ self.devices.remove_device(name)
241
+
201
242
  def get_device_by_name(self, name: str) -> MammotionMixedDeviceManager:
202
243
  return self.devices.get_device(name)
203
244
 
@@ -205,9 +246,9 @@ class Mammotion:
205
246
  """Send a command to the device."""
206
247
  device = self.get_device_by_name(name)
207
248
  if device:
208
- if self._preference is ConnectionPreference.BLUETOOTH:
249
+ if device.preference is ConnectionPreference.BLUETOOTH:
209
250
  return await device.ble().command(key)
210
- if self._preference is ConnectionPreference.WIFI:
251
+ if device.preference is ConnectionPreference.WIFI:
211
252
  return await device.cloud().command(key)
212
253
  # TODO work with both with EITHER
213
254
 
@@ -215,27 +256,27 @@ class Mammotion:
215
256
  """Send a command with args to the device."""
216
257
  device = self.get_device_by_name(name)
217
258
  if device:
218
- if self._preference is ConnectionPreference.BLUETOOTH:
259
+ if device.preference is ConnectionPreference.BLUETOOTH:
219
260
  return await device.ble().command(key, **kwargs)
220
- if self._preference is ConnectionPreference.WIFI:
261
+ if device.preference is ConnectionPreference.WIFI:
221
262
  return await device.cloud().command(key, **kwargs)
222
263
  # TODO work with both with EITHER
223
264
 
224
265
  async def start_sync(self, name: str, retry: int):
225
266
  device = self.get_device_by_name(name)
226
267
  if device:
227
- if self._preference is ConnectionPreference.BLUETOOTH:
268
+ if device.preference is ConnectionPreference.BLUETOOTH:
228
269
  return await device.ble().start_sync(retry)
229
- if self._preference is ConnectionPreference.WIFI:
270
+ if device.preference is ConnectionPreference.WIFI:
230
271
  return await device.cloud().start_sync(retry)
231
272
  # TODO work with both with EITHER
232
273
 
233
274
  async def start_map_sync(self, name: str):
234
275
  device = self.get_device_by_name(name)
235
276
  if device:
236
- if self._preference is ConnectionPreference.BLUETOOTH:
277
+ if device.preference is ConnectionPreference.BLUETOOTH:
237
278
  return await device.ble().start_map_sync()
238
- if self._preference is ConnectionPreference.WIFI:
279
+ if device.preference is ConnectionPreference.WIFI:
239
280
  return await device.cloud().start_map_sync()
240
281
  # TODO work with both with EITHER
241
282
 
@@ -88,6 +88,7 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
88
88
  self._connect_lock = asyncio.Lock()
89
89
  self._operation_lock = asyncio.Lock()
90
90
  self._key: str | None = None
91
+ self.set_queue_callback(self.queue_command)
91
92
 
92
93
  def update_device(self, device: BLEDevice) -> None:
93
94
  """Update the BLE device."""
@@ -7,7 +7,7 @@ from typing import Any, Awaitable, Callable, Optional, cast
7
7
 
8
8
  import betterproto
9
9
 
10
- from pymammotion import MammotionMQTT
10
+ from pymammotion import CloudIOTGateway, MammotionMQTT
11
11
  from pymammotion.aliyun.dataclass.dev_by_account_response import Device
12
12
  from pymammotion.data.model.device import MowingDevice
13
13
  from pymammotion.data.mqtt.event import ThingEventMessage
@@ -24,7 +24,8 @@ _LOGGER = logging.getLogger(__name__)
24
24
  class MammotionCloud:
25
25
  """Per account MQTT cloud."""
26
26
 
27
- def __init__(self, mqtt_client: MammotionMQTT) -> None:
27
+ def __init__(self, mqtt_client: MammotionMQTT, cloud_client: CloudIOTGateway) -> None:
28
+ self.cloud_client = cloud_client
28
29
  self.loop = asyncio.get_event_loop()
29
30
  self._ble_sync_task = None
30
31
  self.is_ready = False
@@ -155,6 +156,7 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
155
156
  self.currentID = ""
156
157
  self._mqtt.mqtt_message_event.add_subscribers(self._parse_message_for_device)
157
158
  self._mqtt.on_ready_event.add_subscribers(self.on_ready)
159
+ self.set_queue_callback(self.queue_command)
158
160
 
159
161
  if self._mqtt.is_ready:
160
162
  self.run_periodic_sync_task()
@@ -186,7 +188,6 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
186
188
  160, lambda: asyncio.ensure_future(self.run_periodic_sync_task())
187
189
  )
188
190
 
189
-
190
191
  async def queue_command(self, key: str, **kwargs: Any) -> bytes:
191
192
  # Create a future to hold the result
192
193
  _LOGGER.debug("Queueing command: %s", key)
@@ -245,3 +246,7 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
245
246
  if not fut.fut.cancelled():
246
247
  fut.resolve(cast(bytes, binary_data))
247
248
  await self._state_manager.notification(new_msg)
249
+
250
+ @property
251
+ def mqtt(self):
252
+ return self._mqtt
@@ -95,7 +95,6 @@ class DeviceType(Enum):
95
95
  elif DeviceType.LUBA.get_name() in substring2 or DeviceType.contain_luba_product_key(product_key):
96
96
  return DeviceType.LUBA
97
97
  else:
98
- print("unknown device type")
99
98
  return DeviceType.UNKNOWN
100
99
  except Exception:
101
100
  return DeviceType.UNKNOWN
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pymammotion
3
- Version: 0.2.34
3
+ Version: 0.2.36
4
4
  Summary:
5
5
  License: GNU-3.0
6
6
  Author: Michael Arthur
@@ -1,13 +1,13 @@
1
1
  pymammotion/__init__.py,sha256=jHCQrpJaG1jAoID9T4RT3g4JsZc0JpJqIcqjnA7cXd0,1605
2
2
  pymammotion/aliyun/__init__.py,sha256=T1lkX7TRYiL4nqYanG4l4MImV-SlavSbuooC-W-uUGw,29
3
- pymammotion/aliyun/cloud_gateway.py,sha256=Fktzd7ej9b02hr0Dl82_EgUkDCCzWqcp92_wd8D2vaw,22716
3
+ pymammotion/aliyun/cloud_gateway.py,sha256=X6Y9yyIXCwkPwgRqsBESUI5v6j-CKlJGJaEnMINyp8c,22780
4
4
  pymammotion/aliyun/cloud_service.py,sha256=px7dUKow5Z7VyebjYzuKkzkm77XbUXYiFiYO_2e-UQ0,2207
5
5
  pymammotion/aliyun/dataclass/aep_response.py,sha256=8f6GIP58ve8gd6AL3HBoXxsy0n2q4ygWvjELGnoOnVc,452
6
6
  pymammotion/aliyun/dataclass/connect_response.py,sha256=Yz-fEbDzgGPTo5Of2oAjmFkSv08T7ze80pQU4k-gKIU,824
7
7
  pymammotion/aliyun/dataclass/dev_by_account_response.py,sha256=gskum11h9HPf4lKjLJKVrsxRl5BHaHJP2TPrI09SUYs,1032
8
8
  pymammotion/aliyun/dataclass/login_by_oauth_response.py,sha256=IXSLZ6XnOliOnyXo5Bh0ErqFjA11puACh_9NH0sSJGQ,1262
9
9
  pymammotion/aliyun/dataclass/regions_response.py,sha256=CVPpdFhDD6_emWHyLRzOdp2j3HLPtP8tlNyzGnr8AcI,690
10
- pymammotion/aliyun/dataclass/session_by_authcode_response.py,sha256=1K8Uu_V6flSBU8kLCh1Qj59tD9aZQSn7X3hLKeouE_g,407
10
+ pymammotion/aliyun/dataclass/session_by_authcode_response.py,sha256=FVqT2EmePt4ze83sndkw45GHtMsx12nsMU7Nljd35RM,412
11
11
  pymammotion/aliyun/tmp_constant.py,sha256=M4Hq_lrGB3LZdX6R2XohRPFoK1NDnNV-pTJwJcJ9838,6650
12
12
  pymammotion/bluetooth/__init__.py,sha256=LAl8jqZ1fPh-3mLmViNQsP3s814C1vsocYUa6oSaXt0,36
13
13
  pymammotion/bluetooth/ble.py,sha256=YfkfEK3TLJ8BaidjAXfUVFv8reLCu6U_lYa3Bo0pddw,2449
@@ -22,12 +22,12 @@ pymammotion/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
22
22
  pymammotion/data/model/__init__.py,sha256=aSyroxYQQS-WMRi6WmWm2js4wLa9nmsi160gx9tts4o,323
23
23
  pymammotion/data/model/account.py,sha256=vJM-KTf2q6eBfVC-UlNHBSmJvqHiCawZ40vnuhXhaz8,140
24
24
  pymammotion/data/model/device.py,sha256=ejIMloTHlJcBi-qNFewTDt1K0pPJYpgjtJqqqhUqR1g,11182
25
- pymammotion/data/model/device_config.py,sha256=Y3J2zIrmbk_9GLpscAVxRiIIVm9-Qq7OZ49tOh4Ffqo,2627
25
+ pymammotion/data/model/device_config.py,sha256=Ez55DiYV7QTxgSk6R6XjUSZ7XdTGf8w38VaFsAOBNv8,2637
26
26
  pymammotion/data/model/enums.py,sha256=EpKmO8yVUZyEnTY4yH0DMMVKYNQM42zpW1maUu0i3IE,1582
27
27
  pymammotion/data/model/excute_boarder_params.py,sha256=9CpUqrygcle1C_1hDW-riLmm4map4ZbE842NXjcomEI,1394
28
28
  pymammotion/data/model/execute_boarder.py,sha256=9rd_h4fbcsXxgnLOd2rO2hWyD1abnTGc47QTEpp8DD0,1103
29
29
  pymammotion/data/model/generate_route_information.py,sha256=MkUBoqGtCAKmiVQ4Q1pEoDVHZs5uLIo7vhfWT4nGbtY,801
30
- pymammotion/data/model/hash_list.py,sha256=4nZv5zE-jMJvKYmdr3aaO80qYNJYNDgiHjn-thbGUvs,3024
30
+ pymammotion/data/model/hash_list.py,sha256=fszjpbt0e7DNqibQnPaYCTiuK39gwtNVjBF0hUyMeeE,3025
31
31
  pymammotion/data/model/location.py,sha256=H1h4Rhr0z_mDplNf1CP_ZCA3zM4_FJ_nMqzkbaoh7To,787
32
32
  pymammotion/data/model/mowing_modes.py,sha256=5TrHSijUyPtIDWpNtgzx_vFQukRJWRz4gIrUaXggKPw,827
33
33
  pymammotion/data/model/plan.py,sha256=mcadkSL7fQXy0iJ0q786I3GEQY4i6kmQXfW6Ri69lcQ,2906
@@ -35,10 +35,10 @@ pymammotion/data/model/rapid_state.py,sha256=BdJFD_DlhrVneg-PqEruqCoMty-CR7q_9Qn
35
35
  pymammotion/data/model/region_data.py,sha256=FLuL6kA7lbbh_idRh1eT9EosDqh4SpAqzpqHuQRDM88,2888
36
36
  pymammotion/data/model/report_info.py,sha256=gBSOmylSUdsYyIDsRms0L0nhQBx4V4LO-4tERvFpqXU,4475
37
37
  pymammotion/data/mqtt/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
38
- pymammotion/data/mqtt/event.py,sha256=wzh92MsvivgsJaOgqSJ6zQOssRdO3c0sFTHhXFa4HdU,4591
38
+ pymammotion/data/mqtt/event.py,sha256=GKh4NdcaeeaG5pPktzy-eguFKK74Mfxpl0eIAkKU1Vc,4567
39
39
  pymammotion/data/mqtt/properties.py,sha256=HkBPghr26L9_b4QaOi1DtPgb0UoPIOGSe9wb3kgnM6Y,2815
40
40
  pymammotion/data/mqtt/status.py,sha256=zqnlo-MzejEQZszl0i0Wucoc3E76x6UtI9JLxoBnu54,1067
41
- pymammotion/data/state_manager.py,sha256=fhV5icW9q4rCsHdD8lh4wvaDSzq6NP2116Wgdc7mR_U,3364
41
+ pymammotion/data/state_manager.py,sha256=ZIOB1Th7yDAgCJk17rd-kXjwD50GIqQMsNgf1iFldUs,3615
42
42
  pymammotion/event/__init__.py,sha256=mgATR6vPHACNQ-0zH5fi7NdzeTCDV1CZyaWPmtUusi8,115
43
43
  pymammotion/event/event.py,sha256=UzYnxV5DfvMDK3E06UvSzvzuBbaXOOUwO6xYt_zn9To,2034
44
44
  pymammotion/http/_init_.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -54,15 +54,15 @@ pymammotion/mammotion/commands/messages/media.py,sha256=ps0l06CXy5Ej--gTNCsyKttw
54
54
  pymammotion/mammotion/commands/messages/navigation.py,sha256=4rXBL-mViWc38K6x1w5O-GjwV8UWS5xZXkf4aHYjs8A,23684
55
55
  pymammotion/mammotion/commands/messages/network.py,sha256=gD7NKVKg8U2KNbPvgOxvTJXbznWdpdPQo9jBsQSx4OI,8027
56
56
  pymammotion/mammotion/commands/messages/ota.py,sha256=XkeuWBZtpYMMBze6r8UN7dJXbe2FxUNGNnjwBpXJKM0,1240
57
- pymammotion/mammotion/commands/messages/system.py,sha256=xm9Nj3wva9leVV1tyzfS_Hf53t-j7Nk8RBlFd00CQFM,10972
57
+ pymammotion/mammotion/commands/messages/system.py,sha256=4ijnn0VPHOIizgjqDGpeD3jVetEscnSDt2E4tIBwkoQ,10888
58
58
  pymammotion/mammotion/commands/messages/video.py,sha256=_8lJsU4sLm2CGnc7RDkueA0A51Ysui6x7SqFnhX8O2g,1007
59
59
  pymammotion/mammotion/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
60
  pymammotion/mammotion/control/joystick.py,sha256=QfBVxM_gxpWsZAGO90whtgxCI2tIZ3TTad9wHIPsU9s,5640
61
61
  pymammotion/mammotion/devices/__init__.py,sha256=f2qQFPgLGmV85W2hSlMUh5BYuht9o_Ar_JEAAMD4fsE,102
62
- pymammotion/mammotion/devices/base.py,sha256=s3xnEHzPIbo9LU0WB7I-y04JiX0CjR5g35Q_-kpWE70,11346
63
- pymammotion/mammotion/devices/mammotion.py,sha256=ijKaQ5mlvOoolBMYJ8ZoDlPzNO0Wa2hJaE04y2tUJNs,9727
64
- pymammotion/mammotion/devices/mammotion_bluetooth.py,sha256=3XUjhE2sb_2aZnJlevmwxd99zR_4qZOfaK86h6hKV5E,17303
65
- pymammotion/mammotion/devices/mammotion_cloud.py,sha256=mCzj-SOSke6mU010NQcO-r9qtHyPXCCY0OlYiO-MA9A,9915
62
+ pymammotion/mammotion/devices/base.py,sha256=5k82KHwW2JqZXW-MBYe8SkedZw_bQ2gqShX5RQd_Yio,11504
63
+ pymammotion/mammotion/devices/mammotion.py,sha256=uiKtzLHfVPyk-cdm5gFraI9nkrIX3ZkdZPE7xEyuoTQ,11597
64
+ pymammotion/mammotion/devices/mammotion_bluetooth.py,sha256=sgGeyQeAeA3lQodcalRYS4nDNAzjfFs9SddIB1kadvw,17355
65
+ pymammotion/mammotion/devices/mammotion_cloud.py,sha256=dk49VY9yHO3d-Nb17y-D4YIURs2FTLMzWHUYrxBYhtw,10116
66
66
  pymammotion/mqtt/__init__.py,sha256=Ocs5e-HLJvTuDpVXyECEsWIvwsUaxzj7lZ9mSYutNDY,105
67
67
  pymammotion/mqtt/mammotion_future.py,sha256=_OWqKOlUGl2yT1xOsXFQYpGd-1zQ63OxqXgy7KRQgYc,710
68
68
  pymammotion/mqtt/mammotion_mqtt.py,sha256=QZuqp2G5nywCFSwGNSayPI5JVL789yOxyr0UM0G7wzg,8295
@@ -112,12 +112,12 @@ pymammotion/utility/constant/__init__.py,sha256=tcY0LDeD-qDDHx2LKt55KOyv9ZI0UfCN
112
112
  pymammotion/utility/constant/device_constant.py,sha256=rAEK60F52VyJL31uLnq0Y60D-0VK5gVm59yi9kBfndM,7123
113
113
  pymammotion/utility/conversions.py,sha256=v3YICy0zZwwBBzrUZgabI7GRfiDBnkiAX2qdtk3NxOY,89
114
114
  pymammotion/utility/datatype_converter.py,sha256=SPM_HuaaD_XOawlqEnA8qlRRZXGba3WjA8kGOZgeBlQ,4284
115
- pymammotion/utility/device_type.py,sha256=6Mmv8oJoJ0DQrfGhRGt3rg20f_GLwIRw4NtpAdF6wcE,9478
115
+ pymammotion/utility/device_type.py,sha256=xOgfIhOkzgcAtoKtlhlB1q8FpiKe1rVVV5BvN7K7zYc,9433
116
116
  pymammotion/utility/map.py,sha256=GYscVMg2cX3IPlNpCBNHDW0S55yS1WGRf1iHnNZ7TfQ,2227
117
117
  pymammotion/utility/movement.py,sha256=N75oAoAgFydqoaOedYIxGUHmuTCtPzAOtb-d_29tpfI,615
118
118
  pymammotion/utility/periodic.py,sha256=MbeSb9cfhxzYmdT_RiE0dZe3H9IfbQW_zSqhmSX2RUc,3321
119
119
  pymammotion/utility/rocker_util.py,sha256=6tX7sS87qoQC_tsxbx3NLL-HgS08wtzXiZkhDiz7uo0,7179
120
- pymammotion-0.2.34.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
121
- pymammotion-0.2.34.dist-info/METADATA,sha256=RA-Cn-7qH2KLMJEGosBcGfrZIsXLhPPa8Twcxg8knU8,4052
122
- pymammotion-0.2.34.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
123
- pymammotion-0.2.34.dist-info/RECORD,,
120
+ pymammotion-0.2.36.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
121
+ pymammotion-0.2.36.dist-info/METADATA,sha256=oKsEdniFXVqz9dbL-obKVKoZWsKWlS0xDGjaob8n3Lc,4052
122
+ pymammotion-0.2.36.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
123
+ pymammotion-0.2.36.dist-info/RECORD,,