python-roborock 2.51.0__tar.gz → 2.52.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 (64) hide show
  1. {python_roborock-2.51.0 → python_roborock-2.52.0}/PKG-INFO +1 -1
  2. {python_roborock-2.51.0 → python_roborock-2.52.0}/pyproject.toml +1 -1
  3. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/cli.py +11 -0
  4. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/device_manager.py +4 -4
  5. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/traits/v1/__init__.py +11 -4
  6. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/traits/v1/common.py +2 -2
  7. python_roborock-2.52.0/roborock/devices/traits/v1/rooms.py +93 -0
  8. {python_roborock-2.51.0 → python_roborock-2.52.0}/LICENSE +0 -0
  9. {python_roborock-2.51.0 → python_roborock-2.52.0}/README.md +0 -0
  10. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/__init__.py +0 -0
  11. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/api.py +0 -0
  12. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/b01_containers.py +0 -0
  13. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/broadcast_protocol.py +0 -0
  14. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/callbacks.py +0 -0
  15. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/clean_modes.py +0 -0
  16. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/cloud_api.py +0 -0
  17. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/code_mappings.py +0 -0
  18. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/command_cache.py +0 -0
  19. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/const.py +0 -0
  20. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/containers.py +0 -0
  21. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/device_features.py +0 -0
  22. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/README.md +0 -0
  23. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/__init__.py +0 -0
  24. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/a01_channel.py +0 -0
  25. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/b01_channel.py +0 -0
  26. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/cache.py +0 -0
  27. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/channel.py +0 -0
  28. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/device.py +0 -0
  29. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/local_channel.py +0 -0
  30. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/mqtt_channel.py +0 -0
  31. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/traits/__init__.py +0 -0
  32. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/traits/a01/__init__.py +0 -0
  33. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/traits/b01/__init__.py +0 -0
  34. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/traits/traits_mixin.py +0 -0
  35. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/traits/v1/clean_summary.py +0 -0
  36. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/traits/v1/consumeable.py +0 -0
  37. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/traits/v1/do_not_disturb.py +0 -0
  38. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/traits/v1/maps.py +0 -0
  39. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/traits/v1/status.py +0 -0
  40. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/traits/v1/volume.py +0 -0
  41. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/v1_channel.py +0 -0
  42. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/devices/v1_rpc_channel.py +0 -0
  43. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/exceptions.py +0 -0
  44. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/map/map_parser.py +0 -0
  45. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/mqtt/__init__.py +0 -0
  46. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/mqtt/roborock_session.py +0 -0
  47. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/mqtt/session.py +0 -0
  48. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/protocol.py +0 -0
  49. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/protocols/a01_protocol.py +0 -0
  50. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/protocols/b01_protocol.py +0 -0
  51. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/protocols/v1_protocol.py +0 -0
  52. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/py.typed +0 -0
  53. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/roborock_future.py +0 -0
  54. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/roborock_message.py +0 -0
  55. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/roborock_typing.py +0 -0
  56. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/util.py +0 -0
  57. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/version_1_apis/__init__.py +0 -0
  58. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/version_1_apis/roborock_client_v1.py +0 -0
  59. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/version_1_apis/roborock_local_client_v1.py +0 -0
  60. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/version_1_apis/roborock_mqtt_client_v1.py +0 -0
  61. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/version_a01_apis/__init__.py +0 -0
  62. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/version_a01_apis/roborock_client_a01.py +0 -0
  63. {python_roborock-2.51.0 → python_roborock-2.52.0}/roborock/version_a01_apis/roborock_mqtt_client_a01.py +0 -0
  64. {python_roborock-2.51.0 → python_roborock-2.52.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.51.0
3
+ Version: 2.52.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.51.0"
3
+ version = "2.52.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"
@@ -475,6 +475,16 @@ async def reset_consumable(ctx, device_id: str, consumable: str):
475
475
  click.echo(f"Reset {consumable} for device {device_id}")
476
476
 
477
477
 
478
+ @session.command()
479
+ @click.option("--device_id", required=True)
480
+ @click.pass_context
481
+ @async_command
482
+ async def rooms(ctx, device_id: str):
483
+ """Get device room mapping info."""
484
+ context: RoborockContext = ctx.obj
485
+ await _display_v1_trait(context, device_id, lambda v1: v1.rooms)
486
+
487
+
478
488
  @click.command()
479
489
  @click.option("--device_id", required=True)
480
490
  @click.option("--cmd", required=True)
@@ -719,6 +729,7 @@ cli.add_command(set_volume)
719
729
  cli.add_command(maps)
720
730
  cli.add_command(consumables)
721
731
  cli.add_command(reset_consumable)
732
+ cli.add_command(rooms)
722
733
 
723
734
 
724
735
  def main():
@@ -35,7 +35,7 @@ __all__ = [
35
35
 
36
36
 
37
37
  HomeDataApi = Callable[[], Awaitable[HomeData]]
38
- DeviceCreator = Callable[[HomeDataDevice, HomeDataProduct], RoborockDevice]
38
+ DeviceCreator = Callable[[HomeData, HomeDataDevice, HomeDataProduct], RoborockDevice]
39
39
 
40
40
 
41
41
  class DeviceVersion(enum.StrEnum):
@@ -84,7 +84,7 @@ class DeviceManager:
84
84
  for duid, (device, product) in device_products.items():
85
85
  if duid in self._devices:
86
86
  continue
87
- new_device = self._device_creator(device, product)
87
+ new_device = self._device_creator(home_data, device, product)
88
88
  await new_device.connect()
89
89
  new_devices[duid] = new_device
90
90
 
@@ -143,13 +143,13 @@ async def create_device_manager(
143
143
  mqtt_params = create_mqtt_params(user_data.rriot)
144
144
  mqtt_session = await create_lazy_mqtt_session(mqtt_params)
145
145
 
146
- def device_creator(device: HomeDataDevice, product: HomeDataProduct) -> RoborockDevice:
146
+ def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDataProduct) -> RoborockDevice:
147
147
  channel: Channel
148
148
  trait: Trait
149
149
  match device.pv:
150
150
  case DeviceVersion.V1:
151
151
  channel = create_v1_channel(user_data, mqtt_params, mqtt_session, device, cache)
152
- trait = v1.create(product, channel.rpc_channel, channel.mqtt_rpc_channel)
152
+ trait = v1.create(product, home_data, channel.rpc_channel, channel.mqtt_rpc_channel)
153
153
  case DeviceVersion.A01:
154
154
  channel = create_mqtt_channel(user_data, mqtt_params, mqtt_session, device)
155
155
  trait = a01.create(product, channel)
@@ -12,6 +12,7 @@ from .common import V1TraitMixin
12
12
  from .consumeable import ConsumableTrait
13
13
  from .do_not_disturb import DoNotDisturbTrait
14
14
  from .maps import MapsTrait
15
+ from .rooms import RoomsTrait
15
16
  from .status import StatusTrait
16
17
  from .volume import SoundVolumeTrait
17
18
 
@@ -41,14 +42,18 @@ class PropertiesApi(Trait):
41
42
  dnd: DoNotDisturbTrait
42
43
  clean_summary: CleanSummaryTrait
43
44
  sound_volume: SoundVolumeTrait
45
+ rooms: RoomsTrait
44
46
  maps: MapsTrait
45
47
  consumables: ConsumableTrait
46
48
 
47
49
  # In the future optional fields can be added below based on supported features
48
50
 
49
- def __init__(self, product: HomeDataProduct, rpc_channel: V1RpcChannel, mqtt_rpc_channel: V1RpcChannel) -> None:
50
- """Initialize the V1TraitProps with None values."""
51
+ def __init__(
52
+ self, product: HomeDataProduct, home_data: HomeData, rpc_channel: V1RpcChannel, mqtt_rpc_channel: V1RpcChannel
53
+ ) -> None:
54
+ """Initialize the V1TraitProps."""
51
55
  self.status = StatusTrait(product)
56
+ self.rooms = RoomsTrait(home_data)
52
57
  self.maps = MapsTrait(self.status)
53
58
 
54
59
  # This is a hack to allow setting the rpc_channel on all traits. This is
@@ -67,6 +72,8 @@ class PropertiesApi(Trait):
67
72
  trait._rpc_channel = rpc_channel
68
73
 
69
74
 
70
- def create(product: HomeDataProduct, rpc_channel: V1RpcChannel, mqtt_rpc_channel: V1RpcChannel) -> PropertiesApi:
75
+ def create(
76
+ product: HomeDataProduct, home_data: HomeData, rpc_channel: V1RpcChannel, mqtt_rpc_channel: V1RpcChannel
77
+ ) -> PropertiesApi:
71
78
  """Create traits for V1 devices."""
72
- return PropertiesApi(product, rpc_channel, mqtt_rpc_channel)
79
+ return PropertiesApi(product, home_data, rpc_channel, mqtt_rpc_channel)
@@ -38,7 +38,7 @@ class V1TraitMixin(ABC):
38
38
  command: ClassVar[RoborockCommand]
39
39
 
40
40
  @classmethod
41
- def _parse_type_response(cls, response: V1ResponseData) -> Self:
41
+ def _parse_type_response(cls, response: V1ResponseData) -> RoborockBase:
42
42
  """Parse the response from the device into a a RoborockBase.
43
43
 
44
44
  Subclasses should override this method to implement custom parsing
@@ -53,7 +53,7 @@ class V1TraitMixin(ABC):
53
53
  raise ValueError(f"Unexpected {cls} response format: {response!r}")
54
54
  return cls.from_dict(response)
55
55
 
56
- def _parse_response(self, response: V1ResponseData) -> Self:
56
+ def _parse_response(self, response: V1ResponseData) -> RoborockBase:
57
57
  """Parse the response from the device into a a RoborockBase.
58
58
 
59
59
  This is used by subclasses that want to override the class
@@ -0,0 +1,93 @@
1
+ """Trait for managing room mappings on Roborock devices."""
2
+
3
+ import logging
4
+ from dataclasses import dataclass
5
+
6
+ from roborock.containers import HomeData, RoborockBase, RoomMapping
7
+ from roborock.devices.traits.v1 import common
8
+ from roborock.roborock_typing import RoborockCommand
9
+
10
+ _LOGGER = logging.getLogger(__name__)
11
+
12
+ _DEFAULT_NAME = "Unknown"
13
+
14
+
15
+ @dataclass
16
+ class NamedRoomMapping(RoomMapping):
17
+ """Dataclass representing a mapping of a room segment to a name.
18
+
19
+ The name information is not provided by the device directly, but is provided
20
+ from the HomeData based on the iot_id from the room.
21
+ """
22
+
23
+ name: str
24
+ """The human-readable name of the room, if available."""
25
+
26
+
27
+ @dataclass
28
+ class Rooms(RoborockBase):
29
+ """Dataclass representing a collection of room mappings."""
30
+
31
+ rooms: list[NamedRoomMapping] | None = None
32
+ """List of room mappings."""
33
+
34
+ @property
35
+ def room_map(self) -> dict[int, NamedRoomMapping]:
36
+ """Returns a mapping of segment_id to NamedRoomMapping."""
37
+ if self.rooms is None:
38
+ return {}
39
+ return {room.segment_id: room for room in self.rooms}
40
+
41
+
42
+ class RoomsTrait(Rooms, common.V1TraitMixin):
43
+ """Trait for managing the room mappings of Roborock devices."""
44
+
45
+ command = RoborockCommand.GET_ROOM_MAPPING
46
+
47
+ def __init__(self, home_data: HomeData) -> None:
48
+ """Initialize the RoomsTrait."""
49
+ super().__init__()
50
+ self._home_data = home_data
51
+
52
+ @property
53
+ def _iot_id_room_name_map(self) -> dict[str, str]:
54
+ """Returns a dictionary of Room IOT IDs to room names."""
55
+ return {str(room.id): room.name for room in self._home_data.rooms or ()}
56
+
57
+ def _parse_response(self, response: common.V1ResponseData) -> Rooms:
58
+ """Parse the response from the device into a list of NamedRoomMapping."""
59
+ if not isinstance(response, list):
60
+ raise ValueError(f"Unexpected RoomsTrait response format: {response!r}")
61
+ name_map = self._iot_id_room_name_map
62
+ segment_pairs = _extract_segment_pairs(response)
63
+ return Rooms(
64
+ rooms=[
65
+ NamedRoomMapping(segment_id=segment_id, iot_id=iot_id, name=name_map.get(iot_id, _DEFAULT_NAME))
66
+ for segment_id, iot_id in segment_pairs
67
+ ]
68
+ )
69
+
70
+
71
+ def _extract_segment_pairs(response: list) -> list[tuple[int, str]]:
72
+ """Extract segment_id and iot_id pairs from the response.
73
+
74
+ The response format can be either a flat list of [segment_id, iot_id] or a
75
+ list of lists, where each inner list is a pair of [segment_id, iot_id]. This
76
+ function normalizes the response into a list of (segment_id, iot_id) tuples
77
+
78
+ NOTE: We currently only partial samples of the room mapping formats, so
79
+ improving test coverage with samples from a real device with this format
80
+ would be helpful.
81
+ """
82
+ if len(response) == 2 and not isinstance(response[0], list):
83
+ segment_id, iot_id = response[0], response[1]
84
+ return [(segment_id, iot_id)]
85
+
86
+ segment_pairs: list[tuple[int, str]] = []
87
+ for part in response:
88
+ if not isinstance(part, list) or len(part) < 2:
89
+ _LOGGER.warning("Unexpected room mapping entry format: %r", part)
90
+ continue
91
+ segment_id, iot_id = part[0], part[1]
92
+ segment_pairs.append((segment_id, iot_id))
93
+ return segment_pairs