Skip to content

feat: Q10 (B01/ss07) room/segment cleaning (clean_segments)#851

Draft
tubededentifrice wants to merge 12 commits into
Python-roborock:mainfrom
tubededentifrice:q10-room-clean
Draft

feat: Q10 (B01/ss07) room/segment cleaning (clean_segments)#851
tubededentifrice wants to merge 12 commits into
Python-roborock:mainfrom
tubededentifrice:q10-room-clean

Conversation

@tubededentifrice

Copy link
Copy Markdown

Summary

Adds Q10 (roborock.vacuum.ss07, B01 protocol) room / segment cleaning — sending the robot to clean only a chosen set of rooms.

Room retrieval already exists (the map parser in #847 exposes map.rooms, each with an id and name). This PR adds the missing command side: VacuumTrait.clean_segments(segment_ids).

This is stacked on #846 (which introduces the Q10 VacuumTrait). Until #846 merges, GitHub will show those commits here too; the incremental work is the single feat: Q10 (B01/ss07) room/segment cleaning commit.

Protocol notes

Unlike whole-home ({"dps": {"201": 1}}) and spot ({"dps": {"201": 5}}), which take a bare task code, segment cleaning sends dpStartClean (201) as an object:

{"dps": {"201": {"cmd": 2, "clean_paramters": [<room_id>, ...]}}}
  • cmd 2 is the segment-clean task code (1 = whole-home, 2 = segment, 3 = zone, 4 = build map, 5 = spot).
  • clean_paramters (sic) is the list of room ids — this intentionally mirrors the device's misspelling of "parameters"; the firmware only accepts that exact key.
  • The room ids are the same ids the map reports (map.rooms[i].id), so callers can go straight from rooms to clean_segments.

The payload was captured from the official app (the generic vacuum plugin only implements whole-home; room cleaning is built in the app's native map flow, so the shape isn't visible in the plugin source) and then verified live against ss07 hardware: it drives the device into clean_task_type -> 2 (electoral) and cleans exactly the selected room.

Changes

  • Add VacuumTrait.clean_segments(segment_ids: list[int]).
  • Update the start_clean docstring now that the segment payload is known.
  • Add a q10-clean-segments CLI command (--segments 9,2).
  • Add trait payload tests for clean_segments.

Testing

  • uv run pytest tests/devices/traits/b01/q10/ tests/protocols/test_b01_q10_protocol.py — 50 passed
  • uv run pre-commit run --files ... — ruff format, ruff, mypy, codespell all pass
  • Live: confirmed on ss07 hardware that clean_segments([9]) starts a room clean (status → cleaning, clean_task_type → electoral), cross-checked against the room ids from rooms.

Usage

device = await device_manager.get_device(duid)
props = device.b01_q10_properties
# rooms: {1: "Maid's Room", 2: "Laundry", 9: "Kitchen", ...} (from map.rooms)
await props.vacuum.clean_segments([9])  # clean only the kitchen

Verified against two physical Roborock Q10 (roborock.vacuum.ss07) devices:

- Add the roborock.vacuum.ss07 schema to device_info.yaml (captured via
  `roborock get-device-info`), per CONTRIBUTING.
- Expand Q10Status to capture the ~25 additional fields the device reports in
  its full status dump (volume, child_lock, DND, net_info, time_zone, etc.).
- Fix dpNetInfo/dpTimeZone container field names so the decamelized device keys
  actually parse (they previously failed to construct).
- Add a CLI `status` path for B01 Q10 devices, which refreshes and prints the
  decoded status (previously `status` only worked for V1 devices).
- Add a second real (scrubbed) full-status protocol fixture and extend the
  status trait tests to cover the new fields.
Adds a SettingsTrait so Q10 settings the status trait already reads can also be
written: volume, child lock, Do Not Disturb, indicator LED, and dust collection.

Q10 setting writes were reverse-engineered + validated against a live ss07
robot: the bare data point write ({"dps": {"26": v}}) is silently ignored; the
value must be wrapped in the dpCommon (101) data point
({"dps": {"101": {"26": v}}}). Verified live by setting volume and child lock and
reading the new value back from the status trait, then restoring.

- traits/b01/q10/settings.py: SettingsTrait wired into Q10PropertiesApi.
- cli: q10-set-volume / -child-lock / -dnd / -led / -dust-collection.
- Unit tests assert the exact dpCommon-wrapped payload for each setter.
Adds SettingsTrait.set_dust_collection_frequency (dpDustSetting 50 via the
dpCommon wrapper) accepting YXDeviceDustCollectionFrequency or its int code
(0=daily, 15/30/45/60 cleans), plus a q10-set-dust-frequency CLI command.
Validated live on an R1: dust_setting 0 -> 15 -> restored to 0.
Address PR Python-roborock#846 review feedback:

- Split the monolithic Q10Status read-model + catch-all SettingsTrait into
  per-concern read/write traits mirroring the v1 layout: SoundVolumeTrait,
  ChildLockTrait, DoNotDisturbTrait, DustCollectionTrait, ButtonLightTrait,
  NetworkInfoTrait, ConsumableTrait. Each owns its read-model and setters.
- Add an UpdatableTrait base (q10/common.py) that wires a RoborockBase
  read-model to the DPS push stream; Q10PropertiesApi fans each decoded
  message out to every read-model trait.
- Convert clear on/off flags to bool (child_lock, not_disturb, dust_switch,
  auto_boost, multi_map_switch, line_laser_obstacle_avoidance) and map
  dust_setting to YXDeviceDustCollectionFrequency.
- set_frequency now takes YXDeviceDustCollectionFrequency only (no int union).
- Drop redundant decamelize comments on dpNetInfo/dpTimeZone; keep the note
  documenting the device's "ipAdress" typo.
- Update CLI Q10 setters and tests for the new trait layout.
Reverse-engineered against live Q10 hardware (toggling each setting in the
app and diffing the status dump):

- area_unit -> YXAreaUnit (square_meter=0, square_feet=1)
- carpet_clean_type -> YXCarpetCleanType (rise=0, avoid=1, ignore=2, cross=3)
- mop_state -> bool (mop module attached)
- ground_clean -> bool ("clean along floor direction")
- Rename YXDeviceDustCollectionFrequency.DAILY -> REGULAR to match the app
  ("regular" vs "frequent" 15/30/45/60); codes unchanged.

breakpoint_clean / add_clean_state / timer_type / user_plan / robot_type
remain int (no app control found, runtime-only, or constant).
Observed on live hardware: dpAddCleanState (96) pulses 0->1 while the app's
"re cleaning" (draw-a-rectangle / add-area) request is in progress, then back
to 0 once the robot has the area. Model it as a bool.
…rnings

ss07 hardware pushes data points this library does not model (confirmed live:
DPs 112 and 113, which stay 0 across docked/charging, segment cleaning,
lifted-off-ground fault, returning to dock and dustbin-removed states, and are
absent from the official app's vacuum plugin). Decoding every status push ran
them through `from_code_optional`, which routed through `from_code` and logged a
WARNING before returning None -- so each unknown DP spammed
"<code> is not a valid code for B01_Q10_DP".

- Make `from_code_optional` (and the int path of `from_any_optional`) do their
  own silent lookup instead of going through `from_code`. `from_code` stays
  strict (warns + raises) for callers that require a known code.
- Remove the duplicate-coded legacy entries `JUMP_SCAN` (101) and
  `CLIFF_RESTRICTED_AREA` (102): they collided with the confirmed
  `COMMON` (101) / `REQUEST_DPS` (102) codes, shadowing them in `from_code`, and
  are unused with unverifiable codes. Also drop the orphaned
  `CLIFF_RESTRICTED_AREA_UP` (103).
- Document DPs 112/113 as observed-but-unidentified.
- Tests: assert `from_code_optional` is silent, `from_code` still warns, and the
  B01 decode drops 112/113 without logging.
The new per-concern Q10 read-models `Consumable` and `NetworkInfo` were
star-exported into the `roborock.data` namespace where v1 also exports those
names, so `from roborock.data import Consumable` silently resolved to the v1
class (and mypy flagged the incompatible re-import). Rename the Q10 read-models
to `Q10Consumable` / `Q10NetworkInfo`; the traits already import them by fully
qualified path.
While removing the duplicate-coded legacy entries I also dropped DP 103, but
live capture shows ss07 hardware does push data point 103 (an empty list when no
cliff-restricted areas are configured). Restore CLIFF_RESTRICTED_AREA_UP at 103;
only the genuinely-colliding dpJumpScan (101) and dpCliffRestrictedArea (102)
stay removed.
The VacuumTrait movement commands were unverified guesses (with TODO notes) and
sent payload shapes the device ignores. Reverse-engineered the correct format
from the official app's vacuum plugin and confirmed each one live against an
ss07 robot (watching the DP stream react):

- start_clean:    {"201": 1}  (was {"201": {"cmd": 1}})  -> clean_task_type 1
- pause_clean:    {"204": 0}  (was {"204": {}})
- resume_clean:   {"205": 0}  (was {"205": {}})
- stop_clean:     {"206": 0}  (was {"206": {}})
- return_to_dock: {"202": 5}  (was {"203": {}}; dpStartBack=charge, and DP 203
                               is not the dock-return command)
- empty_dustbin:  {"203": 2}  (already correct; confirmed -> status emptying)

Also add spot_clean ({"201": 5} -> clean_task_type 5), confirmed live, plus a
`q10-vacuum-spot` CLI command. set_fan_level / set_clean_mode were confirmed to
work as bare DP writes (no dpCommon wrapper needed, unlike the settings).

Segment ({"201": 2}), zone ({"201": 3}) and build-map ({"201": 4}) clean are
documented but not exposed: they need a room/zone selection payload whose shape
could not be verified (the broker rejects subscribing to the app's command
topic, so the app's exact request can't be captured).
The Q10 status command only printed properties.status, omitting the other
read-model traits that refresh() populates (volume, child lock, do-not-disturb,
dust collection, network info, consumables). It also gated on
properties.status.status is not None, which the persistent subscribe loop can
satisfy with stale data pushed before the command ran.

Register update listeners on all refreshed traits, fire refresh(), and wait
for a fresh update (with a short settle window for multi-packet pushes) before
dumping every trait. Verified live against ss07 (R1).
Add VacuumTrait.clean_segments(segment_ids) for the Q10. dpStartClean (201)
carries the room selection as an object: {"cmd": 2, "clean_paramters": [ids]}
(cmd 2 = segment clean; "clean_paramters" mirrors the firmware's misspelling).
Segment ids are the same room ids reported by the map (map.rooms).

Captured from the official app and verified live against ss07 hardware
(clean_task_type -> 2 / electoral). Adds a q10-clean-segments CLI command and
trait payload tests.
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.

1 participant