Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Breaking

- **`CleaningMode` value mapping corrected on r2449a** β€” `Sweeping` and
`SweepAndMop` values were inverted in v0.4.1 / v0.4.2. The previous
mapping (`Sweeping = 0`, `SweepAndMop = 2`) was round-trip-verified
(write `N` β†’ echo `N`) but the named-mode ↔ value pairing was not
established against the Dreamehome app. A 2026-05-25 observation
probe on r2449a (tap each sub-mode in the app with marker labels
typed BEFORE the tap, capture the MQTT echo) produced the correct
mapping:
- `SweepAndMop = 0` (was `2`)
- `Mopping = 1` (unchanged)
- `Sweeping = 2` (was `0`)
- `MopAfterSweep = 3` (unchanged)
Behavioural corroboration: with the old mapping, writing `0` for
"Sweeping" caused the dock to attach the mop pad (because the device
actually entered `SweepAndMop`).

**r2532a β€” needs re-verification.** Prior r2532a notes that paired
`5120 ↔ 5122` transitions with the mop-install / remove dock sequence
were named under the inverted mapping. The physical co-firing with
the dock sequence is unchanged; the *direction* (`Sweeping β†’
SweepAndMop` vs the reverse) depends on whether r2532a uses the
same value mapping as r2449a. Affected JSDocs on `CLEAN_MODE_SETTING`
and `CLEANING_MODE` now carry a "needs re-verification on r2532a"
note instead of the previous named-mode direction claim.

### Added

- **`DOCK_PROP.WATER_VOLUME_FINE` at `siid 28 piid 1`.** Fine-grained
per-job mop-water volume; integer slider `1..32`. The axis the
Dreamehome app surfaces in the per-mode water controls β€” the coarse
`VACUUM_PROP.WATER_VOLUME` (siid 4 piid 5) 3-step `Low/Medium/High`
enum is the legacy axis; new code should write the fine field.
VERIFIED on r2449a 2026-05-25 by dragging the app slider through
`1, 8, 16, 32` and observing the MQTT echo. Not yet observed on
r2532a.

### Changed

- **`FEATURE_CONFIG_KEYS.SuctionMax` promoted from `~` to `βœ“`.**
VERIFIED on r2449a 2026-05-25 β€” boolean (`0 = off`, `1 = on`),
writeable in both `Sweeping` and `MopAfterSweep` sub-modes (the two
sub-modes the Dreamehome app surfaces the toggle in β€” any mode with
a pure-vacuum phase). Tasshack name: `MAX_SUCTION_POWER`.

## [0.4.2] - 2026-05-24

