python-roborock 2.43.0__tar.gz → 2.44.1__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 (59) hide show
  1. {python_roborock-2.43.0 → python_roborock-2.44.1}/PKG-INFO +1 -1
  2. {python_roborock-2.43.0 → python_roborock-2.44.1}/pyproject.toml +1 -1
  3. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/cli.py +43 -1
  4. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/device_manager.py +8 -2
  5. python_roborock-2.44.1/roborock/devices/traits/sound_volume.py +31 -0
  6. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/v1_rpc_channel.py +4 -1
  7. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/protocols/v1_protocol.py +28 -12
  8. {python_roborock-2.43.0 → python_roborock-2.44.1}/LICENSE +0 -0
  9. {python_roborock-2.43.0 → python_roborock-2.44.1}/README.md +0 -0
  10. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/__init__.py +0 -0
  11. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/api.py +0 -0
  12. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/b01_containers.py +0 -0
  13. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/broadcast_protocol.py +0 -0
  14. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/callbacks.py +0 -0
  15. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/clean_modes.py +0 -0
  16. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/cloud_api.py +0 -0
  17. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/code_mappings.py +0 -0
  18. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/command_cache.py +0 -0
  19. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/const.py +0 -0
  20. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/containers.py +0 -0
  21. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/device_features.py +0 -0
  22. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/README.md +0 -0
  23. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/__init__.py +0 -0
  24. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/a01_channel.py +0 -0
  25. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/b01_channel.py +0 -0
  26. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/cache.py +0 -0
  27. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/channel.py +0 -0
  28. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/device.py +0 -0
  29. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/local_channel.py +0 -0
  30. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/mqtt_channel.py +0 -0
  31. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/traits/b01/__init__.py +0 -0
  32. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/traits/b01/props.py +0 -0
  33. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/traits/clean_summary.py +0 -0
  34. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/traits/dnd.py +0 -0
  35. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/traits/dyad.py +0 -0
  36. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/traits/status.py +0 -0
  37. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/traits/trait.py +0 -0
  38. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/traits/zeo.py +0 -0
  39. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/devices/v1_channel.py +0 -0
  40. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/exceptions.py +0 -0
  41. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/mqtt/__init__.py +0 -0
  42. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/mqtt/roborock_session.py +0 -0
  43. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/mqtt/session.py +0 -0
  44. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/protocol.py +0 -0
  45. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/protocols/a01_protocol.py +0 -0
  46. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/protocols/b01_protocol.py +0 -0
  47. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/py.typed +0 -0
  48. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/roborock_future.py +0 -0
  49. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/roborock_message.py +0 -0
  50. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/roborock_typing.py +0 -0
  51. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/util.py +0 -0
  52. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/version_1_apis/__init__.py +0 -0
  53. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/version_1_apis/roborock_client_v1.py +0 -0
  54. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  55. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  56. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/version_a01_apis/__init__.py +0 -0
  57. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  58. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
  59. {python_roborock-2.43.0 → python_roborock-2.44.1}/roborock/web_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-roborock
3
- Version: 2.43.0
3
+ Version: 2.44.1
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.43.0"
3
+ version = "2.44.1"
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"
@@ -382,7 +382,7 @@ async def execute_scene(ctx, scene_id):
382
382
  @click.pass_context
383
383
  @async_command
384
384
  async def status(ctx, device_id: str):
385
- """Get device status - unified implementation for both modes."""
385
+ """Get device status."""
386
386
  context: RoborockContext = ctx.obj
387
387
 
388
388
  device_manager = await context.get_device_manager()
@@ -414,6 +414,46 @@ async def clean_summary(ctx, device_id: str):
414
414
  click.echo(dump_json(clean_summary_result.as_dict()))
415
415
 
416
416
 
