roborock-cli 0.1.1__py3-none-any.whl

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 (106) hide show
  1. roborock_cli/__init__.py +3 -0
  2. roborock_cli/__main__.py +76 -0
  3. roborock_cli/_vendor/VERSION +6 -0
  4. roborock_cli/_vendor/__init__.py +0 -0
  5. roborock_cli/_vendor/roborock/__init__.py +27 -0
  6. roborock_cli/_vendor/roborock/broadcast_protocol.py +114 -0
  7. roborock_cli/_vendor/roborock/callbacks.py +130 -0
  8. roborock_cli/_vendor/roborock/cli.py +1338 -0
  9. roborock_cli/_vendor/roborock/const.py +84 -0
  10. roborock_cli/_vendor/roborock/data/__init__.py +9 -0
  11. roborock_cli/_vendor/roborock/data/b01_q10/__init__.py +2 -0
  12. roborock_cli/_vendor/roborock/data/b01_q10/b01_q10_code_mappings.py +213 -0
  13. roborock_cli/_vendor/roborock/data/b01_q10/b01_q10_containers.py +102 -0
  14. roborock_cli/_vendor/roborock/data/b01_q7/__init__.py +2 -0
  15. roborock_cli/_vendor/roborock/data/b01_q7/b01_q7_code_mappings.py +303 -0
  16. roborock_cli/_vendor/roborock/data/b01_q7/b01_q7_containers.py +302 -0
  17. roborock_cli/_vendor/roborock/data/code_mappings.py +198 -0
  18. roborock_cli/_vendor/roborock/data/containers.py +530 -0
  19. roborock_cli/_vendor/roborock/data/dyad/__init__.py +2 -0
  20. roborock_cli/_vendor/roborock/data/dyad/dyad_code_mappings.py +102 -0
  21. roborock_cli/_vendor/roborock/data/dyad/dyad_containers.py +28 -0
  22. roborock_cli/_vendor/roborock/data/v1/__init__.py +3 -0
  23. roborock_cli/_vendor/roborock/data/v1/v1_clean_modes.py +192 -0
  24. roborock_cli/_vendor/roborock/data/v1/v1_code_mappings.py +644 -0
  25. roborock_cli/_vendor/roborock/data/v1/v1_containers.py +800 -0
  26. roborock_cli/_vendor/roborock/data/zeo/__init__.py +2 -0
  27. roborock_cli/_vendor/roborock/data/zeo/zeo_code_mappings.py +138 -0
  28. roborock_cli/_vendor/roborock/data/zeo/zeo_containers.py +0 -0
  29. roborock_cli/_vendor/roborock/device_features.py +668 -0
  30. roborock_cli/_vendor/roborock/devices/README.md +41 -0
  31. roborock_cli/_vendor/roborock/devices/__init__.py +11 -0
  32. roborock_cli/_vendor/roborock/devices/cache.py +143 -0
  33. roborock_cli/_vendor/roborock/devices/device.py +240 -0
  34. roborock_cli/_vendor/roborock/devices/device_manager.py +269 -0
  35. roborock_cli/_vendor/roborock/devices/file_cache.py +79 -0
  36. roborock_cli/_vendor/roborock/devices/rpc/__init__.py +14 -0
  37. roborock_cli/_vendor/roborock/devices/rpc/a01_channel.py +94 -0
  38. roborock_cli/_vendor/roborock/devices/rpc/b01_q10_channel.py +57 -0
  39. roborock_cli/_vendor/roborock/devices/rpc/b01_q7_channel.py +101 -0
  40. roborock_cli/_vendor/roborock/devices/rpc/v1_channel.py +457 -0
  41. roborock_cli/_vendor/roborock/devices/traits/__init__.py +28 -0
  42. roborock_cli/_vendor/roborock/devices/traits/a01/__init__.py +191 -0
  43. roborock_cli/_vendor/roborock/devices/traits/b01/__init__.py +12 -0
  44. roborock_cli/_vendor/roborock/devices/traits/b01/q10/__init__.py +76 -0
  45. roborock_cli/_vendor/roborock/devices/traits/b01/q10/command.py +32 -0
  46. roborock_cli/_vendor/roborock/devices/traits/b01/q10/common.py +115 -0
  47. roborock_cli/_vendor/roborock/devices/traits/b01/q10/status.py +32 -0
  48. roborock_cli/_vendor/roborock/devices/traits/b01/q10/vacuum.py +81 -0
  49. roborock_cli/_vendor/roborock/devices/traits/b01/q7/__init__.py +136 -0
  50. roborock_cli/_vendor/roborock/devices/traits/b01/q7/clean_summary.py +75 -0
  51. roborock_cli/_vendor/roborock/devices/traits/traits_mixin.py +64 -0
  52. roborock_cli/_vendor/roborock/devices/traits/v1/__init__.py +344 -0
  53. roborock_cli/_vendor/roborock/devices/traits/v1/child_lock.py +29 -0
  54. roborock_cli/_vendor/roborock/devices/traits/v1/clean_summary.py +83 -0
  55. roborock_cli/_vendor/roborock/devices/traits/v1/command.py +38 -0
  56. roborock_cli/_vendor/roborock/devices/traits/v1/common.py +172 -0
  57. roborock_cli/_vendor/roborock/devices/traits/v1/consumeable.py +48 -0
  58. roborock_cli/_vendor/roborock/devices/traits/v1/device_features.py +74 -0
  59. roborock_cli/_vendor/roborock/devices/traits/v1/do_not_disturb.py +41 -0
  60. roborock_cli/_vendor/roborock/devices/traits/v1/dust_collection_mode.py +13 -0
  61. roborock_cli/_vendor/roborock/devices/traits/v1/flow_led_status.py +29 -0
  62. roborock_cli/_vendor/roborock/devices/traits/v1/home.py +285 -0
  63. roborock_cli/_vendor/roborock/devices/traits/v1/led_status.py +43 -0
  64. roborock_cli/_vendor/roborock/devices/traits/v1/map_content.py +83 -0
  65. roborock_cli/_vendor/roborock/devices/traits/v1/maps.py +80 -0
  66. roborock_cli/_vendor/roborock/devices/traits/v1/network_info.py +55 -0
  67. roborock_cli/_vendor/roborock/devices/traits/v1/rooms.py +105 -0
  68. roborock_cli/_vendor/roborock/devices/traits/v1/routines.py +26 -0
  69. roborock_cli/_vendor/roborock/devices/traits/v1/smart_wash_params.py +13 -0
  70. roborock_cli/_vendor/roborock/devices/traits/v1/status.py +101 -0
  71. roborock_cli/_vendor/roborock/devices/traits/v1/valley_electricity_timer.py +44 -0
  72. roborock_cli/_vendor/roborock/devices/traits/v1/volume.py +27 -0
  73. roborock_cli/_vendor/roborock/devices/traits/v1/wash_towel_mode.py +13 -0
  74. roborock_cli/_vendor/roborock/devices/transport/__init__.py +8 -0
  75. roborock_cli/_vendor/roborock/devices/transport/channel.py +32 -0
  76. roborock_cli/_vendor/roborock/devices/transport/local_channel.py +295 -0
  77. roborock_cli/_vendor/roborock/devices/transport/mqtt_channel.py +118 -0
  78. roborock_cli/_vendor/roborock/diagnostics.py +166 -0
  79. roborock_cli/_vendor/roborock/exceptions.py +95 -0
  80. roborock_cli/_vendor/roborock/map/__init__.py +7 -0
  81. roborock_cli/_vendor/roborock/map/map_parser.py +123 -0
  82. roborock_cli/_vendor/roborock/mqtt/__init__.py +10 -0
  83. roborock_cli/_vendor/roborock/mqtt/health_manager.py +60 -0
  84. roborock_cli/_vendor/roborock/mqtt/roborock_session.py +463 -0
  85. roborock_cli/_vendor/roborock/mqtt/session.py +108 -0
  86. roborock_cli/_vendor/roborock/protocol.py +558 -0
  87. roborock_cli/_vendor/roborock/protocols/__init__.py +3 -0
  88. roborock_cli/_vendor/roborock/protocols/a01_protocol.py +74 -0
  89. roborock_cli/_vendor/roborock/protocols/b01_q10_protocol.py +87 -0
  90. roborock_cli/_vendor/roborock/protocols/b01_q7_protocol.py +81 -0
  91. roborock_cli/_vendor/roborock/protocols/v1_protocol.py +271 -0
  92. roborock_cli/_vendor/roborock/py.typed +0 -0
  93. roborock_cli/_vendor/roborock/roborock_message.py +246 -0
  94. roborock_cli/_vendor/roborock/roborock_typing.py +382 -0
  95. roborock_cli/_vendor/roborock/util.py +54 -0
  96. roborock_cli/_vendor/roborock/web_api.py +761 -0
  97. roborock_cli/cli.py +715 -0
  98. roborock_cli/connection.py +202 -0
  99. roborock_cli/helpers.py +71 -0
  100. roborock_cli/server.py +759 -0
  101. roborock_cli/setup_auth.py +92 -0
  102. roborock_cli-0.1.1.dist-info/METADATA +172 -0
  103. roborock_cli-0.1.1.dist-info/RECORD +106 -0
  104. roborock_cli-0.1.1.dist-info/WHEEL +4 -0
  105. roborock_cli-0.1.1.dist-info/entry_points.txt +2 -0
  106. roborock_cli-0.1.1.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,80 @@
