Skip to content

feat(v1): expose clean-then-mop status from the device - #959

Open
klowdo wants to merge 3 commits into
Python-roborock:mainfrom
klowdo:feat/status-seq-type
Open

klowdo wants to merge 3 commits into
Python-roborock:mainfrom
klowdo:feat/status-seq-type

Conversation

@klowdo

@klowdo klowdo commented Sep 15, 2026 •

Copy link
Copy Markdown

Summary

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.

This adds seq_type to StatusV2, a clean_then_mop property on StatusTrait and a set_clean_then_mop setter, reporting whether the current configuration vacuums each room fully before mopping it ("clean then mop" / "vacuum then mop").

Why it is get_status

The vendor Android app derives its own clean-then-mop state from the same field on the same payload. In the cloud-downloaded device plugin for roborock.vacuum.a288, parseCleanModeStatus does:

r6 = r1.seq_type;
r6 = 1 == r6;
r2['cleanThenMop'] = r6;

and its caller passes the status object — the same one it parses charge_status, wash_ready and kct from.

Verification

Tested against a Roborock Saros 20 (roborock.vacuum.a288). Raw get_status excerpt:

{"state": 8, "fan_power": 102, "water_box_mode": 235, "mop_mode": 300,
 "repeat": 1, "kct": 0, "subdivision_sets": 0, "seq_type": 0, ...}

Toggling "vacuum then mop" in the Roborock app, with the robot docked and idle (state: 8, in_cleaning: 0), flips the field and the new property follows it:

app setting seq_type clean_then_mop
vacuum and mop 0 False
vacuum then mop 1 True

So this is a persisted device setting that tracks the app, not merely a per-run artifact.

uv run pytest (1037 passed) and uv run pre-commit run --all-files both pass. One syrupy snapshot updated for the new field.

