python-roborock 2.30.0__tar.gz → 2.32.0__tar.gz

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.
Files changed (48) hide show
  1. {python_roborock-2.30.0 → python_roborock-2.32.0}/PKG-INFO +1 -1
  2. {python_roborock-2.30.0 → python_roborock-2.32.0}/pyproject.toml +1 -1
  3. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/cli.py +81 -63
  4. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/containers.py +7 -0
  5. {python_roborock-2.30.0 → python_roborock-2.32.0}/LICENSE +0 -0
  6. {python_roborock-2.30.0 → python_roborock-2.32.0}/README.md +0 -0
  7. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/__init__.py +0 -0
  8. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/api.py +0 -0
  9. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/cloud_api.py +0 -0
  10. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/code_mappings.py +0 -0
  11. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/command_cache.py +0 -0
  12. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/const.py +0 -0
  13. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/device_features.py +0 -0
  14. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/devices/README.md +0 -0
  15. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/devices/__init__.py +0 -0
  16. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/devices/a01_channel.py +0 -0
  17. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/devices/channel.py +0 -0
  18. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/devices/device.py +0 -0
  19. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/devices/device_manager.py +0 -0
  20. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/devices/local_channel.py +0 -0
  21. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/devices/mqtt_channel.py +0 -0
  22. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/devices/traits/dyad.py +0 -0
  23. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/devices/traits/status.py +0 -0
  24. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/devices/traits/trait.py +0 -0
  25. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/devices/traits/zeo.py +0 -0
  26. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/devices/v1_channel.py +0 -0
  27. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/devices/v1_rpc_channel.py +0 -0
  28. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/exceptions.py +0 -0
  29. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/local_api.py +0 -0
  30. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/mqtt/__init__.py +0 -0
  31. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/mqtt/roborock_session.py +0 -0
  32. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/mqtt/session.py +0 -0
  33. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/protocol.py +0 -0
  34. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/protocols/a01_protocol.py +0 -0
  35. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/protocols/v1_protocol.py +0 -0
  36. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/py.typed +0 -0
  37. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/roborock_future.py +0 -0
  38. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/roborock_message.py +0 -0
  39. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/roborock_typing.py +0 -0
  40. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/util.py +0 -0
  41. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/version_1_apis/__init__.py +0 -0
  42. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
  43. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  44. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  45. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/version_a01_apis/__init__.py +0 -0
  46. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  47. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
  48. {python_roborock-2.30.0 → python_roborock-2.32.0}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-roborock
3
- Version: 2.30.0
3
+ Version: 2.32.0
4
4
  Summary: A package to control Roborock vacuums.
5
5
  Home-page: https://github.com/humbertogontijo/python-roborock
6
6
  License: GPL-3.0-only
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-roborock"
3
- version = "2.30.0"
3
+ version = "2.32.0"
4
4
  description = "A package to control Roborock vacuums."
5
5
  authors = ["humbertogontijo <humbertogontijo@users.noreply.github.com>"]
6
6
  license = "GPL-3.0-only"
@@ -1,8 +1,7 @@
1
- from __future__ import annotations
2
-
3
1
  import asyncio
4
2
  import json
5
3
  import logging
4
+ from dataclasses import dataclass
6
5
  from pathlib import Path
7
6
  from typing import Any
8
7
 
@@ -12,7 +11,7 @@ from pyshark.capture.live_capture import LiveCapture, UnknownInterfaceException
12
11
  from pyshark.packet.packet import Packet # type: ignore
13
12
 
14
13
  from roborock import RoborockException
15
- from roborock.containers import DeviceData, HomeData, HomeDataProduct, LoginData
14
+ from roborock.containers import DeviceData, HomeData, HomeDataProduct, LoginData, NetworkInfo, RoborockBase, UserData
16
15
  from roborock.devices.device_manager import create_device_manager, create_home_data_api
17
16
  from roborock.protocol import MessageParser
18
17
  from roborock.util import run_sync
@@ -23,9 +22,26 @@ from roborock.web_api import RoborockApiClient
23
22
  _LOGGER = logging.getLogger(__name__)
24
23
 
25
24
 
25
+ @dataclass
26
+ class ConnectionCache(RoborockBase):
27
+ """Cache for Roborock data.
28
+
29
+ This is used to store data retrieved from the Roborock API, such as user
30
+ data and home data to avoid repeated API calls.
31
+
32
+ This cache is superset of `LoginData` since we used to directly store that
33
+ dataclass, but now we also store additional data.
34
+ """
35
+
36
+ user_data: UserData
37
+ email: str
38
+ home_data: HomeData | None = None
39
+ network_info: dict[str, NetworkInfo] | None = None
40
+
41
+
26
42
  class RoborockContext:
27
43
  roborock_file = Path("~/.roborock").expanduser()
28
- _login_data: LoginData | None = None
44
+ _cache_data: ConnectionCache | None = None
29
45
 
30
46
  def __init__(self):
31
47
  self.reload()
