diff --git a/CHANGELOG.md b/CHANGELOG.md index 176a872..45b4556 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/spec/dock-props.ts b/src/spec/dock-props.ts index fc043d8..d515471 100644 --- a/src/spec/dock-props.ts +++ b/src/spec/dock-props.ts @@ -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) diff --git a/src/spec/enums.ts b/src/spec/enums.ts index efc61a4..54adc6e 100644 --- a/src/spec/enums.ts +++ b/src/spec/enums.ts @@ -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, } diff --git a/src/spec/feature-config.ts b/src/spec/feature-config.ts index ae354be..9aec25f 100644 --- a/src/spec/feature-config.ts +++ b/src/spec/feature-config.ts @@ -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", diff --git a/src/spec/vacuum-props.ts b/src/spec/vacuum-props.ts index 544aade..e50dc19 100644 --- a/src/spec/vacuum-props.ts +++ b/src/spec/vacuum-props.ts @@ -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 @@ -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 @@ -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 diff --git a/test/miot-spec.test.ts b/test/miot-spec.test.ts index c6ff44c..e584bca 100644 --- a/test/miot-spec.test.ts +++ b/test/miot-spec.test.ts @@ -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", () => {