1
+ """Trait for managing maps and room mappings on Roborock devices.
2
+
3
+ New datatypes are introduced here to manage the additional information associated
4
+ with maps and rooms, such as map names and room names. These override the
5
+ base container datatypes to add additional fields.
6
+ """
7
+
8
+ import logging
9
+ from typing import Self
10
+
11
+ from roborock_cli._vendor.roborock.data import MultiMapsList, MultiMapsListMapInfo
12
+ from roborock_cli._vendor.roborock.devices.traits.v1 import common
13
+ from roborock_cli._vendor.roborock.roborock_typing import RoborockCommand
14
+
15
+ from .status import StatusTrait
16
+
17
+ _LOGGER = logging.getLogger(__name__)
18
+
19
+
20
+ @common.mqtt_rpc_channel
21
+ class MapsTrait(MultiMapsList, common.V1TraitMixin):
22
+ """Trait for managing the maps of Roborock devices.
23
+
24
+ A device may have multiple maps, each identified by a unique map_flag.
25
+ Each map can have multiple rooms associated with it, in a `RoomMapping`.
26
+
27
+ The MapsTrait depends on the StatusTrait to determine the currently active
28
+ map. It is the responsibility of the caller to ensure that the StatusTrait
29
+ is up to date before using this trait. However, there is a possibility of
30
+ races if another client changes the current map between the time the
31
+ StatusTrait is refreshed and when the MapsTrait is used. This is mitigated
32
+ by the fact that the map list is unlikely to change frequently, and the
33
+ current map is only changed when the user explicitly switches maps.
34
+ """
35
+
36
+ command = RoborockCommand.GET_MULTI_MAPS_LIST
37
+
38
+ def __init__(self, status_trait: StatusTrait) -> None:
39
+ """Initialize the MapsTrait.
40
+
41
+ We keep track of the StatusTrait to ensure we have the latest
42
+ status information when dealing with maps.
43
+ """
44
+ super().__init__()
45
+ self._status_trait = status_trait
46
+
47
+ @property
48
+ def current_map(self) -> int | None:
49
+ """Returns the currently active map (map_flag), if available."""
50
+ return self._status_trait.current_map
51
+
52
+ @property
53
+ def current_map_info(self) -> MultiMapsListMapInfo | None:
54
+ """Returns the currently active map info, if available."""
55
+ if (current_map := self.current_map) is None or self.map_info is None:
56
+ return None
57
+ for map_info in self.map_info:
58
+ if map_info.map_flag == current_map:
59
+ return map_info
60
+ return None
61
+
62
+ async def set_current_map(self, map_flag: int) -> None:
63
+ """Update the current map of the device by it's map_flag id."""
64
+ await self.rpc_channel.send_command(RoborockCommand.LOAD_MULTI_MAP, params=[map_flag])
65
+ # Refresh our status to make sure it reflects the new map
66
+ await self._status_trait.refresh()
67
+
68
+ def _parse_response(self, response: common.V1ResponseData) -> Self:
69
+ """Parse the response from the device into a MapsTrait instance.
70
+
71
+ This overrides the base implementation to handle the specific
72
+ response format for the multi maps list. This is needed because we have
73
+ a custom constructor that requires the StatusTrait.
74
+ """
75
+ if not isinstance(response, list):
76
+ raise ValueError(f"Unexpected MapsTrait response format: {response!r}")
77
+ response = response[0]
78
+ if not isinstance(response, dict):
79
+ raise ValueError(f"Unexpected MapsTrait response format: {response!r}")
80
+ return MultiMapsList.from_dict(response)
@@ -0,0 +1,55 @@
1
+ """Trait for device network information."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ from roborock_cli._vendor.roborock.data import NetworkInfo
8
+ from roborock_cli._vendor.roborock.devices.cache import DeviceCache
9
+ from roborock_cli._vendor.roborock.devices.traits.v1 import common
10
+ from roborock_cli._vendor.roborock.roborock_typing import RoborockCommand
11
+
12
+ _LOGGER = logging.getLogger(__name__)
13
+
14
+
15
+ class NetworkInfoTrait(NetworkInfo, common.V1TraitMixin):
16
+ """Trait for device network information.
17
+
18
+ This trait will always prefer reading from the cache if available. This
19
+ information is usually already fetched when creating the device local
20
+ connection, so reading from the cache avoids an unnecessary RPC call.
21
+ However, we have the fallback to reading from the device if the cache is
22
+ not populated for some reason.
23
+ """
24
+
25
+ command = RoborockCommand.GET_NETWORK_INFO
26
+
27
+ def __init__(self, device_uid: str, device_cache: DeviceCache) -> None: # pylint: disable=super-init-not-called
28
+ """Initialize the trait."""
29
+ self._device_uid = device_uid
30
+ self._device_cache = device_cache
31
+ self.ip = ""
32
+
33
+ async def refresh(self) -> None:
34
+ """Refresh the network info from the cache."""
35
+
36
+ device_cache_data = await self._device_cache.get()
37
+ if device_cache_data.network_info:
38
+ _LOGGER.debug("Using cached network info for device %s", self._device_uid)
39
+ self._update_trait_values(device_cache_data.network_info)
40
+ return
41
+
42
+ # Load from device if not in cache
43
+ _LOGGER.debug("No cached network info for device %s, fetching from device", self._device_uid)
44
+ await super().refresh()
45
+
46
+ # Update the cache with the new network info
47
+ device_cache_data = await self._device_cache.get()
48
+ device_cache_data.network_info = self
49
+ await self._device_cache.set(device_cache_data)
50
+
51
+ def _parse_response(self, response: common.V1ResponseData) -> NetworkInfo:
52
+ """Parse the response from the device into a NetworkInfo."""
53
+ if not isinstance(response, dict):
54
+ raise ValueError(f"Unexpected NetworkInfoTrait response format: {response!r}")
55
+ return NetworkInfo.from_dict(response)
@@ -0,0 +1,105 @@
1
+ """Trait for managing room mappings on Roborock devices."""
2
+
3
+ import logging
4
+ from dataclasses import dataclass
5
+
6
+ from roborock_cli._vendor.roborock.data import HomeData, HomeDataRoom, NamedRoomMapping, RoborockBase
7
+ from roborock_cli._vendor.roborock.devices.traits.v1 import common
8
+ from roborock_cli._vendor.roborock.roborock_typing import RoborockCommand
9
+ from roborock_cli._vendor.roborock.web_api import UserWebApiClient
10
+
11
+ _LOGGER = logging.getLogger(__name__)
12
+
13
+
14
+ @dataclass
15
+ class Rooms(RoborockBase):
16
+ """Dataclass representing a collection of room mappings."""
17
+
18
+ rooms: list[NamedRoomMapping] | None = None
19
+ """List of room mappings."""
20
+
21
+ @property
22
+ def room_map(self) -> dict[int, NamedRoomMapping]:
23
+ """Returns a mapping of segment_id to NamedRoomMapping."""
24
+ if self.rooms is None:
25
+ return {}
26
+ return {room.segment_id: room for room in self.rooms}
27
+
28
+
29
+ class RoomsTrait(Rooms, common.V1TraitMixin):
30
+ """Trait for managing the room mappings of Roborock devices."""
31
+
32
+ command = RoborockCommand.GET_ROOM_MAPPING
33
+
34
+ def __init__(self, home_data: HomeData, web_api: UserWebApiClient) -> None:
35
+ """Initialize the RoomsTrait."""
36
+ super().__init__()
37
+ self._home_data = home_data
38
+ self._web_api = web_api
39
+ self._discovered_iot_ids: set[str] = set()
40
+
41
+ async def refresh(self) -> None:
42
+ """Refresh room mappings and backfill unknown room names from the web API."""
43
+ response = await self.rpc_channel.send_command(self.command)
44
+ if not isinstance(response, list):
45
+ raise ValueError(f"Unexpected RoomsTrait response format: {response!r}")
46
+
47
+ segment_map = _extract_segment_map(response)
48
+ # Track all iot ids seen before. Refresh the room list when new ids are found.
49
+ new_iot_ids = set(segment_map.values()) - set(self._home_data.rooms_map.keys())
50
+ if new_iot_ids - self._discovered_iot_ids:
51
+ _LOGGER.debug("Refreshing room list to discover new room names")
52
+ if updated_rooms := await self._refresh_rooms():
53
+ _LOGGER.debug("Updating rooms: %s", list(updated_rooms))
54
+ self._home_data.rooms = updated_rooms
55
+ self._discovered_iot_ids.update(new_iot_ids)
56
+
57
+ new_data = self._parse_rooms(segment_map, self._home_data.rooms_name_map)
58
+ self._update_trait_values(new_data)
59
+ _LOGGER.debug("Refreshed %s: %s", self.__class__.__name__, new_data)
60
+
61
+ @staticmethod
62
+ def _parse_rooms(
63
+ segment_map: dict[int, str],
64
+ name_map: dict[str, str],
65
+ ) -> Rooms:
66
+ """Parse the response from the device into a list of NamedRoomMapping."""
67
+ return Rooms(
68
+ rooms=[
69
+ NamedRoomMapping(segment_id=segment_id, iot_id=iot_id, raw_name=name_map.get(iot_id))
70
+ for segment_id, iot_id in segment_map.items()
71
+ ]
72
+ )
73
+
74
+ async def _refresh_rooms(self) -> list[HomeDataRoom]:
75
+ """Fetch the latest rooms from the web API."""
76
+ try:
77
+ return await self._web_api.get_rooms()
78
+ except Exception:
79
+ _LOGGER.debug("Failed to fetch rooms from web API", exc_info=True)
80
+ return []
81
+
82
+
83
+ def _extract_segment_map(response: list) -> dict[int, str]:
84
+ """Extract a segment_id -> iot_id mapping from the response.
85
+
86
+ The response format can be either a flat list of [segment_id, iot_id] or a
87
+ list of lists, where each inner list is a pair of [segment_id, iot_id]. This
88
+ function normalizes the response into a dict of segment_id to iot_id.
89
+
90
+ NOTE: We currently only partial samples of the room mapping formats, so
91
+ improving test coverage with samples from a real device with this format
92
+ would be helpful.
93
+ """
94
+ if len(response) == 2 and not isinstance(response[0], list):
95
+ segment_id, iot_id = response[0], response[1]
96
+ return {segment_id: str(iot_id)}
97
+
98
+ segment_map: dict[int, str] = {}
99
+ for part in response:
100
+ if not isinstance(part, list) or len(part) < 2:
101
+ _LOGGER.warning("Unexpected room mapping entry format: %r", part)
102
+ continue
103
+ segment_id, iot_id = part[0], part[1]
104
+ segment_map[segment_id] = str(iot_id)
105
+ return segment_map
@@ -0,0 +1,26 @@
1
+ """Routines trait for V1 devices."""
2
+
3
+ from roborock_cli._vendor.roborock.data.containers import HomeDataScene
4
+ from roborock_cli._vendor.roborock.web_api import UserWebApiClient
5
+
6
+
7
+ class RoutinesTrait:
8
+ """Trait for interacting with routines."""
9
+
10
+ def __init__(self, device_id: str, web_api: UserWebApiClient) -> None:
11
+ """Initialize the routines trait."""
12
+ self._device_id = device_id
13
+ self._web_api = web_api
14
+
15
+ async def get_routines(self) -> list[HomeDataScene]:
16
+ """Get available routines."""
17
+ return await self._web_api.get_routines(self._device_id)
18
+
19
+ async def execute_routine(self, routine_id: int) -> None:
20
+ """Execute a routine by its ID.
21
+
22
+ Technically, routines are per-device, but the API does not
23
+ require the device ID to execute them. This can execute a
24
+ routine for any device but it is exposed here for convenience.
25
+ """
26
+ await self._web_api.execute_routine(routine_id)
@@ -0,0 +1,13 @@
1
+ """Trait for smart wash parameters."""
2
+
3
+ from roborock_cli._vendor.roborock.data import SmartWashParams
4
+ from roborock_cli._vendor.roborock.device_features import is_wash_n_fill_dock
5
+ from roborock_cli._vendor.roborock.devices.traits.v1 import common
6
+ from roborock_cli._vendor.roborock.roborock_typing import RoborockCommand
7
+
8
+
9
+ class SmartWashParamsTrait(SmartWashParams, common.V1TraitMixin):
10
+ """Trait for smart wash parameters."""
11
+
12
+ command = RoborockCommand.GET_SMART_WASH_PARAMS
13
+ requires_dock_type = is_wash_n_fill_dock
@@ -0,0 +1,101 @@
1
+ from functools import cached_property
2
+ from typing import Self
3
+
4
+ from roborock_cli._vendor.roborock import (
5
+ CleanRoutes,
6
+ StatusV2,
7
+ VacuumModes,
8
+ WaterModes,
9
+ get_clean_modes,
10
+ get_clean_routes,
11
+ get_water_mode_mapping,
12
+ get_water_modes,
13
+ )
14
+ from roborock_cli._vendor.roborock.roborock_typing import RoborockCommand
15
+
16
+ from . import common
17
+ from .device_features import DeviceFeaturesTrait
18
+
19
+
20
+ class StatusTrait(StatusV2, common.V1TraitMixin):
21
+ """Trait for managing the status of Roborock devices.
22
+
23
+ The StatusTrait gives you the access to the state of a Roborock vacuum.
24
+ The various attribute options on state change per each device.
25
+ Values like fan speed, mop mode, etc. have different options for every device
26
+ and are dynamically determined.
27
+
28
+ Usage:
29
+ Before accessing status properties, you should call `refresh()` to fetch
30
+ the latest data from the device. You must pass in the device feature trait
31
+ to this trait so that the dynamic attributes can be pre-determined.
32
+
33
+ The current dynamic attributes are:
34
+ - Fan Speed
35
+ - Water Mode
36
+ - Mop Route
37
+
38
+ You should call the _options() version of the attribute to know which are supported for your device
39
+ (i.e. fan_speed_options())
40
+ Then you can call the _mapping to convert an int value to the actual Enum. (i.e. fan_speed_mapping())
41
+ You can call the _name property to get the str value of the enum. (i.e. fan_speed_name)
42
+
43
+ """
44
+
45
+ command = RoborockCommand.GET_STATUS
46
+
47
+ def __init__(self, device_feature_trait: DeviceFeaturesTrait, region: str | None = None) -> None:
48
+ """Initialize the StatusTrait."""
49
+ super().__init__()
50
+ self._device_features_trait = device_feature_trait
51
+ self._region = region
52
+
53
+ @cached_property
54
+ def fan_speed_options(self) -> list[VacuumModes]:
55
+ return get_clean_modes(self._device_features_trait)
56
+
57
+ @cached_property
58
+ def fan_speed_mapping(self) -> dict[int, str]:
59
+ return {fan.code: fan.value for fan in self.fan_speed_options}
60
+
61
+ @cached_property
62
+ def water_mode_options(self) -> list[WaterModes]:
63
+ return get_water_modes(self._device_features_trait)
64
+
65
+ @cached_property
66
+ def water_mode_mapping(self) -> dict[int, str]:
67
+ return get_water_mode_mapping(self._device_features_trait)
68
+
69
+ @cached_property
70
+ def mop_route_options(self) -> list[CleanRoutes]:
71
+ return get_clean_routes(self._device_features_trait, self._region or "us")
72
+
73
+ @cached_property
74
+ def mop_route_mapping(self) -> dict[int, str]:
75
+ return {route.code: route.value for route in self.mop_route_options}
76
+
77
+ @property
78
+ def fan_speed_name(self) -> str | None:
79
+ if self.fan_power is None:
80
+ return None
81
+ return self.fan_speed_mapping.get(self.fan_power)
82
+
83
+ @property
84
+ def water_mode_name(self) -> str | None:
85
+ if self.water_box_mode is None:
86
+ return None
87
+ return self.water_mode_mapping.get(self.water_box_mode)
88
+
89
+ @property
90
+ def mop_route_name(self) -> str | None:
91
+ if self.mop_mode is None:
92
+ return None
93
+ return self.mop_route_mapping.get(self.mop_mode)
94
+
95
+ def _parse_response(self, response: common.V1ResponseData) -> Self:
96
+ """Parse the response from the device into a StatusV2-based status object."""
97
+ if isinstance(response, list):
98
+ response = response[0]
99
+ if isinstance(response, dict):
100
+ return StatusV2.from_dict(response)
101
+ raise ValueError(f"Unexpected status format: {response!r}")
@@ -0,0 +1,44 @@
1
+ from roborock_cli._vendor.roborock.data import ValleyElectricityTimer
2
+ from roborock_cli._vendor.roborock.devices.traits.v1 import common
3
+ from roborock_cli._vendor.roborock.roborock_typing import RoborockCommand
4
+
5
+ _ENABLED_PARAM = "enabled"
6
+
7
+
8
+ class ValleyElectricityTimerTrait(ValleyElectricityTimer, common.V1TraitMixin, common.RoborockSwitchBase):
9
+ """Trait for managing Valley Electricity Timer settings on Roborock devices."""
10
+
11
+ command = RoborockCommand.GET_VALLEY_ELECTRICITY_TIMER
12
+ requires_feature = "is_supported_valley_electricity"
13
+
14
+ @property
15
+ def is_on(self) -> bool:
16
+ """Return whether the Valley Electricity Timer is enabled."""
17
+ return self.enabled == 1
18
+
19
+ async def set_timer(self, timer: ValleyElectricityTimer) -> None:
20
+ """Set the Valley Electricity Timer settings of the device."""
21
+ await self.rpc_channel.send_command(RoborockCommand.SET_VALLEY_ELECTRICITY_TIMER, params=timer.as_list())
22
+ await self.refresh()
23
+
24
+ async def clear_timer(self) -> None:
25
+ """Clear the Valley Electricity Timer settings of the device."""
26
+ await self.rpc_channel.send_command(RoborockCommand.CLOSE_VALLEY_ELECTRICITY_TIMER)
27
+ await self.refresh()
28
+
29
+ async def enable(self) -> None:
30
+ """Enable the Valley Electricity Timer settings of the device."""
31
+ await self.rpc_channel.send_command(
32
+ RoborockCommand.SET_VALLEY_ELECTRICITY_TIMER,
33
+ params=self.as_list(),
34
+ )
35
+ # Optimistic update to avoid an extra refresh
36
+ self.enabled = 1
37
+
38
+ async def disable(self) -> None:
39
+ """Disable the Valley Electricity Timer settings of the device."""
40
+ await self.rpc_channel.send_command(
41
+ RoborockCommand.CLOSE_VALLEY_ELECTRICITY_TIMER,
42
+ )
43
+ # Optimistic update to avoid an extra refresh
44
+ self.enabled = 0
@@ -0,0 +1,27 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from roborock_cli._vendor.roborock.devices.traits.v1 import common
4
+ from roborock_cli._vendor.roborock.roborock_typing import RoborockCommand
5
+
6
+ # TODO: This is currently the pattern for holding all the commands that hold a
7
+ # single value, but it still seems too verbose. Maybe we can generate these
8
+ # dynamically or somehow make them less code.
9
+
10
+
11
+ @dataclass
12
+ class SoundVolume(common.RoborockValueBase):
13
+ """Dataclass for sound volume."""
14
+
15
+ volume: int | None = field(default=None, metadata={"roborock_value": True})
16
+ """Sound volume level (0-100)."""
17
+
18
+
19
+ class SoundVolumeTrait(SoundVolume, common.V1TraitMixin):
20
+ """Trait for controlling the sound volume of a Roborock device."""
21
+
22
+ command = RoborockCommand.GET_SOUND_VOLUME
23
+
24
+ async def set_volume(self, volume: int) -> None:
25
+ """Set the sound volume of the device."""
26
+ await self.rpc_channel.send_command(RoborockCommand.CHANGE_SOUND_VOLUME, params=[volume])
27
+ self.volume = volume
@@ -0,0 +1,13 @@
1
+ """Trait for wash towel mode."""
2
+
3
+ from roborock_cli._vendor.roborock.data import WashTowelMode
4
+ from roborock_cli._vendor.roborock.device_features import is_wash_n_fill_dock
5
+ from roborock_cli._vendor.roborock.devices.traits.v1 import common
6
+ from roborock_cli._vendor.roborock.roborock_typing import RoborockCommand
7
+
8
+
9
+ class WashTowelModeTrait(WashTowelMode, common.V1TraitMixin):
10
+ """Trait for wash towel mode."""
11
+
12
+ command = RoborockCommand.GET_WASH_TOWEL_MODE
13
+ requires_dock_type = is_wash_n_fill_dock
@@ -0,0 +1,8 @@
1
+ """Module for handling network connections to Roborock devices.
2
+
3
+ This is used internally by the device manager for creating connections to
4
+ Roborock devices. These modules contain common code, not specific to a
5
+ particular device or application level protocol.
6
+ """
7
+
8
+ __all__: list[str] = []
@@ -0,0 +1,32 @@
1
+ """Low-level interface for connections to Roborock devices."""
2
+
3
+ import logging
4
+ from collections.abc import Callable
5
+ from typing import Protocol
6
+
7
+ from roborock_cli._vendor.roborock.roborock_message import RoborockMessage
8
+
9
+ _LOGGER = logging.getLogger(__name__)
10
+
11
+
12
+ class Channel(Protocol):
13
+ """A generic channel for establishing a connection with a Roborock device.
14
+
15
+ Individual channel implementations have their own methods for speaking to
16
+ the device that hide some of the protocol specific complexity, but they
17
+ are still specialized for the device type and protocol.
18
+ """
19
+
20
+ @property
21
+ def is_connected(self) -> bool:
22
+ """Return true if the channel is connected."""
23
+ ...
24
+
25
+ @property
26
+ def is_local_connected(self) -> bool:
27
+ """Return true if the channel is connected locally."""
28
+ ...
29
+
30
+ async def subscribe(self, callback: Callable[[RoborockMessage], None]) -> Callable[[], None]:
31
+ """Subscribe to messages from the device."""
32
+ ...