python-roborock 3.10.2__tar.gz → 3.10.4__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 (96) hide show
  1. {python_roborock-3.10.2 → python_roborock-3.10.4}/PKG-INFO +1 -1
  2. {python_roborock-3.10.2 → python_roborock-3.10.4}/pyproject.toml +1 -1
  3. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/device.py +18 -3
  4. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/device_manager.py +3 -1
  5. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/local_channel.py +2 -2
  6. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/v1_channel.py +27 -15
  7. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/mqtt/roborock_session.py +1 -1
  8. {python_roborock-3.10.2 → python_roborock-3.10.4}/.gitignore +0 -0
  9. {python_roborock-3.10.2 → python_roborock-3.10.4}/LICENSE +0 -0
  10. {python_roborock-3.10.2 → python_roborock-3.10.4}/README.md +0 -0
  11. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/__init__.py +0 -0
  12. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/api.py +0 -0
  13. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/broadcast_protocol.py +0 -0
  14. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/callbacks.py +0 -0
  15. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/cli.py +0 -0
  16. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/cloud_api.py +0 -0
  17. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/command_cache.py +0 -0
  18. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/const.py +0 -0
  19. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/data/__init__.py +0 -0
  20. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/data/b01_q10/__init__.py +0 -0
  21. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/data/b01_q10/b01_q10_code_mappings.py +0 -0
  22. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/data/b01_q10/b01_q10_containers.py +0 -0
  23. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/data/b01_q7/__init__.py +0 -0
  24. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/data/b01_q7/b01_q7_code_mappings.py +0 -0
  25. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/data/b01_q7/b01_q7_containers.py +0 -0
  26. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/data/code_mappings.py +0 -0
  27. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/data/containers.py +0 -0
  28. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/data/dyad/__init__.py +0 -0
  29. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/data/dyad/dyad_code_mappings.py +0 -0
  30. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/data/dyad/dyad_containers.py +0 -0
  31. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/data/v1/__init__.py +0 -0
  32. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/data/v1/v1_clean_modes.py +0 -0
  33. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/data/v1/v1_code_mappings.py +0 -0
  34. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/data/v1/v1_containers.py +0 -0
  35. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/data/zeo/__init__.py +0 -0
  36. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/data/zeo/zeo_code_mappings.py +0 -0
  37. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/data/zeo/zeo_containers.py +0 -0
  38. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/device_features.py +0 -0
  39. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/README.md +0 -0
  40. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/__init__.py +0 -0
  41. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/a01_channel.py +0 -0
  42. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/b01_channel.py +0 -0
  43. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/cache.py +0 -0
  44. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/channel.py +0 -0
  45. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/file_cache.py +0 -0
  46. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/mqtt_channel.py +0 -0
  47. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/__init__.py +0 -0
  48. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/a01/__init__.py +0 -0
  49. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/b01/__init__.py +0 -0
  50. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/traits_mixin.py +0 -0
  51. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/__init__.py +0 -0
  52. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/child_lock.py +0 -0
  53. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/clean_summary.py +0 -0
  54. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/command.py +0 -0
  55. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/common.py +0 -0
  56. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/consumeable.py +0 -0
  57. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/device_features.py +0 -0
  58. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  59. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/dust_collection_mode.py +0 -0
  60. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/flow_led_status.py +0 -0
  61. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/home.py +0 -0
  62. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/led_status.py +0 -0
  63. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/map_content.py +0 -0
  64. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/maps.py +0 -0
  65. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/network_info.py +0 -0
  66. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/rooms.py +0 -0
  67. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/routines.py +0 -0
  68. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/smart_wash_params.py +0 -0
  69. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/status.py +0 -0
  70. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/valley_electricity_timer.py +0 -0
  71. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/volume.py +0 -0
  72. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/devices/traits/v1/wash_towel_mode.py +0 -0
  73. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/exceptions.py +0 -0
  74. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/map/__init__.py +0 -0
  75. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/map/map_parser.py +0 -0
  76. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/mqtt/__init__.py +0 -0
  77. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/mqtt/health_manager.py +0 -0
  78. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/mqtt/session.py +0 -0
  79. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/protocol.py +0 -0
  80. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/protocols/__init__.py +0 -0
  81. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/protocols/a01_protocol.py +0 -0
  82. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/protocols/b01_protocol.py +0 -0
  83. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/protocols/v1_protocol.py +0 -0
  84. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/py.typed +0 -0
  85. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/roborock_future.py +0 -0
  86. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/roborock_message.py +0 -0
  87. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/roborock_typing.py +0 -0
  88. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/util.py +0 -0
  89. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/version_1_apis/__init__.py +0 -0
  90. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/version_1_apis/roborock_client_v1.py +0 -0
  91. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  92. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  93. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/version_a01_apis/__init__.py +0 -0
  94. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  95. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
  96. {python_roborock-3.10.2 → python_roborock-3.10.4}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-roborock
3
- Version: 3.10.2
3
+ Version: 3.10.4
4
4
  Summary: A package to control Roborock vacuums.
