Skip to content

feat(r2449a): X40 Ultra Complete compat + decode CLEANING_MODE bitfield#9

Open
carvalho2707 wants to merge 1 commit into
malard:mainfrom
carvalho2707:r2449a-compat
Open

feat(r2449a): X40 Ultra Complete compat + decode CLEANING_MODE bitfield#9
carvalho2707 wants to merge 1 commit into
malard:mainfrom
carvalho2707:r2449a-compat

Conversation

@carvalho2707
Copy link
Copy Markdown

Summary

Verified node-dreame end-to-end against a Dreame X40 Ultra Complete (dreame.vacuum.r2449a, firmware FU174072, EU region) on 2026-05-21. Auth, transport, MQTT, OSS map fetch, and the catalogue subset I use from a Next.js dashboard all work unchanged on the X40 — confirming the README's model-agnostic claim about the auth/transport layer.

The capture also closes the open siid 4 piid 23 — not decoded question flagged in README.md and VACUUM_PROP.CLEANING_MODE's JSDoc:

  • value & 0x3CleaningMode enum (Sweeping / Mopping / SweepAndMop / MopAfterSweep)
  • value & 0x1400 → always-on capability bits (constant across every observation on both r2532a and r2449a)

The r2532a 5120 ↔ 5122 transitions decode cleanly as Sweeping ↔ SweepAndMop — the user (or app) toggling Vac-only vs Vac + Mop, which co-fires with the dock's mop-install/remove sequence. The earlier "bit 1 = mop pads physically attached" reading is bit-pattern-compatible but the better mental model is "low 2 bits = clean-mode enum"; the mop-attachment correlation falls out of which modes need pads.

Verified the same field is mirrored cleanly on MOP_PADS_STATE (siid 2 piid 6) as plain 0..3. Writing directly to CLEANING_MODE and dropping the 0x1400 capability bits silently bricks the next clean on r2449a — the device 200-OKs the write but refuses subsequent start() until the bits are restored. MOP_PADS_STATE avoids that trap, which is why this PR documents it as the canonical write path.

Two more X40 findings landed alongside:

  • FEATURE_CONFIG_KEYS.SmartHost is 3-state, not boolean — values 0=Off, 1=Normal, 2=Deep (added CleanGenius enum).
  • siid 28 piid 5 is CleanGenius's sub-mode selector — 2=Vac+Mop, 3=MopAfterVac (added DOCK_PROP.CLEAN_GENIUS_SUB_MODE + CleanGeniusSubMode enum). Not in Tasshack's enum.

Both FEATURE_CONFIG_KEYS.CleanRoute and SmartHost are promoted from ~ to per CONTRIBUTING.md's tag conventions.

What changed

File Change
src/spec/vacuum-props.ts Decode CLEANING_MODE bitfield; rewrite MOP_PADS_STATE JSDoc to reflect r2449a observation that it mirrors the low 2 bits of CLEANING_MODE (and that the r2532a 0 ↔ 2 mop-install correlation is consistent with that).
src/spec/feature-config.ts Promote CleanRoute and SmartHost from ~ to with verified value enums.
src/spec/dock-props.ts Add CLEAN_GENIUS_SUB_MODE at siid 28 piid 5.
src/spec/enums.ts Promote CleaningMode from ASSUMED to VERIFIED; add CleanGenius and CleanGeniusSubMode enums.
src/capabilities.ts Add dreame.vacuum.r2449a entry (mirrors r2532a — both are flagship Ultra-class Dock-Series-3 robots).
src/vacuum.ts One-line annotation on the refreshFromCloud JSDoc confirming the getProperties 80001-spin behaviour also holds on r2449a.
README.md Tested-model badge lists both r2532a and r2449a; "Supported devices" paragraph documents what's shared; CleaningMode line in "Known specifically-NOT-verified pieces" updated from "not decoded" to "decoded; see JSDoc". FEATURE_CONFIG_KEYS count bumped 11 → 13.
CHANGELOG.md [Unreleased] entry under Added / Changed.