### Breaking
Expand Down
15 changes: 15 additions & 0 deletions src/spec/dock-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@ export const DOCK_PROP = {
* the Dreamehome app's UI). Use the `CleanGeniusSubMode` enum.
*/
CLEAN_GENIUS_SUB_MODE: { siid: 28, piid: 5 } as const,
/**
* VERIFIED on r2449a 2026-05-25 β€” fine-grained per-job mop-water
* volume. Integer slider, range `1..32`. This is the axis the
* Dreamehome app surfaces in the per-mode water controls β€” the
* coarse `VACUUM_PROP.WATER_VOLUME` (siid 4 piid 5) 3-step
* `Low/Medium/High` enum is a legacy field; new code should write
* here. Observation probe (drag the app slider through several
* values) echoed back cleanly: `1`, `8`, `16`, `32`. No `WaterVolume`
* enum is defined β€” values are written as bare integers.
*
* **r2532a β€” not yet observed.** Likely shared given that all other
* dock-cluster properties on `siid 28` are present on both models,
* but the slider hasn't been confirmed live on r2532a.
*/
WATER_VOLUME_FINE: { siid: 28, piid: 1 } as const,
/**
* VERIFIED on r2532a β€” Mop-Washing Water Temperature enum (4 values).
* 0 = Normal (no heating)
Expand Down
31 changes: 24 additions & 7 deletions src/spec/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,21 +296,38 @@ export enum WaterVolume {
}

/**
* VERIFIED on r2449a 2026-05-21 β€” packed into the **low 2 bits** of
* VERIFIED on r2449a 2026-05-25 β€” packed into the **low 2 bits** of
* `CLEANING_MODE` (siid 4 piid 23) and exposed as plain `0..3` on the
* writable `CLEAN_MODE_SETTING` (siid 2 piid 6). The full `CLEANING_MODE`
* value is `(CleaningMode | 0x1400)`; mask with `& 0x3` to extract this
* enum.
*
* Consistent with r2532a 2026-05-02 observations after bitfield decode:
* the `5120 ↔ 5122` transitions are `(0x1400 | Sweeping) ↔ (0x1400 |
* SweepAndMop)`. See `VACUUM_PROP.CLEANING_MODE` for the trap to avoid
* when writing the raw bitfield directly.
* **Correction (vs v0.4.1 / v0.4.2):** the prior values `Sweeping = 0`
* and `SweepAndMop = 2` were inverted. The v0.4.1 PR verified
* round-trippability (write N β†’ echo N) but not the named-mode ↔ value
* pairing. A 2026-05-25 observation probe on r2449a (tap each sub-mode
* in the Dreamehome app, label the marker BEFORE the tap, capture the
* MQTT echo) produced:
* SweepAndMop β†’ `2.6 = 0`, `4.23 = 5120` (`0x1400 | 0`)
* Mopping β†’ `2.6 = 1`, `4.23 = 5121` (`0x1400 | 1`)
* Sweeping β†’ `2.6 = 2`, `4.23 = 5122` (`0x1400 | 2`)
* MopAfterSweep β†’ `2.6 = 3`, `4.23 = 5123` (`0x1400 | 3`)
* Behavioural corroboration: with the old map, writing `0` for "Sweeping"
* caused the dock to attach the mop pad (because the device actually
* entered `SweepAndMop`).
*
* The r2532a `5120 ↔ 5122` transition analysis previously here is now
* known to be ambiguous on naming and has been moved into a "needs
* re-verification on r2532a" note on `VACUUM_PROP.CLEANING_MODE` β€” the
* physical co-firing with the mop-install/remove dock sequence still
* holds, but the *direction* (`Sweeping β†’ SweepAndMop` vs the reverse)
* now depends on whether the r2532a model uses the same value mapping
* as r2449a. Until re-verified, only the r2449a mapping is normative.
*/
export enum CleaningMode {
Sweeping = 0,
SweepAndMop = 0,
Mopping = 1,
SweepAndMop = 2,
Sweeping = 2,
MopAfterSweep = 3,
}

Expand Down
9 changes: 8 additions & 1 deletion src/spec/feature-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,14 @@ export const FEATURE_CONFIG_KEYS = {
ExtrFreq: "ExtrFreq",
/** ~ Smart Drying mode (Tasshack: SMART_DRYING). */
SmartDrying: "SmartDrying",
/** ~ Max Suction Power toggle (Tasshack: MAX_SUCTION_POWER). */
/**
* βœ“ Max Suction Power toggle. VERIFIED on r2449a 2026-05-25 by
* toggling each direction in the Dreamehome app and observing
* `FEATURE_CONFIG_JSON` deltas. Boolean (`0 = off`, `1 = on`); writes
* accepted in both `Sweeping` and `MopAfterSweep` sub-modes (the two
* sub-modes the app surfaces the toggle in β€” any mode with a pure-
* vacuum phase). Tasshack name: `MAX_SUCTION_POWER`.
*/
SuctionMax: "SuctionMax",
/** ~ Collision Avoidance (Tasshack: COLLISION_AVOIDANCE). Softer obstacle approach. */
LessColl: "LessColl",
Expand Down
59 changes: 37 additions & 22 deletions src/spec/vacuum-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ export const VACUUM_PROP = {
*/
FAULTS_STR: { siid: 4, piid: 18 } as const,
/**
* VERIFIED on r2449a 2026-05-21 β€” writable `CleaningMode` enum at
* `siid 2 piid 6`. Holds the enum directly as plain `0..3` (Sweeping
* / Mopping / SweepAndMop / MopAfterSweep), with no capability-bit
* mask. The canonical write path for clean-mode changes.
* VERIFIED on r2449a 2026-05-25 β€” writable `CleaningMode` enum at
* `siid 2 piid 6`. Holds the enum directly as plain `0..3`
* (SweepAndMop / Mopping / Sweeping / MopAfterSweep, in value order),
* with no capability-bit mask. The canonical write path for
* clean-mode changes.
*
* Writing `0..3` here moves both this property and `CLEANING_MODE`
* (siid 4 piid 23) in lockstep β€” the device echoes the value into
Expand All @@ -70,12 +71,19 @@ export const VACUUM_PROP = {
* drops the capability bits and silently bricks the next clean;
* prefer this property to avoid the trap.
*
* Consistent with r2532a 2026-05-02 observations (`0 ↔ 2` synchronised
* with `MiotState 17 ReturnInstallMop` / `18 ReturnRemoveMop` edges):
* a `Sweeping β†’ SweepAndMop` mode change is exactly what triggers the
* dock-side mop-install sequence, so the `0 β†’ 2` edge "the moment the
* device prepared to drop pads" was the user-write into this field
* driving the install, not a separate "mop pads available" signal.
* **r2532a β€” needs re-verification.** Earlier 2026-05-02 notes on
* this property paired the `0 ↔ 2` echoes with the
* `MiotState 17 ReturnInstallMop` / `18 ReturnRemoveMop` edges and
* read them as `Sweeping β†’ SweepAndMop` (mop-install) transitions.
* The r2449a value swap (`0 = SweepAndMop`, `2 = Sweeping` β€” see the
* `CleaningMode` enum) inverts the named direction of that
* transition: under the corrected mapping, `0 β†’ 2` would be
* `SweepAndMop β†’ Sweeping` (mop-REMOVE), not install. The physical
* co-firing with the dock sequence still holds, but the direction
* depends on whether r2532a uses the same value mapping as r2449a.
* Re-verify on r2532a by tapping each sub-mode in the Dreamehome app
* (label-before-tap protocol) and reading back the echo before
* trusting the named-mode direction in r2532a notes.
*
* Previously named `MOP_PADS_STATE` (pre-v0.5); the rename reflects
* the actual semantics β€” this is the clean-mode setting, not an
Expand Down Expand Up @@ -234,25 +242,32 @@ export const VACUUM_PROP = {
*/
TASK_PHASE: { siid: 4, piid: 25 } as const,
/**
* VERIFIED on r2449a 2026-05-21 β€” packed `(capability_mask | clean_mode)`.
* VERIFIED on r2449a 2026-05-25 β€” packed `(capability_mask | clean_mode)`.
*
* `value & 0x3` β€” the `CleaningMode` enum (0=Sweeping, 1=Mopping,
* 2=SweepAndMop, 3=MopAfterSweep). Mirrors the
* canonical write path at `CLEAN_MODE_SETTING`.
* `value & 0x3` β€” the `CleaningMode` enum (0=SweepAndMop,
* 1=Mopping, 2=Sweeping, 3=MopAfterSweep).
* Mirrors the canonical write path at
* `CLEAN_MODE_SETTING`.
* `value & 0x1400` β€” always-on capability flags (bits 10 + 12).
* Constant across every observation on r2449a and
* on r2532a; suspected to encode "mop-attachment
* supported" / "auto-install supported" hardware
* capability claims rather than runtime state.
*
* The `5120 ↔ 5122` transitions observed on r2532a decode as
* `(0x1400 | Sweeping) ↔ (0x1400 | SweepAndMop)` β€” i.e. the user
* toggling between Vac-only and Vac+Mop, which co-fires with the
* mop-install/remove dock sequence (`MiotState 17` / `18`). The earlier
* "bit 1 = mop pads physically attached" reading is consistent with
* the bit pattern but the better mental model is "low 2 bits = clean
* mode enum"; the mop-attachment correlation falls out of which clean
* modes need pads.
* The r2449a observation probe captured all four values cleanly:
* `5120 = (0x1400 | SweepAndMop)`, `5121 = (0x1400 | Mopping)`,
* `5122 = (0x1400 | Sweeping)`, `5123 = (0x1400 | MopAfterSweep)`.
*
* **r2532a β€” needs re-verification.** The 2026-05-02 r2532a notes
* paired `5120 ↔ 5122` transitions with the mop-install / remove
* dock sequence (`MiotState 17` / `18`) and read them as `Sweeping ↔
* SweepAndMop` β€” but that pre-dates the r2449a value swap and was
* named under the inverted mapping. The physical correlation
* (clean-mode change co-fires with the dock sequence) is unchanged,
* but the *named direction* depends on whether r2532a uses the same
* value mapping as r2449a. Re-verify on r2532a before trusting the
* named-mode direction. The "low 2 bits = clean-mode enum" mental
* model itself remains correct on both models.
*
* **Prefer writing `CLEAN_MODE_SETTING` over this field.** A direct
* write to `CLEANING_MODE` that drops the `0x1400` capability bits
Expand Down
9 changes: 7 additions & 2 deletions test/miot-spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,13 @@ describe("Property bundle siids do not collide", () => {
});

describe("CleaningMode + WaterVolume (kept for back-compat with Tasshack callers)", () => {
it("CleaningMode covers the standard 0..3", () => {
expect(CleaningMode.Sweeping).toBe(0);
it("CleaningMode covers the corrected r2449a mapping", () => {
// Verified via 2026-05-25 observation probe on r2449a (label-before-tap,
// capture MQTT echo). Sweeping ↔ SweepAndMop values were inverted in
// v0.4.1/v0.4.2; see CHANGELOG for full context.
expect(CleaningMode.SweepAndMop).toBe(0);
expect(CleaningMode.Mopping).toBe(1);
expect(CleaningMode.Sweeping).toBe(2);
expect(CleaningMode.MopAfterSweep).toBe(3);
});
it("WaterVolume covers Low/Medium/High = 1/2/3", () => {
Expand Down