@@ -35,22 +51,22 @@ class RoborockContext:
35
51
  with open(self.roborock_file) as f:
36
52
  data = json.load(f)
37
53
  if data:
38
- self._login_data = LoginData.from_dict(data)
54
+ self._cache_data = ConnectionCache.from_dict(data)
39
55
 
40
- def update(self, login_data: LoginData):
41
- data = json.dumps(login_data.as_dict(), default=vars)
56
+ def update(self, cache_data: ConnectionCache):
57
+ data = json.dumps(cache_data.as_dict(), default=vars, indent=4)
42
58
  with open(self.roborock_file, "w") as f:
43
59
  f.write(data)
44
60
  self.reload()
45
61
 
46
62
  def validate(self):
47
- if self._login_data is None:
63
+ if self._cache_data is None:
48
64
  raise RoborockException("You must login first")
49
65
 
50
- def login_data(self) -> LoginData:
51
- """Get the login data."""
66
+ def cache_data(self) -> ConnectionCache:
67
+ """Get the cache data."""
52
68
  self.validate()
53
- return self._login_data
69
+ return self._cache_data
54
70
 
55
71
 
56
72
  @click.option("-d", "--debug", default=False, count=True)
@@ -99,18 +115,18 @@ async def login(ctx, email, password):
99
115
  @run_sync()
100
116
  async def session(ctx, duration: int):
101
117
  context: RoborockContext = ctx.obj
102
- login_data = context.login_data()
118
+ cache_data = context.cache_data()
103
119
 
104
- home_data_api = create_home_data_api(login_data.email, login_data.user_data)
120
+ home_data_api = create_home_data_api(cache_data.email, cache_data.user_data)
105
121
 
106
122
  async def home_data_cache() -> HomeData:
107
- if login_data.home_data is None:
108
- login_data.home_data = await home_data_api()
109
- context.update(login_data)
110
- return login_data.home_data
123
+ if cache_data.home_data is None:
124
+ cache_data.home_data = await home_data_api()
125
+ context.update(cache_data)
126
+ return cache_data.home_data
111
127
 
112
128
  # Create device manager
113
- device_manager = await create_device_manager(login_data.user_data, home_data_cache)
129
+ device_manager = await create_device_manager(cache_data.user_data, home_data_cache)
114
130
 
115
131
  devices = await device_manager.get_devices()
116
132
  click.echo(f"Discovered devices: {', '.join([device.name for device in devices])}")
@@ -136,16 +152,26 @@ async def session(ctx, duration: int):
136
152
 
137
153
  async def _discover(ctx):
138
154
  context: RoborockContext = ctx.obj
139
- login_data = context.login_data()
140
- if not login_data:
155
+ cache_data = context.cache_data()
156
+ if not cache_data:
141
157
  raise Exception("You need to login first")
142
- client = RoborockApiClient(login_data.email)
143
- home_data = await client.get_home_data(login_data.user_data)
144
- login_data.home_data = home_data
145
- context.update(login_data)
158
+ client = RoborockApiClient(cache_data.email)
159
+ home_data = await client.get_home_data(cache_data.user_data)
160
+ cache_data.home_data = home_data
161
+ context.update(cache_data)
146
162
  click.echo(f"Discovered devices {', '.join([device.name for device in home_data.get_all_devices()])}")
147
163
 
148
164
 
165
+ async def _load_and_discover(ctx) -> RoborockContext:
166
+ """Discover devices if home data is not available."""
167
+ context: RoborockContext = ctx.obj
168
+ cache_data = context.cache_data()
169
+ if not cache_data.home_data:
170
+ await _discover(ctx)
171
+ cache_data = context.cache_data()
172
+ return context
173
+
174
+
149
175
  @click.command()
150
176
  @click.pass_context
151
177
  @run_sync()
@@ -157,16 +183,11 @@ async def discover(ctx):
157
183
  @click.pass_context
158
184
  @run_sync()
159
185
  async def list_devices(ctx):
160
- context: RoborockContext = ctx.obj
161
- login_data = context.login_data()
162
- if not login_data.home_data:
163
- await _discover(ctx)
164
- login_data = context.login_data()
165
- home_data = login_data.home_data
166
- device_name_id = ", ".join(
167
- [f"{device.name}: {device.duid}" for device in home_data.devices + home_data.received_devices]
168
- )
169
- click.echo(f"Known devices {device_name_id}")
186
+ context: RoborockContext = await _load_and_discover(ctx)
187
+ cache_data = context.cache_data()
188
+ home_data = cache_data.home_data
189
+ device_name_id = {device.name: device.duid for device in home_data.devices + home_data.received_devices}
190
+ click.echo(json.dumps(device_name_id, indent=4))
170
191
 
171
192
 
172
193
  @click.command()
@@ -174,13 +195,10 @@ async def list_devices(ctx):
174
195
  @click.pass_context
175
196
  @run_sync()
176
197
  async def list_scenes(ctx, device_id):
