Skip to content

Commit ea16311

Browse files
committed
feat(v1): expose clean-then-mop status from the device
The device reports seq_type in its get_status payload, but StatusV2 did not model it, so RoborockBase.from_dict discarded it along with every other unmatched key. The vendor app reads the same field off the same payload to derive its "clean then mop" state. Add seq_type to StatusV2 and a clean_then_mop property on StatusTrait that reports whether the current run vacuums each room fully before mopping it. It describes the run in progress rather than a persisted setting, and the device offers no setter, so it is read only here; the value travels outbound as a parameter of the cleaning command instead. Gated for reporting on is_clean_then_mop_mode_supported. Verified against a Roborock Saros 20 (roborock.vacuum.a288).
1 parent d54fc90 commit ea16311

4 files changed

Lines changed: 39 additions & 1 deletion

File tree

‎roborock/data/v1/v1_containers.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ class StatusV2(RoborockBase):
137137
repeat: int | None = None
138138
kct: int | None = None
139139
subdivision_sets: int | None = None
140+
seq_type: int | None = field(default=None, metadata={"feature": "is_clean_then_mop_mode_supported"})
141+
"""Cleaning sequence of the current run; 1 means vacuum the room, then mop it."""
140142

141143
@property
142144
def square_meter_clean_area(self) -> float | None:

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828

2929
_DPS_CONVERTER = DpsDataConverter.from_dataclass(StatusV2)
3030

31+
_SEQ_TYPE_CLEAN_THEN_MOP = 1
32+
3133

3234
class StatusTrait(StatusV2, common.V1TraitMixin, TraitUpdateListener):
3335
"""Trait for managing the status of Roborock devices.
@@ -111,6 +113,17 @@ def mop_route_name(self) -> str | None:
111113
return None
112114
return self.mop_route_mapping.get(self.mop_mode)
113115

116+
@property
117+
def clean_then_mop(self) -> bool | None:
118+
"""Whether the current run vacuums each room fully before mopping it.
119+
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+
"""
123+
if self.seq_type is None:
124+
return None
125+
return self.seq_type == _SEQ_TYPE_CLEAN_THEN_MOP
126+
114127
@property
115128
def current_cleaning_mode(self) -> CleaningMode | None:
116129
return get_current_cleaning_mode(

‎tests/devices/__snapshots__/test_v1_device.ambr‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,7 +876,7 @@
876876
})
877877
# ---
878878
# name: test_device_trait_command_parsing[status]
879-
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_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)
879+
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_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)
880880
# ---
881881
# name: test_device_trait_command_parsing[status].1
882882
dict({

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
RoborockDockErrorCode,
2121
RoborockErrorCode,
2222
RoborockStateCode,
23+
StatusV2,
2324
)
2425
from roborock.device_features import DeviceFeatures
2526
from roborock.devices.device import RoborockDevice
@@ -679,3 +680,25 @@ def test_update_listener_ignores_unrelated(status_trait: StatusTrait) -> None:
679680

680681
assert not event.is_set()
681682
unsubscribe()
683+
684+
685+
@pytest.mark.parametrize(
686+
("seq_type", "expected"),
687+
[(1, True), (0, False), (None, None)],
688+
)
689+
def test_clean_then_mop(status_trait: StatusTrait, seq_type: int | None, expected: bool | None) -> None:
690+
"""seq_type reports whether the current run vacuums before mopping."""
691+
status_trait.seq_type = seq_type
692+
693+
assert status_trait.clean_then_mop is expected
694+
695+
696+
def test_status_parses_seq_type() -> None:
697+
"""seq_type is read from the device status payload.
698+
699+
The device reports it in get_status; before it was modelled here it was
700+
silently dropped along with every other unmatched key.
701+
"""
702+
status = StatusV2.from_dict({**STATUS, "seq_type": 1})
703+
704+
assert status.seq_type == 1

0 commit comments

Comments
 (0)