417
+ @session.command()
418
+ @click.option("--device_id", required=True)
419
+ @click.pass_context
420
+ @async_command
421
+ async def volume(ctx, device_id: str):
422
+ """Get device volume."""
423
+ context: RoborockContext = ctx.obj
424
+
425
+ device_manager = await context.get_device_manager()
426
+ device = await device_manager.get_device(device_id)
427
+
428
+ if not (volume_trait := device.traits.get("sound_volume")):
429
+ click.echo(f"Device {device.name} does not have a volume trait")
430
+ return
431
+
432
+ volume_result = await volume_trait.get_volume()
433
+ click.echo(f"Device {device_id} volume:")
434
+ click.echo(volume_result)
435
+
436
+
437
+ @session.command()
438
+ @click.option("--device_id", required=True)
439
+ @click.option("--volume", required=True, type=int)
440
+ @click.pass_context
441
+ @async_command
442
+ async def set_volume(ctx, device_id: str, volume: int):
443
+ """Set the devicevolume."""
444
+ context: RoborockContext = ctx.obj
445
+
446
+ device_manager = await context.get_device_manager()
447
+ device = await device_manager.get_device(device_id)
448
+
449
+ if not (volume_trait := device.traits.get("sound_volume")):
450
+ click.echo(f"Device {device.name} does not have a volume trait")
451
+ return
452
+
453
+ await volume_trait.set_volume(volume)
454
+ click.echo(f"Set Device {device_id} volume to {volume}")
455
+
456
+
417
457
  @click.command()
418
458
  @click.option("--device_id", required=True)
419
459
  @click.option("--cmd", required=True)
@@ -653,6 +693,8 @@ cli.add_command(session)
653
693
  cli.add_command(get_device_info)
654
694
  cli.add_command(update_docs)
655
695
  cli.add_command(clean_summary)
696
+ cli.add_command(volume)
697
+ cli.add_command(set_volume)
656
698
 
657
699
 
658
700
  def main():
@@ -5,6 +5,8 @@ import enum
5
5
  import logging
6
6
  from collections.abc import Awaitable, Callable
7
7
 
8
+ import aiohttp
9
+
8
10
  from roborock.code_mappings import RoborockCategory
