Skip to content

Commit ff36835

Browse files
committed
feat(v1): add set_clean_then_mop and document how it relates to CleaningMode
1 parent ea16311 commit ff36835

3 files changed

Lines changed: 79 additions & 3 deletions

File tree

‎roborock/devices/traits/v1/status.py‎

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
resolve_cleaning_mode,
1919
)
2020
from roborock.devices.traits.common import DpsDataConverter, TraitUpdateListener
21+
from roborock.exceptions import RoborockUnsupportedFeature
2122
from roborock.roborock_message import RoborockDataProtocol
2223
from roborock.roborock_typing import RoborockCommand
2324

@@ -48,6 +49,7 @@ class StatusTrait(StatusV2, common.V1TraitMixin, TraitUpdateListener):
4849
- Fan Speed
4950
- Water Mode
5051
- Mop Route
52+
- Clean then mop (``clean_then_mop``, independent of the cleaning mode)
5153
5254
You should use the _options version of the attribute to know which are
5355
supported for your device (i.e. fan_speed_options)
@@ -115,10 +117,18 @@ def mop_route_name(self) -> str | None:
115117

116118
@property
117119
def clean_then_mop(self) -> bool | None:
118-
"""Whether the current run vacuums each room fully before mopping it.
120+
"""Whether each room is vacuumed fully before it is mopped.
119121
120-
Reported by the device as ``seq_type``. It describes the run in
121-
progress; it is not a persisted setting and cannot be set directly.
122+
Reported by the device as ``seq_type`` and persisted on the device: it
123+
tracks the "vacuum then mop" toggle in the vendor app, also while docked.
124+
125+
This is a separate axis from ``current_cleaning_mode``. The cleaning
126+
mode is derived from ``fan_power``, ``water_box_mode`` and ``mop_mode``
127+
and describes *what* the robot does in a room (vacuum, mop, both). This
128+
flag describes the *order* when it does both. Changing one leaves the
129+
other untouched; set it with ``set_clean_then_mop``.
130+
131+
``None`` means the device does not support the feature.
122132
"""
123133
if self.seq_type is None:
124134
return None
@@ -163,6 +173,27 @@ async def resolve_error(self, error_code: int | None = None) -> None:
163173
await self.rpc_channel.send_command(RoborockCommand.RESOLVE_ERROR, params={"error_code": error_code})
164174
await self.refresh()
165175

176+
async def set_clean_then_mop(self, enabled: bool) -> None:
177+
"""Set whether each room is vacuumed fully before it is mopped.
178+
179+
Independent of ``set_cleaning_mode``. The firmware requires the current
180+
motor-mode values in this request, so they are echoed back unchanged.
181+
"""
182+
if not self._device_features_trait.is_clean_then_mop_mode_supported:
183+
raise RoborockUnsupportedFeature("Clean then mop is not supported")
184+
seq_type = _SEQ_TYPE_CLEAN_THEN_MOP if enabled else 0
185+
params: dict[str, int | None] = {
186+
"type": seq_type,
187+
"fan_power": self.fan_power,
188+
"water_box_mode": self.water_box_mode,
189+
}
190+
if self.mop_mode is not None:
191+
params["mop_mode"] = self.mop_mode
192+
if enabled and self._device_features_trait.is_ctm_with_repeat_supported:
193+
params["repeat"] = 1
194+
await self.rpc_channel.send_command(RoborockCommand.APP_SET_CLEAN_SEQUENCE_TYPE, params=params)
195+
self.seq_type = seq_type
196+
166197
def update_from_dps(self, decoded_dps: dict[RoborockDataProtocol, Any]) -> None:
167198
"""Update the trait from data protocol push message data.
168199

‎roborock/roborock_typing.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class RoborockCommand(str, Enum):
3434
APP_SET_CROSS_CARPET_CLEANING_STATUS = "app_set_cross_carpet_cleaning_status"
3535
APP_SET_DOOR_SILL_BLOCKS = "app_set_door_sill_blocks"
3636
APP_SET_DIRTY_REPLENISH_CLEAN_STATUS = "app_set_dirty_replenish_clean_status"
37+
APP_SET_CLEAN_SEQUENCE_TYPE = "app_set_clean_sequence_type"
3738
APP_SET_DRYER_SETTING = "app_set_dryer_setting"
3839
APP_SET_DRYER_STATUS = "app_set_dryer_status"
3940
APP_SET_DYNAMIC_CONFIG = "app_set_dynamic_config"

‎tests/devices/traits/v1/test_status.py‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,3 +702,47 @@ def test_status_parses_seq_type() -> None:
702702
status = StatusV2.from_dict({**STATUS, "seq_type": 1})
703703

704704
assert status.seq_type == 1
705+
706+
707+
@pytest.mark.parametrize(
708+
("enabled", "ctm_with_repeat", "expected_params"),
709+
[
710+
(True, True, {"type": 1, "fan_power": 102, "water_box_mode": 202, "mop_mode": 300, "repeat": 1}),
711+
(True, False, {"type": 1, "fan_power": 102, "water_box_mode": 202, "mop_mode": 300}),
712+
(False, True, {"type": 0, "fan_power": 102, "water_box_mode": 202, "mop_mode": 300}),
713+
],
714+
)
715+
async def test_set_clean_then_mop(
716+
mock_rpc_channel: AsyncMock,
717+
enabled: bool,
718+
ctm_with_repeat: bool,
719+
expected_params: dict[str, int],
720+
) -> None:
721+
"""The setter echoes the current motor modes and updates seq_type optimistically."""
722+
status_trait = _create_cleaning_mode_status_trait(
723+
is_clean_then_mop_mode_supported=True,
724+
is_ctm_with_repeat_supported=ctm_with_repeat,
725+
)
726+
status_trait._rpc_channel = mock_rpc_channel # type: ignore[assignment]
727+
status_trait.fan_power = 102
728+
status_trait.water_box_mode = 202
729+
status_trait.mop_mode = 300
730+
status_trait.seq_type = 0 if enabled else 1
731+
732+
await status_trait.set_clean_then_mop(enabled)
733+
734+
mock_rpc_channel.send_command.assert_called_once_with(
735+
RoborockCommand.APP_SET_CLEAN_SEQUENCE_TYPE, params=expected_params
736+
)
737+
assert status_trait.clean_then_mop is enabled
738+
739+
740+
async def test_set_clean_then_mop_unsupported(mock_rpc_channel: AsyncMock) -> None:
741+
"""Devices without the feature reject the setter before sending anything."""
742+
status_trait = _create_cleaning_mode_status_trait(is_clean_then_mop_mode_supported=False)
743+
status_trait._rpc_channel = mock_rpc_channel # type: ignore[assignment]
744+
745+
with pytest.raises(RoborockUnsupportedFeature):
746+
await status_trait.set_clean_then_mop(True)
747+
748+
mock_rpc_channel.send_command.assert_not_called()

0 commit comments

Comments
 (0)