5
5
  Project-URL: Repository, https://github.com/humbertogontijo/python-roborock
6
6
  Project-URL: Documentation, https://python-roborock.readthedocs.io/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-roborock"
3
- version = "3.10.2"
3
+ version = "3.10.4"
4
4
  description = "A package to control Roborock vacuums."
5
5
  authors = [{ name = "humbertogontijo", email = "humbertogontijo@users.noreply.github.com" }, {name="Lash-L"}, {name="allenporter"}]
6
6
  requires-python = ">=3.11, <4"
@@ -29,6 +29,7 @@ __all__ = [
29
29
  MIN_BACKOFF_INTERVAL = datetime.timedelta(seconds=10)
30
30
  MAX_BACKOFF_INTERVAL = datetime.timedelta(minutes=30)
31
31
  BACKOFF_MULTIPLIER = 1.5
32
+ START_ATTEMPT_TIMEOUT = datetime.timedelta(seconds=5)
32
33
 
33
34
 
34
35
  class RoborockDevice(ABC, TraitsMixin):
@@ -107,16 +108,21 @@ class RoborockDevice(ABC, TraitsMixin):
107
108
  """
108
109
  return self._channel.is_local_connected
109
110
 
110
- def start_connect(self) -> None:
111
+ async def start_connect(self) -> None:
111
112
  """Start a background task to connect to the device.
112
113
 
113
- This will attempt to connect to the device using the appropriate protocol
114
- channel. If the connection fails, it will retry with exponential backoff.
114
+ This will give a moment for the first connection attempt to start so
115
+ that the device will have connections established -- however, this will
116
+ never directly fail.
117
+
118
+ If the connection fails, it will retry in the background with
119
+ exponential backoff.
115
120
 
116
121
  Once connected, the device will remain connected until `close()` is
117
122
  called. The device will automatically attempt to reconnect if the connection
118
123
  is lost.
119
124
  """
125
+ start_attempt: asyncio.Event = asyncio.Event()
120
126
 
121
127
  async def connect_loop() -> None:
122
128
  backoff = MIN_BACKOFF_INTERVAL
@@ -124,8 +130,10 @@ class RoborockDevice(ABC, TraitsMixin):
124
130
  while True:
125
131
  try:
126
132
  await self.connect()
133
+ start_attempt.set()
127
134
  return
128
135
  except RoborockException as e:
136
+ start_attempt.set()
129
137
  _LOGGER.info("Failed to connect to device %s: %s", self.name, e)
130
138
  _LOGGER.info(
131
139
  "Retrying connection to device %s in %s seconds", self.name, backoff.total_seconds()
@@ -136,9 +144,16 @@ class RoborockDevice(ABC, TraitsMixin):
136
144
  _LOGGER.info("connect_loop for device %s was cancelled", self.name)
137
145
  # Clean exit on cancellation
138
146
  return
147
+ finally:
148
+ start_attempt.set()
139
149
 
140
150
  self._connect_task = asyncio.create_task(connect_loop())
141
151
 
152
+ try:
153
+ await asyncio.wait_for(start_attempt.wait(), timeout=START_ATTEMPT_TIMEOUT.total_seconds())
154
+ except TimeoutError:
155
+ _LOGGER.debug("Initial connection attempt to device %s is taking longer than expected", self.name)
156
+
142
157
  async def connect(self) -> None:
143
158
  """Connect to the device using the appropriate protocol channel."""
144
159
  if self._unsub:
@@ -82,14 +82,16 @@ class DeviceManager:
82
82
 
83
83
  # These are connected serially to avoid overwhelming the MQTT broker
84
84
  new_devices = {}
85
+ start_tasks = []
85
86
  for duid, (device, product) in device_products.items():
86
87
  if duid in self._devices:
87
88
  continue
88
89
  new_device = self._device_creator(home_data, device, product)
89
- new_device.start_connect()
90
+ start_tasks.append(new_device.start_connect())
90
91
  new_devices[duid] = new_device
91
92
 
92
93
  self._devices.update(new_devices)
94
+ await asyncio.gather(*start_tasks)
93
95
  return list(self._devices.values())
94
96
 
95
97
  async def get_device(self, duid: str) -> RoborockDevice | None:
@@ -176,7 +176,7 @@ class LocalChannel(Channel):
176
176
  async def connect(self) -> None:
177
177
  """Connect to the device and negotiate protocol."""
178
178
  if self._is_connected:
179
- _LOGGER.warning("Already connected")
179
+ _LOGGER.debug("Unexpected call to connect when already connected")
180
180
  return
181
181
  _LOGGER.debug("Connecting to %s:%s", self._host, _PORT)
182
182
  loop = asyncio.get_running_loop()
@@ -214,7 +214,7 @@ class LocalChannel(Channel):
214
214
 
215
215
  def _connection_lost(self, exc: Exception | None) -> None:
216
216
  """Handle connection loss."""
217
- _LOGGER.warning("Connection lost to %s", self._host, exc_info=exc)
217
+ _LOGGER.debug("Connection lost to %s", self._host, exc_info=exc)
218
218
  if self._keep_alive_task:
219
219
  self._keep_alive_task.cancel()
220
220
  self._keep_alive_task = None
@@ -70,9 +70,9 @@ class RpcStrategy:
70
70
  class RpcChannel(V1RpcChannel):
71
71
  """Provides an RPC interface around a pub/sub transport channel."""
72
72
 
73
- def __init__(self, rpc_strategies: list[RpcStrategy]) -> None:
74
- """Initialize the RpcChannel with on ordered list of strategies."""
75
- self._rpc_strategies = rpc_strategies
73
+ def __init__(self, rpc_strategies_cb: Callable[[], list[RpcStrategy]]) -> None:
74
+ """Initialize the RpcChannel with an ordered list of strategies."""
75
+ self._rpc_strategies_cb = rpc_strategies_cb
76
76
 
77
77
  async def send_command(
78
78
  self,
@@ -86,11 +86,11 @@ class RpcChannel(V1RpcChannel):
86
86
 
87
87
  # Try each channel in order until one succeeds
88
88
  last_exception = None
89
- for strategy in self._rpc_strategies:
89
+ for strategy in self._rpc_strategies_cb():
90
90
  try:
91
91
  decoded_response = await self._send_rpc(strategy, request)
92
92
  except RoborockException as e:
93
- _LOGGER.warning("Command %s failed on %s channel: %s", method, strategy.name, e)
93
+ _LOGGER.debug("Command %s failed on %s channel: %s", method, strategy.name, e)
94
94
  last_exception = e
95
95
  except Exception as e:
96
96
  _LOGGER.exception("Unexpected error sending command %s on %s channel", method, strategy.name)
@@ -203,23 +203,35 @@ class V1Channel(Channel):
203
203
 
204
204
  @property
205
205
  def rpc_channel(self) -> V1RpcChannel:
206
- """Return the combined RPC channel that prefers local with a fallback to MQTT."""
207
- strategies = []
208
- if local_rpc_strategy := self._create_local_rpc_strategy():
209
- strategies.append(local_rpc_strategy)
210
- strategies.append(self._create_mqtt_rpc_strategy())
211
- return RpcChannel(strategies)
206
+ """Return the combined RPC channel that prefers local with a fallback to MQTT.
207
+
208
+ The returned V1RpcChannel may be long lived and will respect the
209
+ current connection state of the underlying channels.
210
+ """
211
+
212
+ def rpc_strategies_cb() -> list[RpcStrategy]:
213
+ strategies = []
214
+ if local_rpc_strategy := self._create_local_rpc_strategy():
215
+ strategies.append(local_rpc_strategy)
216
+ strategies.append(self._create_mqtt_rpc_strategy())
217
+ return strategies
218
+
219
+ return RpcChannel(rpc_strategies_cb)
212
220
 
213
221
  @property
214
222
  def mqtt_rpc_channel(self) -> V1RpcChannel:
215
- """Return the MQTT-only RPC channel."""
216
- return RpcChannel([self._create_mqtt_rpc_strategy()])
223
+ """Return the MQTT-only RPC channel.
224
+
225
+ The returned V1RpcChannel may be long lived and will respect the
226
+ current connection state of the underlying channels.
227
+ """
228
+ return RpcChannel(lambda: [self._create_mqtt_rpc_strategy()])
217
229
 
218
230
  @property
219
231
  def map_rpc_channel(self) -> V1RpcChannel:
220
232
  """Return the map RPC channel used for fetching map content."""
221
233
  decoder = create_map_response_decoder(security_data=self._security_data)
222
- return RpcChannel([self._create_mqtt_rpc_strategy(decoder)])
234
+ return RpcChannel(lambda: [self._create_mqtt_rpc_strategy(decoder)])
223
235
 
224
236
  def _create_local_rpc_strategy(self) -> RpcStrategy | None:
225
237
  """Create the RPC strategy for local transport."""
@@ -282,7 +294,7 @@ class V1Channel(Channel):
282
294
  try:
283
295
  await self._local_connect(prefer_cache=True)
284
296
  except RoborockException as err:
285
- _LOGGER.warning("Could not establish local connection for device %s: %s", self._device_uid, err)
297
+ _LOGGER.debug("First local connection attempt for device %s failed, will retry: %s", self._device_uid, err)
286
298
 
287
299
  # Start a background task to manage the local connection health. This
288
300
  # happens independent of whether we were able to connect locally now.
@@ -24,7 +24,7 @@ from .session import MqttParams, MqttSession, MqttSessionException
24
24
  _LOGGER = logging.getLogger(__name__)
25
25
  _MQTT_LOGGER = logging.getLogger(f"{__name__}.aiomqtt")
26
26
 
27
- CLIENT_KEEPALIVE = datetime.timedelta(seconds=120)
27
+ CLIENT_KEEPALIVE = datetime.timedelta(seconds=60)
28
28
  TOPIC_KEEPALIVE = datetime.timedelta(seconds=60)
29
29
 
30
30
  # Exponential backoff parameters