Notes and open questions

  • Setter. set_clean_then_mop(enabled) sends app_set_clean_sequence_type (as pointed out by Lash-L, and matching the app plugin's setCleanThenMopType). The firmware requires the current fan_power / water_box_mode / mop_mode in the request, so the trait echoes its own values back; repeat: 1 is added when enabling on devices with is_ctm_with_repeat_supported. mop_type / mop_power are omitted because the library does not model those fields or the isSupportChangeMop / isSupportVibrateMop feature bits. Verified on the Saros 20 (idle, undocked):

    step seq_type
    before 0
    set_clean_then_mop(True) 1
    set_clean_motor_mode with the same motor values 1 (unchanged)
    set_clean_then_mop(False) 0
  • Relationship to CleaningMode. Documented on the property. CleaningMode is derived from fan_power / water_box_mode / mop_mode and says what the robot does in a room; clean_then_mop is the order when it does both. Writing either one leaves the other unchanged (table above, and toggling in the app earlier). A consumer exposes them as two independent controls, e.g. a select plus a switch.

  • There is also a per-room layer. get_customize_clean_mode returns seq_type and repeat per segment, e.g. [{"segment": 4, "fan_power": 102, "water_box_mode": 235, "mop_mode": 300, "repeat": 1, "seq_type": 0}]. This PR models only the global value from get_status; the per-room layer is left alone.

  • Single device. Verified on one Saros 20 only. The field is gated for reporting on is_clean_then_mop_mode_supported, so devices without the feature are unaffected, but I cannot confirm behaviour on other models.

  • get_status carries several other keys this library still drops (distance_off, cleaning_info, extra_time, monitor_status, exit_dock, pet_reminding, sub_error_code, sub_zone). Out of scope here, but happy to follow up if wanted.

Motivation

Downstream this lets Home Assistant expose clean-then-mop as real device state rather than guessing. Deliberately not folded into CleaningMode: the axes are independent — toggling clean-then-mop on the test device left current_cleaning_mode at vac_and_mop — and with no setter available a CleaningMode member would be readable but not selectable.

@Lash-L

Lash-L commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Hey! Thanks for this.

I think the setter can be used via app_set_clean_sequence_type

{
  "method": "app_set_clean_sequence_type",
  "params": {
    "type": 1,
    "fan_power": 102,
    "water_box_mode": 235,
    "mop_mode": 300
  }
}

type: 1 enables clean then mop.
type: 0 disables clean then mop.

mop_type Included when its value is greater than zero and isSupportChangeMop() is true.

mop_power Included when its value is nonnegative and isSupportVibrateMop() is true

repeat Set to 1 when type == 1 and isCtmWithRepeatSupported() is true.

@allenporter allenporter left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see in the comments this is totally separate from CleaningMode -- but just reading the code it's not clear how these work together and how a consumer like home assistant should know the difference here.

Perhaps we can incorporate the setter info discussed by Lash-L and also clarify how these things work together or don't and why its fine.

@klowdo
klowdo force-pushed the feat/status-seq-type branch from 8d8c9b0 to ff36835 Compare October 6, 2026 08:40
@klowdo

klowdo commented Oct 6, 2026

Copy link
Copy Markdown
Author

@Lash-L confirmed against the a288 plugin bundle (setCleanThenMopType): {type, fan_power, water_box_mode, mop_mode}, mop_type only when >0 && isSupportChangeMop, mop_power only when >=0 && isSupportVibrateMop, repeat: 1 only when type == 1 && isCtmWithRepeatSupported. Added as StatusTrait.set_clean_then_mop(enabled) in ff36835. mop_type / mop_power left out since the library models neither the fields nor those two feature bits.

Verified on the Saros 20: seq_type goes 0 → 1 → 0 through the setter, and a set_clean_motor_mode in between leaves it at 1.

@allenporter the clean_then_mop docstring now spells out the relationship: CleaningMode is derived from fan_power / water_box_mode / mop_mode and never reads seq_type, so they are two independent axes (what vs. order). Writing one does not change the other on the device. The old docstring also wrongly said the value could not be set; fixed. PR description updated with the on-device table.

@Lash-L Lash-L left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! Two small comments

if enabled and self._device_features_trait.is_ctm_with_repeat_supported:
params["repeat"] = 1
await self.rpc_channel.send_command(RoborockCommand.APP_SET_CLEAN_SEQUENCE_TYPE, params=params)
self.seq_type = seq_type

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you see if we need self._notify_update() here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Home Assistant's coordinator subscribes to the status trait's update listener, so without it the optimistic value only shows up after the next poll. Added in 15c0db8, with the test asserting a listener fires.

Comment on lines +187 to +188
"fan_power": self.fan_power,
"water_box_mode": self.water_box_mode,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need to guard against fan power or water box mode being none here

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 15c0db8: raises RoborockException("Status is not loaded, call refresh() first") when either is None, before anything is sent. params is now dict[str, int]. Test added.

@klowdo

klowdo commented Oct 12, 2026

Copy link
Copy Markdown
Author

@allenporter follow-up on "how these work together", from the a288 plugin bundle:

  • The app's mode picker has major tabs (smart / general / custom) and, under "general", minor tabs in this order: clean then mop, clean and mop, mop only, clean only (tabs() builds the list; isCleanThenMopTab is "general tab, minor index 0", gated on feature bit 93 CleanThenMopMode).
  • So in the app, clean-then-mop is presented as a fourth cleaning mode next to the three CleaningMode already models, not as a separate toggle.
  • On confirm, the app compares the selected tab with the stored RSM.cleanThenMop and, if they differ, sends app_set_clean_sequence_type with type 1 or 0 alongside the motor-mode write. Picking "mop only" or "clean only" therefore sends type: 0.
  • On the wire the two are still independent: set_clean_motor_mode does not touch seq_type (verified on the Saros 20), which is why the library keeps them as two properties.

I put that in the clean_then_mop docstring (15c0db8): a consumer that wants app parity turns it off when it selects a mode other than vacuum-and-mop; the library does not do it implicitly.

Open question for you both: would you rather set_cleaning_mode clear seq_type itself when the target mode is not vacuum-and-mop, so the library mirrors the app? I left it out to keep the setter a pure motor-mode write, but it is a two-line change if you prefer that.

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).
@klowdo
klowdo force-pushed the feat/status-seq-type branch from 15c0db8 to d634f41 Compare October 13, 2026 08:46

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants