python-roborock 2.31.0__tar.gz → 2.33.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 (49) hide show
  1. {python_roborock-2.31.0 → python_roborock-2.33.0}/PKG-INFO +1 -1
  2. {python_roborock-2.31.0 → python_roborock-2.33.0}/pyproject.toml +2 -2
  3. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/cli.py +15 -8
  4. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/containers.py +7 -0
  5. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/devices/__init__.py +1 -0
  6. python_roborock-2.33.0/roborock/devices/cache.py +57 -0
  7. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/devices/device_manager.py +19 -6
  8. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/devices/v1_channel.py +20 -7
  9. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/mqtt/roborock_session.py +3 -3
  10. {python_roborock-2.31.0 → python_roborock-2.33.0}/LICENSE +0 -0
  11. {python_roborock-2.31.0 → python_roborock-2.33.0}/README.md +0 -0
  12. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/__init__.py +0 -0
  13. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/api.py +0 -0
  14. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/cloud_api.py +0 -0
  15. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/code_mappings.py +0 -0
  16. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/command_cache.py +0 -0
  17. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/const.py +0 -0
  18. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/device_features.py +0 -0
  19. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/devices/README.md +0 -0
  20. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/devices/a01_channel.py +0 -0
  21. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/devices/channel.py +0 -0
  22. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/devices/device.py +0 -0
  23. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/devices/local_channel.py +0 -0
  24. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/devices/mqtt_channel.py +0 -0
  25. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/devices/traits/dyad.py +0 -0
  26. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/devices/traits/status.py +0 -0
  27. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/devices/traits/trait.py +0 -0
  28. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/devices/traits/zeo.py +0 -0
  29. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/devices/v1_rpc_channel.py +0 -0
  30. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/exceptions.py +0 -0
  31. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/local_api.py +0 -0
  32. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/mqtt/__init__.py +0 -0
  33. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/mqtt/session.py +0 -0
  34. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/protocol.py +0 -0
  35. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/protocols/a01_protocol.py +0 -0
  36. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/protocols/v1_protocol.py +0 -0
  37. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/py.typed +0 -0
  38. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/roborock_future.py +0 -0
  39. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/roborock_message.py +0 -0
  40. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/roborock_typing.py +0 -0
  41. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/util.py +0 -0
  42. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/version_1_apis/__init__.py +0 -0
  43. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
  44. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  45. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  46. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/version_a01_apis/__init__.py +0 -0
  47. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  48. {python_roborock-2.31.0 → python_roborock-2.33.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
  49. {python_roborock-2.31.0 → python_roborock-2.33.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.31.0
3
+ Version: 2.33.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.31.0"
3
+ version = "2.33.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"
@@ -39,7 +39,7 @@ requires = ["poetry-core==1.8.0"]
39
39
  build-backend = "poetry.core.masonry.api"
40
40
 
41
41
  [tool.poetry.group.dev.dependencies]
42
- pytest-asyncio = "*"
42
+ pytest-asyncio = ">=1.1.0"
43
43
  pytest = "*"
44
44
  pre-commit = ">=3.5,<5.0"
45
45
  mypy = "*"
@@ -12,6 +12,7 @@ from pyshark.packet.packet import Packet # type: ignore
12
12
 
13
13
  from roborock import RoborockException
14
14
  from roborock.containers import DeviceData, HomeData, HomeDataProduct, LoginData, NetworkInfo, RoborockBase, UserData
15
+ from roborock.devices.cache import Cache, CacheData
15
16
  from roborock.devices.device_manager import create_device_manager, create_home_data_api
16
17
  from roborock.protocol import MessageParser
17
18
  from roborock.util import run_sync
@@ -39,7 +40,7 @@ class ConnectionCache(RoborockBase):
39
40
  network_info: dict[str, NetworkInfo] | None = None
40
41
 
41
42
 
42
- class RoborockContext:
43
+ class RoborockContext(Cache):
43
44
  roborock_file = Path("~/.roborock").expanduser()
44
45
  _cache_data: ConnectionCache | None = None
45
46
 
@@ -68,6 +69,18 @@ class RoborockContext:
68
69
  self.validate()
69
70
  return self._cache_data
70
71
 
72
+ async def get(self) -> CacheData:
73
+ """Get cached value."""
74
+ connection_cache = self.cache_data()
75
+ return CacheData(home_data=connection_cache.home_data, network_info=connection_cache.network_info or {})
76
+
77
+ async def set(self, value: CacheData) -> None:
78
+ """Set value in the cache."""
79
+ connection_cache = self.cache_data()
80
+ connection_cache.home_data = value.home_data
81
+ connection_cache.network_info = value.network_info
82
+ self.update(connection_cache)
83
+
71
84
 
72
85
  @click.option("-d", "--debug", default=False, count=True)
73
86
  @click.version_option(package_name="python-roborock")
@@ -119,14 +132,8 @@ async def session(ctx, duration: int):
119
132
 
120
133
  home_data_api = create_home_data_api(cache_data.email, cache_data.user_data)
121
134
 
122
- async def home_data_cache() -> HomeData:
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
127
-
128
135
  # Create device manager
129
- device_manager = await create_device_manager(cache_data.user_data, home_data_cache)
136
+ device_manager = await create_device_manager(cache_data.user_data, home_data_api, context)
130
137
 
131
138
  devices = await device_manager.get_devices()
132
139
  click.echo(f"Discovered devices: {', '.join([device.name for device in devices])}")
@@ -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):
@@ -3,4 +3,5 @@
3
3
  __all__ = [
4
4
  "device",
5
5
  "device_manager",
6
+ "cache",
6
7
  ]
@@ -0,0 +1,57 @@
1
+ """This module provides caching functionality for the Roborock device management system.
2
+
3
+ This module defines a cache interface that you may use to cache device
4
+ information to avoid unnecessary API calls. Callers may implement
5
+ this interface to provide their own caching mechanism.
6
+ """
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Protocol
10
+
11
+ from roborock.containers import HomeData, NetworkInfo
12
+
13
+
14
+ @dataclass
15
+ class CacheData:
16
+ """Data structure for caching device information."""
17
+
18
+ home_data: HomeData | None = None
19
+ """Home data containing device and product information."""
20
+
21
+ network_info: dict[str, NetworkInfo] = field(default_factory=dict)
22
+ """Network information indexed by device DUID."""
23
+
24
+
25
+ class Cache(Protocol):
26
+ """Protocol for a cache that can store and retrieve values."""
27
+
28
+ async def get(self) -> CacheData:
29
+ """Get cached value."""
30
+ ...
31
+
32
+ async def set(self, value: CacheData) -> None:
33
+ """Set value in the cache."""
34
+ ...
35
+
36
+
37
+ class InMemoryCache(Cache):
38
+ """In-memory cache implementation."""
39
+
40
+ def __init__(self):
41
+ self._data = CacheData()
42
+
43
+ async def get(self) -> CacheData:
44
+ return self._data
45
+
46
+ async def set(self, value: CacheData) -> None:
47
+ self._data = value
48
+
49
+
50
+ class NoCache(Cache):
51
+ """No-op cache implementation."""
52
+
53
+ async def get(self) -> CacheData:
54
+ return CacheData()
55
+
56
+ async def set(self, value: CacheData) -> None:
57
+ pass
@@ -18,6 +18,7 @@ from roborock.mqtt.session import MqttSession
18
18
  from roborock.protocol import create_mqtt_params
19
19
  from roborock.web_api import RoborockApiClient
20
20
 
21
+ from .cache import Cache, NoCache
21
22
  from .channel import Channel
22
23
  from .mqtt_channel import create_mqtt_channel
23
24
  from .traits.dyad import DyadApi
@@ -32,8 +33,6 @@ __all__ = [
32
33
  "create_device_manager",
33
34
  "create_home_data_api",
34
35
  "DeviceManager",
35
- "HomeDataApi",
36
- "DeviceCreator",
37
36
  ]
38
37
 
39
38
 
@@ -57,19 +56,27 @@ class DeviceManager:
57
56
  home_data_api: HomeDataApi,
58
57
  device_creator: DeviceCreator,
59
58
  mqtt_session: MqttSession,
59
+ cache: Cache,
60
60
  ) -> None:
61
61
  """Initialize the DeviceManager with user data and optional cache storage.
62
62
 
63
63
  This takes ownership of the MQTT session and will close it when the manager is closed.
64
64
  """
65
65
  self._home_data_api = home_data_api
66
+ self._cache = cache
66
67
  self._device_creator = device_creator
67
68
  self._devices: dict[str, RoborockDevice] = {}
68
69
  self._mqtt_session = mqtt_session
69
70
 
70
71
  async def discover_devices(self) -> list[RoborockDevice]:
71
72
  """Discover all devices for the logged-in user."""
72
- home_data = await self._home_data_api()
73
+ cache_data = await self._cache.get()
74
+ if not cache_data.home_data:
75
+ _LOGGER.debug("No cached home data found, fetching from API")
76
+ cache_data.home_data = await self._home_data_api()
77
+ await self._cache.set(cache_data)
78
+ home_data = cache_data.home_data
79
+
73
80
  device_products = home_data.device_products
74
81
  _LOGGER.debug("Discovered %d devices %s", len(device_products), home_data)
75
82
 
@@ -118,13 +125,19 @@ def create_home_data_api(email: str, user_data: UserData) -> HomeDataApi:
118
125
  return home_data_api
119
126
 
120
127
 
121
- async def create_device_manager(user_data: UserData, home_data_api: HomeDataApi) -> DeviceManager:
128
+ async def create_device_manager(
129
+ user_data: UserData,
130
+ home_data_api: HomeDataApi,
131
+ cache: Cache | None = None,
132
+ ) -> DeviceManager:
122
133
  """Convenience function to create and initialize a DeviceManager.
123
134
 
124
135
  The Home Data is fetched using the provided home_data_api callable which
125
136
  is exposed this way to allow for swapping out other implementations to
126
137
  include caching or other optimizations.
127
138
  """
139
+ if cache is None:
140
+ cache = NoCache()
128
141
 
129
142
  mqtt_params = create_mqtt_params(user_data.rriot)
130
143
  mqtt_session = await create_mqtt_session(mqtt_params)
@@ -135,7 +148,7 @@ async def create_device_manager(user_data: UserData, home_data_api: HomeDataApi)
135
148
  # TODO: Define a registration mechanism/factory for v1 traits
136
149
  match device.pv:
137
150
  case DeviceVersion.V1:
138
- channel = create_v1_channel(user_data, mqtt_params, mqtt_session, device)
151
+ channel = create_v1_channel(user_data, mqtt_params, mqtt_session, device, cache)
139
152
  traits.append(StatusTrait(product, channel.rpc_channel))
140
153
  case DeviceVersion.A01:
141
154
  mqtt_channel = create_mqtt_channel(user_data, mqtt_params, mqtt_session, device)
@@ -150,6 +163,6 @@ async def create_device_manager(user_data: UserData, home_data_api: HomeDataApi)
150
163
  raise NotImplementedError(f"Device {device.name} has unsupported version {device.pv}")
151
164
  return RoborockDevice(device, channel, traits)
152
165
 
153
- manager = DeviceManager(home_data_api, device_creator, mqtt_session=mqtt_session)
166
+ manager = DeviceManager(home_data_api, device_creator, mqtt_session=mqtt_session, cache=cache)
154
167
  await manager.discover_devices()
155
168
  return manager
@@ -18,6 +18,7 @@ from roborock.protocols.v1_protocol import (
18
18
  from roborock.roborock_message import RoborockMessage
19
19
  from roborock.roborock_typing import RoborockCommand
20
20
 
21
+ from .cache import Cache
21
22
  from .channel import Channel
22
23
  from .local_channel import LocalChannel, LocalSession, create_local_session
23
24
  from .mqtt_channel import MqttChannel
@@ -46,6 +47,7 @@ class V1Channel(Channel):
46
47
  security_data: SecurityData,
47
48
  mqtt_channel: MqttChannel,
48
49
  local_session: LocalSession,
50
+ cache: Cache,
49
51
  ) -> None:
50
52
  """Initialize the V1Channel.
51
53
 
@@ -62,7 +64,7 @@ class V1Channel(Channel):
62
64
  self._mqtt_unsub: Callable[[], None] | None = None
63
65
  self._local_unsub: Callable[[], None] | None = None
64
66
  self._callback: Callable[[RoborockMessage], None] | None = None
65
- self._networking_info: NetworkInfo | None = None
67
+ self._cache = cache
66
68
 
67
69
  @property
68
70
  def is_connected(self) -> bool:
@@ -131,19 +133,26 @@ class V1Channel(Channel):
131
133
 
132
134
  This is a cloud only command used to get the local device's IP address.
133
135
  """