177
- context: RoborockContext = ctx.obj
178
- login_data = context.login_data()
179
- if not login_data.home_data:
180
- await _discover(ctx)
181
- login_data = context.login_data()
182
- client = RoborockApiClient(login_data.email)
183
- scenes = await client.get_scenes(login_data.user_data, device_id)
198
+ context: RoborockContext = await _load_and_discover(ctx)
199
+ cache_data = context.cache_data()
200
+ client = RoborockApiClient(cache_data.email)
201
+ scenes = await client.get_scenes(cache_data.user_data, device_id)
184
202
  output_list = []
185
203
  for scene in scenes:
186
204
  output_list.append(scene.as_dict())
@@ -192,13 +210,10 @@ async def list_scenes(ctx, device_id):
192
210
  @click.pass_context
193
211
  @run_sync()
194
212
  async def execute_scene(ctx, scene_id):
195
- context: RoborockContext = ctx.obj
196
- login_data = context.login_data()
197
- if not login_data.home_data:
198
- await _discover(ctx)
199
- login_data = context.login_data()
200
- client = RoborockApiClient(login_data.email)
201
- await client.execute_scene(login_data.user_data, scene_id)
213
+ context: RoborockContext = await _load_and_discover(ctx)
214
+ cache_data = context.cache_data()
215
+ client = RoborockApiClient(cache_data.email)
216
+ await client.execute_scene(cache_data.user_data, scene_id)
202
217
 
203
218
 
204
219
  @click.command()
@@ -206,18 +221,23 @@ async def execute_scene(ctx, scene_id):
206
221
  @click.pass_context
207
222
  @run_sync()
208
223
  async def status(ctx, device_id):
209
- context: RoborockContext = ctx.obj
210
- login_data = context.login_data()
211
- if not login_data.home_data:
212
- await _discover(ctx)
213
- login_data = context.login_data()
214
- home_data = login_data.home_data
224
+ context: RoborockContext = await _load_and_discover(ctx)
225
+ cache_data = context.cache_data()
226
+
227
+ home_data = cache_data.home_data
215
228
  devices = home_data.devices + home_data.received_devices
216
229
  device = next(device for device in devices if device.duid == device_id)
217
230
  product_info: dict[str, HomeDataProduct] = {product.id: product for product in home_data.products}
218
231
  device_data = DeviceData(device, product_info[device.product_id].model)
219
- mqtt_client = RoborockMqttClientV1(login_data.user_data, device_data)
220
- networking = await mqtt_client.get_networking()
232
+
233
+ mqtt_client = RoborockMqttClientV1(cache_data.user_data, device_data)
234
+ if not (networking := cache_data.network_info.get(device.duid)):
235
+ networking = await mqtt_client.get_networking()
236
+ cache_data.network_info[device.duid] = networking
237
+ context.update(cache_data)
238
+ else:
239
+ _LOGGER.debug("Using cached networking info for device %s: %s", device.duid, networking)
240
+
221
241
  local_device_data = DeviceData(device, product_info[device.product_id].model, networking.ip)
222
242
  local_client = RoborockLocalClientV1(local_device_data)
223
243
  status = await local_client.get_status()
@@ -231,12 +251,10 @@ async def status(ctx, device_id):
231
251
  @click.pass_context
232
252
  @run_sync()
233
253
  async def command(ctx, cmd, device_id, params):
234
- context: RoborockContext = ctx.obj
235
- login_data = context.login_data()
236
- if not login_data.home_data:
237
- await _discover(ctx)
238
- login_data = context.login_data()
239
- home_data = login_data.home_data
254
+ context: RoborockContext = await _load_and_discover(ctx)
255
+ cache_data = context.cache_data()
256
+
257
+ home_data = cache_data.home_data
240
258
  devices = home_data.devices + home_data.received_devices
241
259
  device = next(device for device in devices if device.duid == device_id)
242
260
  model = next(
@@ -246,7 +264,7 @@ async def command(ctx, cmd, device_id, params):
246
264
  if model is None:
247
265
  raise RoborockException(f"Could not find model for device {device.name}")
248
266
  device_info = DeviceData(device=device, model=model)
249
- mqtt_client = RoborockMqttClientV1(login_data.user_data, device_info)
267
+ mqtt_client = RoborockMqttClientV1(cache_data.user_data, device_info)
250
268
  await mqtt_client.send_command(cmd, json.loads(params) if params is not None else None)
251
269
  await mqtt_client.async_release()
252
270
 
@@ -425,6 +425,13 @@ class Status(RoborockBase):
425
425
  raise RoborockException("Attempted to get mop_mode before status has been updated.")
426
426
  return self.mop_mode.as_dict().get(mop_mode)
427
427
 
428
+ @property
429
+ def current_map(self) -> int | None:
430
+ """Returns the current map ID if the map is present."""
431
+ if self.map_status is not None:
432
+ return (self.map_status - 3) // 4
433
+ return None
434
+
428
435
 
429
436
  @dataclass
430
437
  class S4MaxStatus(Status):