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.
- roborock_cli/__init__.py +3 -0
- roborock_cli/__main__.py +76 -0
- roborock_cli/_vendor/VERSION +6 -0
- roborock_cli/_vendor/__init__.py +0 -0
- roborock_cli/_vendor/roborock/__init__.py +27 -0
- roborock_cli/_vendor/roborock/broadcast_protocol.py +114 -0
- roborock_cli/_vendor/roborock/callbacks.py +130 -0
- roborock_cli/_vendor/roborock/cli.py +1338 -0
- roborock_cli/_vendor/roborock/const.py +84 -0
- roborock_cli/_vendor/roborock/data/__init__.py +9 -0
- roborock_cli/_vendor/roborock/data/b01_q10/__init__.py +2 -0
- roborock_cli/_vendor/roborock/data/b01_q10/b01_q10_code_mappings.py +213 -0
- roborock_cli/_vendor/roborock/data/b01_q10/b01_q10_containers.py +102 -0
- roborock_cli/_vendor/roborock/data/b01_q7/__init__.py +2 -0
- roborock_cli/_vendor/roborock/data/b01_q7/b01_q7_code_mappings.py +303 -0
- roborock_cli/_vendor/roborock/data/b01_q7/b01_q7_containers.py +302 -0
- roborock_cli/_vendor/roborock/data/code_mappings.py +198 -0
- roborock_cli/_vendor/roborock/data/containers.py +530 -0
- roborock_cli/_vendor/roborock/data/dyad/__init__.py +2 -0
- roborock_cli/_vendor/roborock/data/dyad/dyad_code_mappings.py +102 -0
- roborock_cli/_vendor/roborock/data/dyad/dyad_containers.py +28 -0
- roborock_cli/_vendor/roborock/data/v1/__init__.py +3 -0
- roborock_cli/_vendor/roborock/data/v1/v1_clean_modes.py +192 -0
- roborock_cli/_vendor/roborock/data/v1/v1_code_mappings.py +644 -0
- roborock_cli/_vendor/roborock/data/v1/v1_containers.py +800 -0
- roborock_cli/_vendor/roborock/data/zeo/__init__.py +2 -0
- roborock_cli/_vendor/roborock/data/zeo/zeo_code_mappings.py +138 -0
- roborock_cli/_vendor/roborock/data/zeo/zeo_containers.py +0 -0
- roborock_cli/_vendor/roborock/device_features.py +668 -0
- roborock_cli/_vendor/roborock/devices/README.md +41 -0
- roborock_cli/_vendor/roborock/devices/__init__.py +11 -0
- roborock_cli/_vendor/roborock/devices/cache.py +143 -0
- roborock_cli/_vendor/roborock/devices/device.py +240 -0
- roborock_cli/_vendor/roborock/devices/device_manager.py +269 -0
- roborock_cli/_vendor/roborock/devices/file_cache.py +79 -0
- roborock_cli/_vendor/roborock/devices/rpc/__init__.py +14 -0
- roborock_cli/_vendor/roborock/devices/rpc/a01_channel.py +94 -0
- roborock_cli/_vendor/roborock/devices/rpc/b01_q10_channel.py +57 -0
- roborock_cli/_vendor/roborock/devices/rpc/b01_q7_channel.py +101 -0
- roborock_cli/_vendor/roborock/devices/rpc/v1_channel.py +457 -0
- roborock_cli/_vendor/roborock/devices/traits/__init__.py +28 -0
- roborock_cli/_vendor/roborock/devices/traits/a01/__init__.py +191 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/__init__.py +12 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q10/__init__.py +76 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q10/command.py +32 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q10/common.py +115 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q10/status.py +32 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q10/vacuum.py +81 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q7/__init__.py +136 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q7/clean_summary.py +75 -0
- roborock_cli/_vendor/roborock/devices/traits/traits_mixin.py +64 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/__init__.py +344 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/child_lock.py +29 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/clean_summary.py +83 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/command.py +38 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/common.py +172 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/consumeable.py +48 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/device_features.py +74 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/do_not_disturb.py +41 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/dust_collection_mode.py +13 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/flow_led_status.py +29 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/home.py +285 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/led_status.py +43 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/map_content.py +83 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/maps.py +80 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/network_info.py +55 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/rooms.py +105 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/routines.py +26 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/smart_wash_params.py +13 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/status.py +101 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/valley_electricity_timer.py +44 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/volume.py +27 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/wash_towel_mode.py +13 -0
- roborock_cli/_vendor/roborock/devices/transport/__init__.py +8 -0
- roborock_cli/_vendor/roborock/devices/transport/channel.py +32 -0
- roborock_cli/_vendor/roborock/devices/transport/local_channel.py +295 -0
- roborock_cli/_vendor/roborock/devices/transport/mqtt_channel.py +118 -0
- roborock_cli/_vendor/roborock/diagnostics.py +166 -0
- roborock_cli/_vendor/roborock/exceptions.py +95 -0
- roborock_cli/_vendor/roborock/map/__init__.py +7 -0
- roborock_cli/_vendor/roborock/map/map_parser.py +123 -0
- roborock_cli/_vendor/roborock/mqtt/__init__.py +10 -0
- roborock_cli/_vendor/roborock/mqtt/health_manager.py +60 -0
- roborock_cli/_vendor/roborock/mqtt/roborock_session.py +463 -0
- roborock_cli/_vendor/roborock/mqtt/session.py +108 -0
- roborock_cli/_vendor/roborock/protocol.py +558 -0
- roborock_cli/_vendor/roborock/protocols/__init__.py +3 -0
- roborock_cli/_vendor/roborock/protocols/a01_protocol.py +74 -0
- roborock_cli/_vendor/roborock/protocols/b01_q10_protocol.py +87 -0
- roborock_cli/_vendor/roborock/protocols/b01_q7_protocol.py +81 -0
- roborock_cli/_vendor/roborock/protocols/v1_protocol.py +271 -0
- roborock_cli/_vendor/roborock/py.typed +0 -0
- roborock_cli/_vendor/roborock/roborock_message.py +246 -0
- roborock_cli/_vendor/roborock/roborock_typing.py +382 -0
- roborock_cli/_vendor/roborock/util.py +54 -0
- roborock_cli/_vendor/roborock/web_api.py +761 -0
- roborock_cli/cli.py +715 -0
- roborock_cli/connection.py +202 -0
- roborock_cli/helpers.py +71 -0
- roborock_cli/server.py +759 -0
- roborock_cli/setup_auth.py +92 -0
- roborock_cli-0.1.1.dist-info/METADATA +172 -0
- roborock_cli-0.1.1.dist-info/RECORD +106 -0
- roborock_cli-0.1.1.dist-info/WHEEL +4 -0
- roborock_cli-0.1.1.dist-info/entry_points.txt +2 -0
- 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
|
+
...
|