136
+ cache_data = await self._cache.get()
137
+ if cache_data.network_info and (network_info := cache_data.network_info.get(self._device_uid)):
138
+ _LOGGER.debug("Using cached network info for device %s", self._device_uid)
139
+ return network_info
134
140
  try:
135
- return await self._mqtt_rpc_channel.send_command(
141
+ network_info = await self._mqtt_rpc_channel.send_command(
136
142
  RoborockCommand.GET_NETWORK_INFO, response_type=NetworkInfo
137
143
  )
138
144
  except RoborockException as e:
139
145
  raise RoborockException(f"Network info failed for device {self._device_uid}") from e
146
+ _LOGGER.debug("Network info for device %s: %s", self._device_uid, network_info)
147
+ cache_data.network_info[self._device_uid] = network_info
148
+ await self._cache.set(cache_data)
149
+ return network_info
140
150
 
141
151
  async def _local_connect(self) -> Callable[[], None]:
142
152
  """Set up local connection if possible."""
143
153
  _LOGGER.debug("Attempting to connect to local channel for device %s", self._device_uid)
144
- if self._networking_info is None:
145
- self._networking_info = await self._get_networking_info()
146
- host = self._networking_info.ip
154
+ networking_info = await self._get_networking_info()
155
+ host = networking_info.ip
147
156
  _LOGGER.debug("Connecting to local channel at %s", host)
148
157
  self._local_channel = self._local_session(host)
149
158
  try:
@@ -168,10 +177,14 @@ class V1Channel(Channel):
168
177
 
169
178
 
170
179
  def create_v1_channel(
171
- user_data: UserData, mqtt_params: MqttParams, mqtt_session: MqttSession, device: HomeDataDevice
180
+ user_data: UserData,
181
+ mqtt_params: MqttParams,
182
+ mqtt_session: MqttSession,
183
+ device: HomeDataDevice,
184
+ cache: Cache,
172
185
  ) -> V1Channel:
173
186
  """Create a V1Channel for the given device."""
174
187
  security_data = create_security_data(user_data.rriot)
175
188
  mqtt_channel = MqttChannel(mqtt_session, device.duid, device.local_key, user_data.rriot, mqtt_params)
176
189
  local_session = create_local_session(device.local_key)
177
- return V1Channel(device.duid, security_data, mqtt_channel, local_session=local_session)
190
+ return V1Channel(device.duid, security_data, mqtt_channel, local_session=local_session, cache=cache)
@@ -125,14 +125,14 @@ class RoborockMqttSession(MqttSession):
125
125
  except Exception as err:
126
126
  # This error is thrown when the MQTT loop is cancelled
127
127
  # and the generator is not stopped.
128
- if "generator didn't stop" in str(err):
128
+ if "generator didn't stop" in str(err) or "generator didn't yield" in str(err):
129
129
  _LOGGER.debug("MQTT loop was cancelled")
130
130
  return
131
131
  if start_future:
132
132
  _LOGGER.error("Uncaught error starting MQTT session: %s", err)
133
133
  start_future.set_exception(err)
134
134
  return
135
- _LOGGER.error("Uncaught error during MQTT session: %s", err)
135
+ _LOGGER.exception("Uncaught error during MQTT session: %s", err)
136
136
 
137
137
  self._healthy = False
138
138
  _LOGGER.info("MQTT session disconnected, retrying in %s seconds", self._backoff.total_seconds())
@@ -180,7 +180,7 @@ class RoborockMqttSession(MqttSession):
180
180
  except asyncio.CancelledError:
181
181
  raise
182
182
  except Exception as e:
183
- _LOGGER.error("Uncaught exception in subscriber callback: %s", e)
183
+ _LOGGER.exception("Uncaught exception in subscriber callback: %s", e)
184
184
 
185
185
  async def subscribe(self, topic: str, callback: Callable[[bytes], None]) -> Callable[[], None]:
186
186
  """Subscribe to messages on the specified topic and invoke the callback for new messages.