Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 43 additions & 2 deletions roborock/data/v1/v1_clean_modes.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,21 @@ class CleaningMode(StrEnum):

VACUUM = "vacuum"
VAC_AND_MOP = "vac_and_mop"
VAC_THEN_MOP = "vac_then_mop"
"""Vacuum first with the mop pads detached, then return for the pads and mop.

Uses the same motor settings as `VAC_AND_MOP`; the order is a separate
device setting (`seq_type`) that the app writes alongside them.
"""
MOP = "mop"
CUSTOM = "custom"
SMART_MODE = "smart_mode"


# `seq_type` value for "vacuum first, then mop"; 0 means both in one pass.
SEQ_TYPE_CLEAN_THEN_MOP = 1


_WATER_SLIDE_MODES: tuple[WaterModes, ...] = (
WaterModes.OFF,
WaterModes.PURE_WATER_FLOW_START,
Expand Down Expand Up @@ -241,6 +251,8 @@ def get_cleaning_mode_options(features: DeviceFeatures) -> list[CleaningMode]:

supported_water_modes = get_water_modes(features)
options = [CleaningMode.VACUUM, CleaningMode.VAC_AND_MOP]
if features.is_clean_then_mop_mode_supported:
options.append(CleaningMode.VAC_THEN_MOP)
if features.is_pure_clean_mop_supported:
options.append(CleaningMode.MOP)
if features.is_customized_clean_supported and WaterModes.CUSTOMIZED in supported_water_modes:
Expand Down Expand Up @@ -275,7 +287,7 @@ def _get_clean_motor_mode_params(
"""Return (fan_power, water_box_mode, mop_mode) enums for the high-level mode."""
if mode == CleaningMode.VACUUM:
return (VacuumModes.BALANCED, WaterModes.OFF, CleanRoutes.STANDARD)
if mode == CleaningMode.VAC_AND_MOP:
if mode in (CleaningMode.VAC_AND_MOP, CleaningMode.VAC_THEN_MOP):
return (VacuumModes.BALANCED, _get_default_mopping_water_mode(features), CleanRoutes.STANDARD)
if mode == CleaningMode.MOP:
return (
Expand Down Expand Up @@ -312,6 +324,27 @@ def get_cleaning_mode_parameters(cleaning_mode: CleaningMode, features: DeviceFe
return [params]


def get_clean_sequence_parameters(
cleaning_mode: CleaningMode,
motor_params: dict[str, int],
features: DeviceFeatures,
) -> dict[str, int]:
"""Get the `app_set_clean_sequence_type` payload for a high-level cleaning mode.

The firmware requires the motor settings (`fan_power`, `water_box_mode` and
`mop_mode`) in the request, so `motor_params` should be the values just sent
with `set_clean_motor_mode`. The app adds `repeat: 1` when enabling the
sequence on devices with `is_ctm_with_repeat_supported`.
"""
if not features.is_clean_then_mop_mode_supported:
raise RoborockUnsupportedFeature("Clean then mop is not supported")
enabled = cleaning_mode == CleaningMode.VAC_THEN_MOP
params = {"type": SEQ_TYPE_CLEAN_THEN_MOP if enabled else 0, **motor_params}
if enabled and features.is_ctm_with_repeat_supported:
params["repeat"] = 1
return params


def _resolve_mode_code(value: int | ModeEnumT | None, mode_cls: type[ModeEnumT]) -> ModeEnumT | None:
"""Resolve a raw code or enum into a RoborockModeEnum."""
if value is None:
Expand Down Expand Up @@ -346,8 +379,14 @@ def get_current_cleaning_mode(
water_mode: int | WaterModes | None,
mop_mode: int | CleanRoutes | None,
features: DeviceFeatures,
seq_type: int | None = None,
) -> CleaningMode | None:
"""Classify the current high-level cleaning mode from individual mode codes."""
"""Classify the current high-level cleaning mode from individual mode codes.

`seq_type` is the device's cleaning order (`get_status`); when it is
`SEQ_TYPE_CLEAN_THEN_MOP` on a device that supports it, vacuum-and-mop
settings are reported as `CleaningMode.VAC_THEN_MOP`.
"""
if not features.is_support_water_mode:
return None
clean_mode_enum = _resolve_clean_mode(clean_mode, features)
Expand All @@ -368,6 +407,8 @@ def get_current_cleaning_mode(
pass
if water_mode_enum == WaterModes.OFF:
return CleaningMode.VACUUM
if features.is_clean_then_mop_mode_supported and seq_type == SEQ_TYPE_CLEAN_THEN_MOP:
return CleaningMode.VAC_THEN_MOP
return CleaningMode.VAC_AND_MOP


Expand Down
7 changes: 3 additions & 4 deletions roborock/data/v1/v1_containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from roborock.roborock_message import RoborockDataProtocol

from ..containers import NamedRoomMapping, RoborockBase, RoborockBaseTimer, _attr_repr, field_metadata
from .v1_clean_modes import WashTowelModes
from .v1_clean_modes import SEQ_TYPE_CLEAN_THEN_MOP, WashTowelModes
from .v1_code_mappings import (
CleanFluidStatus,
ClearWaterBoxStatus,
Expand Down Expand Up @@ -171,6 +171,8 @@ class StatusV2(RoborockBase):
repeat: int | None = None
kct: int | None = None
subdivision_sets: int | None = None
seq_type: int | None = field(default=None, metadata={"feature": "is_clean_then_mop_mode_supported"})
"""Cleaning sequence of the current run; 1 means vacuum the room, then mop it."""

@property
def square_meter_clean_area(self) -> float | None:
Expand Down Expand Up @@ -515,9 +517,6 @@ class WashTowelMode(RoborockBase):
wash_mode: WashTowelModes | None = None


SEQ_TYPE_CLEAN_THEN_MOP = 1


@dataclass
class SegmentCleanMode(RoborockBase):
"""A single room's clean mode override, as reported by the device.
Expand Down
45 changes: 40 additions & 5 deletions roborock/devices/traits/v1/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Any

from roborock import (
SEQ_TYPE_CLEAN_THEN_MOP,
CleaningMode,
CleanRoutes,
RoborockDockState,
Expand All @@ -11,6 +12,7 @@
WaterModes,
get_clean_modes,
get_clean_routes,
get_clean_sequence_parameters,
get_cleaning_mode_options,
get_cleaning_mode_parameters,
get_current_cleaning_mode,
Expand Down Expand Up @@ -49,6 +51,7 @@ class StatusTrait(StatusV2, common.V1TraitMixin, TraitUpdateListener):
- Fan Speed
- Water Mode
- Mop Route
- Clean then mop (``clean_then_mop``, reported as ``CleaningMode.VAC_THEN_MOP``)

You should use the _options version of the attribute to know which are
supported for your device (i.e. fan_speed_options)
Expand Down Expand Up @@ -131,13 +134,33 @@ def mop_route_name(self) -> str | None:
return None
return self.mop_route_mapping.get(self.mop_mode)

@property
def clean_then_mop(self) -> bool | None:
"""Whether each room is vacuumed fully before it is mopped.

Reported by the device as ``seq_type`` and persisted on the device: it
tracks the "vacuum then mop" choice in the vendor app, also while docked.

The device stores it separately from ``fan_power``, ``water_box_mode``
and ``mop_mode``, but the app presents it as a fourth cleaning mode, so
``current_cleaning_mode`` reports ``CleaningMode.VAC_THEN_MOP`` when it
is set with vacuum-and-mop settings, and ``set_cleaning_mode`` writes it.

``None`` means the device does not support the feature or has not
reported it.
"""
if not self._device_features_trait.is_clean_then_mop_mode_supported or self.seq_type is None:
return None
return self.seq_type == SEQ_TYPE_CLEAN_THEN_MOP

@property
def current_cleaning_mode(self) -> CleaningMode | None:
return get_current_cleaning_mode(
clean_mode=self.fan_power,
water_mode=self.water_box_mode,
mop_mode=self.mop_mode,
features=self._device_features_trait,
seq_type=self.seq_type,
)

@property
Expand All @@ -147,11 +170,23 @@ def current_cleaning_mode_name(self) -> str | None:
return cleaning_mode.value

async def set_cleaning_mode(self, cleaning_mode: str | CleaningMode) -> None:
"""Set the preferred high-level cleaning mode for the device."""
await self.rpc_channel.send_command(
RoborockCommand.SET_CLEAN_MOTOR_MODE,
params=get_cleaning_mode_parameters(resolve_cleaning_mode(cleaning_mode), self._device_features_trait),
)
"""Set the preferred high-level cleaning mode for the device.

Like the app, this also writes the cleaning order on devices that support
clean then mop: it is enabled for ``CleaningMode.VAC_THEN_MOP`` and
cleared when switching to any other mode while it is enabled.
"""
mode = resolve_cleaning_mode(cleaning_mode)
motor_params = get_cleaning_mode_parameters(mode, self._device_features_trait)
await self.rpc_channel.send_command(RoborockCommand.SET_CLEAN_MOTOR_MODE, params=motor_params)
if not self._device_features_trait.is_clean_then_mop_mode_supported:
return
if mode != CleaningMode.VAC_THEN_MOP and not self.clean_then_mop:
return
sequence_params = get_clean_sequence_parameters(mode, motor_params[0], self._device_features_trait)
await self.rpc_channel.send_command(RoborockCommand.APP_SET_CLEAN_SEQUENCE_TYPE, params=sequence_params)
self.seq_type = sequence_params["type"]
self._notify_update()

async def resolve_error(self, error_code: int | None = None) -> None:
"""Resolve an error, like tapping "Resolved" in the Roborock app.
Expand Down
1 change: 1 addition & 0 deletions roborock/roborock_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class RoborockCommand(str, Enum):
APP_SET_CROSS_CARPET_CLEANING_STATUS = "app_set_cross_carpet_cleaning_status"
APP_SET_DOOR_SILL_BLOCKS = "app_set_door_sill_blocks"
APP_SET_DIRTY_REPLENISH_CLEAN_STATUS = "app_set_dirty_replenish_clean_status"
APP_SET_CLEAN_SEQUENCE_TYPE = "app_set_clean_sequence_type"
APP_SET_DRYER_SETTING = "app_set_dryer_setting"
APP_SET_DRYER_STATUS = "app_set_dryer_status"
APP_SET_DYNAMIC_CONFIG = "app_set_dynamic_config"
Expand Down
2 changes: 1 addition & 1 deletion tests/devices/__snapshots__/test_v1_device.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -876,7 +876,7 @@
})
# ---
# name: test_device_trait_command_parsing[status]
StatusTrait(adbumper_status=None, auto_dust_collection=None, avoid_count=None, back_type=None, battery=100, camera_status=None, charge_status=None, clean_area=91287500, clean_fluid_status=None, clean_percent=None, clean_time=5405, cleaning_mode_options=[<CleaningMode.VACUUM: 'vacuum'>, <CleaningMode.VAC_AND_MOP: 'vac_and_mop'>, <CleaningMode.MOP: 'mop'>, <CleaningMode.CUSTOM: 'custom'>], clear_water_box_status=None, collision_avoid_status=None, command=<RoborockCommand.GET_STATUS: 'get_status'>, common_status=None, converter=DefaultConverter, corner_clean_mode=None, current_cleaning_mode=<CleaningMode.CUSTOM: 'custom'>, current_cleaning_mode_name='custom', current_map=0, debug_mode=None, dirty_water_box_status=None, distance_off=0, dnd_enabled=1, dock_cool_fan_status=None, dock_error_status=None, dock_state=<RoborockDockState.full: 'full'>, dock_type=None, dry_status=None, dss=None, dust_bag_status=None, dust_collection_status=None, error_code=<RoborockErrorCode.none: 0>, error_code_name='none', fan_power=106, fan_speed_mapping={101: 'quiet', 102: 'balanced', 103: 'turbo', 104: 'max', 108: 'max_plus', 105: 'off', 106: 'custom'}, fan_speed_name='custom', fan_speed_options=[<VacuumModes.QUIET: 'quiet'>, <VacuumModes.BALANCED: 'balanced'>, <VacuumModes.TURBO: 'turbo'>, <VacuumModes.MAX: 'max'>, <VacuumModes.MAX_PLUS: 'max_plus'>, <VacuumModes.OFF: 'off'>, <VacuumModes.CUSTOMIZED: 'custom'>], has_am=None, hatch_door_status=None, home_sec_enable_password=None, home_sec_status=None, in_cleaning=<RoborockInCleaning.complete: 0>, in_fresh_state=1, in_returning=0, in_warmup=None, is_battery_charging=False, is_exploring=None, is_locating=0, kct=None, lab_status=1, last_clean_t=None, lock_status=0, map_present=1, map_status=3, mop_forbidden_enable=0, mop_mode=None, mop_route_mapping={300: 'standard', 301: 'deep', 302: 'custom'}, mop_route_name=None, mop_route_options=[<CleanRoutes.STANDARD: 'standard'>, <CleanRoutes.DEEP: 'deep'>, <CleanRoutes.CUSTOMIZED: 'custom'>], msg_seq=515, msg_ver=2, rdt=None, repeat=None, replenish_mode=None, rss=None, square_meter_clean_area=91.3, state=<RoborockStateCode.charging: 8>, state_name='charging', subdivision_sets=None, switch_map_mode=None, unsave_map_flag=0, unsave_map_reason=4, wash_phase=None, wash_ready=None, wash_status=None, water_box_carriage_status=0, water_box_filter_status=None, water_box_mode=204, water_box_status=0, water_mode_mapping={200: 'off', 201: 'mild', 202: 'standard', 203: 'intense', 207: 'custom_water_flow', 204: 'custom'}, water_mode_name='custom', water_mode_options=[<WaterModes.OFF: 'off'>, <WaterModes.MILD: 'mild'>, <WaterModes.STANDARD: 'standard'>, <WaterModes.INTENSE: 'intense'>, <WaterModes.CUSTOM: 'custom_water_flow'>, <WaterModes.CUSTOMIZED: 'custom'>], water_shortage_status=None)
StatusTrait(adbumper_status=None, auto_dust_collection=None, avoid_count=None, back_type=None, battery=100, camera_status=None, charge_status=None, clean_area=91287500, clean_fluid_status=None, clean_percent=None, clean_then_mop=None, clean_time=5405, cleaning_mode_options=[<CleaningMode.VACUUM: 'vacuum'>, <CleaningMode.VAC_AND_MOP: 'vac_and_mop'>, <CleaningMode.MOP: 'mop'>, <CleaningMode.CUSTOM: 'custom'>], clear_water_box_status=None, collision_avoid_status=None, command=<RoborockCommand.GET_STATUS: 'get_status'>, common_status=None, converter=DefaultConverter, corner_clean_mode=None, current_cleaning_mode=<CleaningMode.CUSTOM: 'custom'>, current_cleaning_mode_name='custom', current_map=0, debug_mode=None, dirty_water_box_status=None, distance_off=0, dnd_enabled=1, dock_cool_fan_status=None, dock_error_status=None, dock_state=<RoborockDockState.full: 'full'>, dock_type=None, dry_status=None, dss=None, dust_bag_status=None, dust_collection_status=None, error_code=<RoborockErrorCode.none: 0>, error_code_name='none', fan_power=106, fan_speed_mapping={101: 'quiet', 102: 'balanced', 103: 'turbo', 104: 'max', 108: 'max_plus', 105: 'off', 106: 'custom'}, fan_speed_name='custom', fan_speed_options=[<VacuumModes.QUIET: 'quiet'>, <VacuumModes.BALANCED: 'balanced'>, <VacuumModes.TURBO: 'turbo'>, <VacuumModes.MAX: 'max'>, <VacuumModes.MAX_PLUS: 'max_plus'>, <VacuumModes.OFF: 'off'>, <VacuumModes.CUSTOMIZED: 'custom'>], has_am=None, hatch_door_status=None, home_sec_enable_password=None, home_sec_status=None, in_cleaning=<RoborockInCleaning.complete: 0>, in_fresh_state=1, in_returning=0, in_warmup=None, is_battery_charging=False, is_exploring=None, is_locating=0, kct=None, lab_status=1, last_clean_t=None, lock_status=0, map_present=1, map_status=3, mop_forbidden_enable=0, mop_mode=None, mop_route_mapping={300: 'standard', 301: 'deep', 302: 'custom'}, mop_route_name=None, mop_route_options=[<CleanRoutes.STANDARD: 'standard'>, <CleanRoutes.DEEP: 'deep'>, <CleanRoutes.CUSTOMIZED: 'custom'>], msg_seq=515, msg_ver=2, rdt=None, repeat=None, replenish_mode=None, rss=None, seq_type=None, square_meter_clean_area=91.3, state=<RoborockStateCode.charging: 8>, state_name='charging', subdivision_sets=None, switch_map_mode=None, unsave_map_flag=0, unsave_map_reason=4, wash_phase=None, wash_ready=None, wash_status=None, water_box_carriage_status=0, water_box_filter_status=None, water_box_mode=204, water_box_status=0, water_mode_mapping={200: 'off', 201: 'mild', 202: 'standard', 203: 'intense', 207: 'custom_water_flow', 204: 'custom'}, water_mode_name='custom', water_mode_options=[<WaterModes.OFF: 'off'>, <WaterModes.MILD: 'mild'>, <WaterModes.STANDARD: 'standard'>, <WaterModes.INTENSE: 'intense'>, <WaterModes.CUSTOM: 'custom_water_flow'>, <WaterModes.CUSTOMIZED: 'custom'>], water_shortage_status=None)
# ---
# name: test_device_trait_command_parsing[status].1
dict({
Expand Down
Loading