9
11
  from roborock.containers import (
10
12
  HomeData,
@@ -25,6 +27,7 @@ from .traits.b01.props import B01PropsApi
25
27
  from .traits.clean_summary import CleanSummaryTrait
26
28
  from .traits.dnd import DoNotDisturbTrait
27
29
  from .traits.dyad import DyadApi
30
+ from .traits.sound_volume import SoundVolumeTrait
28
31
  from .traits.status import StatusTrait
29
32
  from .traits.trait import Trait
30
33
  from .traits.zeo import ZeoApi
@@ -112,7 +115,9 @@ class DeviceManager:
112
115
  await asyncio.gather(*tasks)
113
116
 
114
117
 
115
- def create_home_data_api(email: str, user_data: UserData) -> HomeDataApi:
118
+ def create_home_data_api(
119
+ email: str, user_data: UserData, base_url: str | None = None, session: aiohttp.ClientSession | None = None
120
+ ) -> HomeDataApi:
116
121
  """Create a home data API wrapper.
117
122
 
118
123
  This function creates a wrapper around the Roborock API client to fetch
@@ -121,7 +126,7 @@ def create_home_data_api(email: str, user_data: UserData) -> HomeDataApi:
121
126
 
122
127
  # Note: This will auto discover the API base URL. This can be improved
123
128
  # by caching this next to `UserData` if needed to avoid unnecessary API calls.
124
- client = RoborockApiClient(email)
129
+ client = RoborockApiClient(username=email, base_url=base_url, session=session)
125
130
 
126
131
  async def home_data_api() -> HomeData:
127
132
  return await client.get_home_data_v3(user_data)
@@ -156,6 +161,7 @@ async def create_device_manager(
156
161
  traits.append(StatusTrait(product, channel.rpc_channel))
157
162
  traits.append(DoNotDisturbTrait(channel.rpc_channel))
158
163
  traits.append(CleanSummaryTrait(channel.rpc_channel))
164
+ traits.append(SoundVolumeTrait(channel.rpc_channel))
159
165
  case DeviceVersion.A01:
160
166
  mqtt_channel = create_mqtt_channel(user_data, mqtt_params, mqtt_session, device)
161
167
  match product.category:
@@ -0,0 +1,31 @@
1
+ """Module for controlling the sound volume of Roborock devices."""
2
+
3
+ from roborock.devices.traits.trait import Trait
4
+ from roborock.devices.v1_rpc_channel import V1RpcChannel
5
+ from roborock.exceptions import RoborockException
6
+ from roborock.roborock_typing import RoborockCommand
7
+
8
+ __all__ = [
9
+ "SoundVolumeTrait",
10
+ ]
11
+
12
+
13
+ class SoundVolumeTrait(Trait):
14
+ """Trait for controlling the sound volume of a Roborock device."""
15
+
16
+ name = "sound_volume"
17
+
18
+ def __init__(self, rpc_channel: V1RpcChannel) -> None:
19
+ """Initialize the SoundVolumeTrait."""
20
+ self._rpc_channel = rpc_channel
21
+
22
+ async def get_volume(self) -> int:
23
+ """Get the current sound volume of the device."""
24
+ response = await self._rpc_channel.send_command(RoborockCommand.GET_SOUND_VOLUME)
25
+ if not isinstance(response, list) or not response:
26
+ raise RoborockException(f"Unexpected volume format: {response!r}")
27
+ return int(response[0])
28
+
29
+ async def set_volume(self, volume: int) -> None:
30
+ """Set the sound volume of the device."""
31
+ await self._rpc_channel.send_command(RoborockCommand.CHANGE_SOUND_VOLUME, params=[volume])
@@ -149,7 +149,10 @@ class PayloadEncodedV1RpcChannel(BaseV1RpcChannel):
149
149
  return
150
150
  _LOGGER.debug("Received response (request_id=%s): %s", self._name, decoded.request_id)
151
151
  if decoded.request_id == request_message.request_id:
152
- future.set_result(decoded.data)
152
+ if decoded.api_error:
153
+ future.set_exception(decoded.api_error)
154
+ else:
155
+ future.set_result(decoded.data)
153
156
 
154
157
  unsub = await self._channel.subscribe(find_response)
155
158
  try:
@@ -108,9 +108,18 @@ class ResponseMessage:
108
108
  data: ResponseData
109
109
  """The data of the response, where the type depends on the command."""
110
110
 
111
+ api_error: RoborockException | None = None
112
+ """The API error message of the response if any."""
113
+
111
114
 
112
115
  def decode_rpc_response(message: RoborockMessage) -> ResponseMessage:
113
- """Decode a V1 RPC_RESPONSE message."""
116
+ """Decode a V1 RPC_RESPONSE message.
117
+
118
+ This will raise a RoborockException if the message cannot be parsed. A
119
+ response object will be returned even if there is an error in the
120
+ response, as long as we can extract the request ID. This is so we can
121
+ associate an API response with a request even if there was an error.
122
+ """
114
123
  if not message.payload:
115
124
  return ResponseMessage(request_id=message.seq, data={})
116
125
  try:
@@ -136,19 +145,26 @@ def decode_rpc_response(message: RoborockMessage) -> ResponseMessage:
136
145
  ) from e
137
146
 
138
147
  request_id: int | None = data_point_response.get("id")
148
+ exc: RoborockException | None = None
139
149
  if error := data_point_response.get("error"):
140
- raise RoborockException(f"Error in message: {error}")
141
-
150
+ exc = RoborockException(error)
142
151
  if not (result := data_point_response.get("result")):
143
- raise RoborockException(f"Invalid V1 message format: missing 'result' in data point for {message.payload!r}")
144
- _LOGGER.debug("Decoded V1 message result: %s", result)
145
- if isinstance(result, str) and result == "ok":
146
- result = {}
147
- if not isinstance(result, (dict, list, int)):
148
- raise RoborockException(
149
- f"Invalid V1 message format: 'result' was unexpected type {type(result)}. {message.payload!r}"
150
- )
151
- return ResponseMessage(request_id=request_id, data=result)
152
+ exc = RoborockException(f"Invalid V1 message format: missing 'result' in data point for {message.payload!r}")
153
+ else:
154
+ _LOGGER.debug("Decoded V1 message result: %s", result)
155
+ if isinstance(result, str):
156
+ if result == "unknown_method":
157
+ exc = RoborockException("The method called is not recognized by the device.")
158
+ elif result != "ok":
159
+ exc = RoborockException(f"Unexpected API Result: {result}")
160
+ result = {}
161
+ if not isinstance(result, (dict, list, int)):
162
+ raise RoborockException(
163
+ f"Invalid V1 message format: 'result' was unexpected type {type(result)}. {message.payload!r}"
164
+ )
165
+ if not request_id and exc:
166
+ raise exc
167
+ return ResponseMessage(request_id=request_id, data=result, api_error=exc)
152
168
 
153
169
 
154
170
  @dataclass