How it was verified

  • Live capture of MQTT properties_changed and event_occured pushes against an X40 Ultra Complete over a multi-minute window (full session: full-house clean → in-progress → mop install → dock → drying), tagged against the published node-dreame MIoT spec.
  • Each promotable FEATURE_CONFIG_KEYS value was confirmed by toggling the corresponding setting in the Dreamehome app and observing the JSON payload echo.
  • The CLEANING_MODEMOP_PADS_STATE lockstep was confirmed by both writing MOP_PADS_STATE = N for each N ∈ {0,1,2,3} and observing CLEANING_MODE settle at (0x1400 | N), AND by writing CLEANING_MODE = N directly (dropping 0x1400) and observing the next start() refusal — once 0x1400 was restored the next clean succeeded.
  • All existing tests still pass (341 passed, 13 skipped); typecheck and lint clean; build emits the expected dist.

Test plan

  • npm run typecheck — clean
  • npm run lint — clean
  • npm test — 341 passed (no new tests; the catalogue/JSDoc changes are documentation-typed)
  • npm run build — emits ESM + CJS + DTS

Notes for the maintainer

  • I followed CONTRIBUTING.md's verification-tag and naming conventions (VERIFIED <model> <YYYY-MM-DD>, UPPER_SNAKE_CASE for spec constants, as const at every level, no firmware-version branches).
  • I kept MOP_PADS_STATE as the property name despite the JSDoc rewrite — a rename is a public API break and the legacy name is fine as long as the doc explains what the field actually is. Happy to rename if you prefer, e.g. to CLEAN_MODE_MIRROR.
  • r2449a capabilities mirror r2532a. I'm confident every dock-side flag holds (X40 Ultra Complete ships with the same Dock-Series-3 station), and every boolean is true on the X40, but I haven't individually verified every supported enum value via setter writes. If you'd rather take the entry with verified: false, I can flip that.
  • I noticed TASK_STATUS = 18 (steady-state post-clean) doesn't appear in the TaskStatus enum; I did NOT add it because I'm not sure of its label (the X40 Ultra log shows 18 as the resting "ready for next task" value). Happy to follow up with a separate PR after a longer capture if useful.

🤖 Generated with Claude Code

Verified node-dreame end-to-end against a Dreame X40 Ultra Complete
(`dreame.vacuum.r2449a`, firmware FU174072, EU region) on 2026-05-21.
Auth, transport, MQTT, OSS map fetch, and the catalogue subset used by
my dashboard integration all work unchanged on the X40.

The X40 capture also closes the open "siid 4 piid 23 — not decoded"
question in README.md: low 2 bits are the `CleaningMode` enum, the
constant `0x1400` mask is always-on capability bits. The r2532a `5120
↔ 5122` transitions decode cleanly as `Sweeping ↔ SweepAndMop`.

Added
- `dreame.vacuum.r2449a` MODEL_CAPABILITIES entry (mirrors r2532a)
- `DOCK_PROP.CLEAN_GENIUS_SUB_MODE` at siid 28 piid 5
- `CleanGenius` enum (Off=0, Normal=1, Deep=2 — 3-state, not bool)
- `CleanGeniusSubMode` enum (VacAndMop=2, MopAfterVac=3)

Changed
- `VACUUM_PROP.CLEANING_MODE` bitfield decoded
- `VACUUM_PROP.MOP_PADS_STATE` JSDoc rewritten — verified canonical
  write path for the clean-mode enum on r2449a (writing to
  `CLEANING_MODE` directly and dropping the `0x1400` capability bits
  silently bricks the next clean)
- `CleaningMode` enum promoted ASSUMED → VERIFIED
- `FEATURE_CONFIG_KEYS.CleanRoute` promoted ~ → ✓ (values 1-4)
- `FEATURE_CONFIG_KEYS.SmartHost` promoted ~ → ✓ (3-state, 0-2)
- README "Tested model" badge lists both r2532a and r2449a

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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