Skip to content

feat: expand Q10 (B01/ss07) status support and add device info#846

Open
tubededentifrice wants to merge 11 commits into
Python-roborock:mainfrom
tubededentifrice:support-q10-devices
Open

feat: expand Q10 (B01/ss07) status support and add device info#846
tubededentifrice wants to merge 11 commits into
Python-roborock:mainfrom
tubededentifrice:support-q10-devices

Conversation

@tubededentifrice

@tubededentifrice tubededentifrice commented Jun 14, 2026

Copy link
Copy Markdown

Summary

Adds Q10 (roborock.vacuum.ss07, B01 protocol) device metadata, expands the decoded Q10 status model, and adds Q10 settings writers for the values now exposed by status.

This was checked against two physical Q10 devices.

Changes

  • Add the roborock.vacuum.ss07 device schema to device_info.yaml.
  • Decode the additional Q10 status fields reported by a full dpRequestDps response, including volume, child lock, Do Not Disturb, mop state, dust settings, map switches, cleaning line, time zone, country code, and network info.
  • Fix dpNetInfo and dpTimeZone container field names so decamelized device payloads construct correctly.
  • Add SettingsTrait writers for volume, child lock, Do Not Disturb, button light, dust collection, and dust collection frequency. Q10 setting writes are wrapped through dpCommon (101), which is what the device accepts.
  • Allow the CLI status command to refresh and display decoded status for B01 Q10 devices.
  • Add a scrubbed vacuum-only status fixture and tests for expanded status decoding, nested containers, and the new settings writers.

Testing

  • uv run pytest — 507 passed on this branch
  • uv run pre-commit run --all-files
  • Live checked Q10 status and settings writes, then restored changed values

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.
@tubededentifrice

tubededentifrice commented Jun 14, 2026

Copy link
Copy Markdown
Author

Added Q10 settings writers for the values this PR now reads: volume, child lock, Do Not Disturb, button light, and dust collection.

Q10 setting writes need the dpCommon (101) wrapper. For example, setting volume sends {"dps":{"101":{"26":65}}}; direct writes to 26 were ignored by the tested device.

I checked volume and child lock by writing a new value, reading status back, and restoring the original value. The session CLI now has q10-set-volume, q10-set-child-lock, q10-set-dnd, q10-set-led, and q10-set-dust-collection.

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.
@tubededentifrice

tubededentifrice commented Jun 14, 2026

Copy link
Copy Markdown
Author

Added SettingsTrait.set_dust_collection_frequency() for dpDustSetting (50), using the same dpCommon (101) wrapper as the other Q10 settings writers.

The method accepts either YXDeviceDustCollectionFrequency or the raw interval code (0, 15, 30, 45, or 60). The session CLI exposes this as q10-set-dust-frequency.

Live check: changed a Q10 from daily (0) to every 15 cleans, read the updated value back, then restored daily.

@tubededentifrice

Copy link
Copy Markdown
Author

@Lash-L @allenporter This is ready for review when you get a chance — part of the Q10 series (#846#847#848, stacked in that order). Thanks!

@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.

Very nice, thank you for this contribution!

As always, the hard part here is naming and grouping, so curious to get your thoughts on some of my questions.

Comment thread roborock/data/b01_q10/b01_q10_containers.py Outdated
Comment thread roborock/data/b01_q10/b01_q10_containers.py Outdated
Comment thread roborock/devices/traits/b01/q10/settings.py Outdated
Comment thread roborock/data/b01_q10/b01_q10_containers.py Outdated
Comment thread roborock/data/b01_q10/b01_q10_containers.py Outdated
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.
@tubededentifrice

Copy link
Copy Markdown
Author

I've managed to get and decompile the plugin fetched from the app, I need small adjustments

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).
@tubededentifrice

tubededentifrice commented Jun 15, 2026

Copy link
Copy Markdown
Author

Alright @allenporter this one should be gtg 😓

@tubededentifrice

Copy link
Copy Markdown
Author

Just put together also #851 for room cleaning -- had to do packet capture in the emulator for this one! It's stacked on top of this one, so I kept it as draft for the moment, I'll properly rebase it once this one merges

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.

2 participants