feat: Q10 (B01/ss07) room/segment cleaning (clean_segments)#851
Draft
tubededentifrice wants to merge 12 commits into
Draft
feat: Q10 (B01/ss07) room/segment cleaning (clean_segments)#851tubededentifrice wants to merge 12 commits into
tubededentifrice wants to merge 12 commits into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 anidandname). 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 singlefeat: Q10 (B01/ss07) room/segment cleaningcommit.Protocol notes
Unlike whole-home (
{"dps": {"201": 1}}) and spot ({"dps": {"201": 5}}), which take a bare task code, segment cleaning sendsdpStartClean(201) as an object:{"dps": {"201": {"cmd": 2, "clean_paramters": [<room_id>, ...]}}}cmd2is 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.map.rooms[i].id), so callers can go straight fromroomstoclean_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
VacuumTrait.clean_segments(segment_ids: list[int]).start_cleandocstring now that the segment payload is known.q10-clean-segmentsCLI command (--segments 9,2).clean_segments.Testing
uv run pytest tests/devices/traits/b01/q10/ tests/protocols/test_b01_q10_protocol.py— 50 passeduv run pre-commit run --files ...— ruff format, ruff, mypy, codespell all passclean_segments([9])starts a room clean (status → cleaning,clean_task_type→ electoral), cross-checked against the room ids fromrooms.Usage