fix(clean): room cleaning via clean/start_clean with a parameterized CleanParam (#25, #37) - #49
Conversation
Greptile SummaryThis PR fixes room cleaning on Flow firmware by switching
Confidence Score: 5/5Safe to merge — the root cause fix (wrong MQTT topic + incorrect proto schema) is well-understood, live-validated on a Flow 2, and covered by a thorough new test suite (167 tests, all green). The core change is a clean protocol-level fix: wrong topic → right topic, wrong payload schema → correctly reverse-engineered CleanTask. All new dispatch paths (map_id guard, NOT_READY retry, CONFLICT passthrough, whole-house fallback) have explicit tests. Enum value corrections were validated against a live device. The two observations are both cosmetic or low-impact behavior changes that do not affect cleaning correctness. No files require special attention. Important Files Changed
Sequence DiagramsequenceDiagram
participant HA as HA vacuum entity
participant Client as NarwalClient
participant Robot as Narwal Robot
HA->>Client: start_rooms(room_ids, work_mode, fan, ...)
alt room_ids is empty
Client->>Robot: clean/plan/start (whole-house)
Robot-->>Client: CommandResponse(SUCCESS)
else room_ids given
Client->>Client: check state.map_data.map_id
alt "map_id == 0"
Client->>Robot: get_map
Robot-->>Client: MapData(map_id, rooms, ...)
end
alt map_id still 0
Client-->>HA: CommandResponse(NOT_APPLICABLE)
else map_id available
Client->>Client: _build_start_clean_payload(room_ids, map_id, CleanParam)
Client->>Robot: clean/start_clean (CleanTask protobuf)
Robot-->>Client: CommandResponse
loop up to 3x while NOT_READY and is_docked
Client->>Client: asyncio.sleep(3s)
Client->>Robot: clean/start_clean (retry)
Robot-->>Client: CommandResponse
end
Client-->>HA: final CommandResponse
end
end
Reviews (3): Last reviewed commit: "fix(clean): room cleaning via clean/star..." | Re-trigger Greptile |
|
Thanks — addressed all three points:
|
…CleanParam (sjmotew#25, sjmotew#37) Room cleans were sent to clean/plan/start, but on Flow firmware that is StartWithPlan{planId, mapId} — it starts a saved plan by id and ignores any room payload, so the robot undocked and wandered instead of cleaning the selection. Switch start_rooms() to clean/start_clean (StartClean → CleanTask). Track the active map id (MapData.map_id, get_map field 2.1), which the CleanTask requires. clean/start_clean only works docked; from STANDBY the robot returns a new code 4 (CommandResult.NOT_READY) — retry briefly while docked. Build the CleanParam from named parameters: start_rooms() takes work_mode/fan/water/mop_strength/passes with defaults at the call site (vacuum-and-mop, standard suction/water/mop, single pass). Names and enums match the app's CleanTask proto: WorkMode (= robot_work_mode_*, whose value is CleanTask.taskType), corrected FanLevel/MopHumidity, added MopStrengthLevel; fan_speed labels use the app's user-visible suction names (quiet/standard/strong/super powerful/ultra powerful). Pre-rename fan_speed values (normal/max) remain accepted for back-compat. Validated live on a Flow 2: room clean returns SUCCESS and the robot cleans the selected rooms (confirmed via clean/current_clean_task/get). Both client copies synced. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…water / mop strength / passes) Adds Home Assistant controls for the room-clean parameters, backed by a CleanSettings dataclass on the coordinator (the single source the clean-start path reads): - select entities: work mode (Vacuum / Mop / Vacuum then mop / Vacuum and mop), mopping humidity (Slightly dry / Normal / Slightly wet), mop strength (Normal / High); - number entity: cleaning passes (1-3); - the vacuum's fan_speed is threaded through the same settings. Entity labels use the app's user-visible wording. Values persist across restarts via RestoreEntity (RestoreSelect / RestoreNumber / RestoreEntity) — set once and kept. async_clean_segments threads them into start_rooms; water and fan also apply live while cleaning. Built on sjmotew#49 (parameterized start_rooms / WorkMode). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…water / mop strength / passes) Adds Home Assistant controls for the room-clean parameters, backed by a CleanSettings dataclass on the coordinator (the single source the clean-start path reads): - select entities: work mode (Vacuum / Mop / Vacuum then mop / Vacuum and mop), mopping humidity (Slightly dry / Normal / Slightly wet), mop strength (Normal / High); - number entity: cleaning passes (1-3); - the vacuum's fan_speed is threaded through the same settings. Entity labels use the app's user-visible wording. Values persist across restarts via RestoreEntity (RestoreSelect / RestoreNumber / RestoreEntity) — set once and kept. async_clean_segments threads them into start_rooms; water and fan also apply live while cleaning. Built on sjmotew#49 (parameterized start_rooms / WorkMode). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@jgus — this is the correct root cause and I'm making your stack the merge base. Sequencing for all four forks currently working on this is in #66; yours is steps 1–7. I verified the diagnosis against master before planning around it: Two things before I merge this one:
Worth noting separately: #48 shows my own #22 fix (5b4dac7, per-model override map) was papering over a mis-derived base table. Good catch, and thank you for chasing it to the i18n source rather than stopping at the symptom. |
….0.0 Community RE in July 2026 established that room-specific cleaning has never worked: clean commands go to clean/plan/start, which discards the payload and runs the plan stored on the robot. Confirmed independently by three contributors (#37). On Flow 2 the same path can clear the robot's stored map (#55), so this needs to be on the front page rather than waiting for the fix to merge. - Add a "Known broken in v1.0.0" block covering room cleaning (#49), the cleaning-area sensor stuck at 1.8 m2 (#51), and misaligned room type labels (#48), each pointing at its tracking PR - Drop the Flow 2 room-label claim — labels were wrong on all models, not different on Flow 2 (#48 removes the per-model override) - Soften the v01.07.22 "auto-fallback handles this" claim; the fallback targets a topic that ignores payloads - Add Freo Z10 Turbo (AX26) as confirmed working (#40), Narwal JX as unconfirmed (#42) - Troubleshooting: open the Narwal app when pressing Submit if setup times out (#40) Gitignore local agent tooling so it can't ride along on a commit. Merge sequencing for the fixes is tracked in #66. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bbtiKLgDSpSiTnMJts89u
|
Answering my own question from above — I pulled this branch locally and read #49 emits neither of the two fields @sytchi flagged in #37. # client.py:1102 — ZoneOption carries only fields 1 and 2, no field 4
items = [
{"1": {"1": 1, "2": rid}, "2": dict(param), "3": idx + 1}
for idx, rid in enumerate(room_ids)
]
task = {
"1": map_id,
"2": items if len(items) > 1 else items[0],
"3": {}, # ← TaskOption is empty
"5": int(work_mode),
}and the typedef confirms it isn't a serialization artifact — @sytchi's report is that on Flow 1 firmware v01.08.03.07, Why this probably blocks merging #49 on its ownI checked my own hardware: my Flow 1 is on v01.08.03.07 — the same firmware as @sytchi. So I'd expect this branch to return More importantly, that's the same firmware line as the users this issue exists for — @saeft2003 is on v01.08.00.07 and @Duqino on v01.07.23.00 (#37). If the requirement is firmware-gated at 01.07/01.08 rather than model-gated, #49 would work on @jgus's Flow 2 and still not fix #37 for the people who reported it. Two readings fit the evidence so far, and they lead to different fixes:
@jgus — no criticism intended here; your branch is live-validated and clearly correct on Flow 2. This looks like genuine firmware divergence that only shows up across hardware neither of us has alone. If you have any signal on which reading is right, I'd value it. One useful data point either way: my |
Bug-fix release. Two fixes, no breaking changes. - Cleaning-area sensor reports real coveredArea instead of a constant 1.8 m² (#51) - Config-flow translations synced with code, orphaned string dropped (#47) Room-type naming (#48) is deliberately NOT in this release. Its enum ordering at indices 8-11 is unconfirmed and renaming rooms is user-visible, so it waits for corroboration rather than shipping wrong twice. See 8f6d50f. Room cleaning (#49) and the silent start() no-op (#69) remain open and are flagged in the README. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UnTUYGYSE554n4tdH87Qu8
Status: approved in principle, blocked on my hardware for several daysI want to be straight about the timeline rather than leave this sitting silently. I can't run the validating test for several days. It undocks a live robot and I need to be physically present; realistically that's early next week. This is not review latency — the code review is done and I think this is right. Where I've landed on the substanceI've read
That reversal is exactly why I want the hardware measurement before merging rather than after. The test, if anyone wants to beat me to itMy Flow 1 runs v01.08.03.07 — byte-identical to @sytchi's firmware, which is what makes this the discriminating measurement rather than just another data point. Either f4 is required on that firmware or it isn't, and this settles it. Plus model, firmware version, and whether the response code was SUCCESS. @sytchi — you're the one whose report this contradicts, so your result carries the most weight. @saeft2003 (v01.08.00.07), @Duqino (v01.07.23.00), @ken99999 (Flow 2, v01.09.05.01), @pspik — same firmware line, all useful. Any single one of you running this closes it days before I can. If it fails on v01.08.03.07 but works elsewhere, we've learned the requirement is firmware-gated and #49 needs a conditional f4 — that's a small follow-up, not a redesign, and I'd rather find out from a real robot than from more decompilation. Thanks for your patience on this one @jgus. It's the right fix and it's been ready longer than it should have been. |
|
Tested #49 (branch fix/room-cleaning) — works. 🎉
Two rooms tested, two different segment IDs, both cleaned the selected room only Starting room-specific clean: rooms=[5] Starting room-specific clean: rooms=[2] Command now correctly goes to Data point on the ZoneOption f4 question: this is a different firmware line Happy to run more tests if useful. |
, #70) @shin906710 (#70) and @romedtino (#40) independently reported product_key qV6BujoYLz on firmware v01.02.00.15 under two different marketing names — "Freo Z10 Pro" and "Freo Z10 Turbo". Same key, same firmware, so treat it as one platform and label the selector entry with both names. AX26 was previously only in KNOWN_PRODUCT_KEYS' unverified section, so these users had to fall back to "Other / Auto-detect" and got entities named after the raw product key. Promote it to the confirmed block (which is ordered so discovery tries confirmed keys first) and expose it in NARWAL_MODELS. README already listed AX26 as "Z10 Turbo" only; widen it and record that @shin906710 confirmed room cleaning works on this line with PR #49. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017N9zqfSeQ2QK1YSkg6iBZF
|
First hardware confirmation — this PR works. @shin906710 ran the test on a Freo Z10 Pro (AX26, firmware v01.02.00.15), reported in #70:
Two rooms, two distinct segment IDs, each cleaning only the selected room, verified in the Narwal app. Started via @jgus — your fix is no longer unproven on real hardware. Given this project already shipped one wrong room-clean fix on reasoning alone, that distinction matters. What this does and doesn't settleSettles: the Doesn't settle: @sytchi's report that firmware v01.08.03.07 requires Still needed: one v01.08.03.07 resultMy Flow is on v01.08.03.07 and my test is still pending hardware access (early this week). Second candidate as of today: @Duqino surfaced in #37 on exactly that firmware, and I've asked them to run the same test. If v01.08 passes, this merges as-is. If it fails, f4 becomes conditional on firmware — a small change, not a redesign. No changes requested to this branch. Holding purely on the firmware question. |
|
Confirming on Narwal Flow (AX12), firmware v01.08.03.07 — the firmware line this PR was waiting on. Setup: upstream master (35509cd, post-v1.0.1) with this PR cherry-picked on top, Home Assistant 2026.7.4, robot segments mapped to HA areas, started from the dock via vacuum.clean_area. Result: room cleaning works on v01.08.03.07, and ZoneOption field 4 is not required. Two rooms, selected in order Main Hallway → Kitchen: It cleaned those two rooms and nothing else — no wandering onto the rest of the floor. Accepted on the first attempt: no NOT_READY, no retry, and no warnings or errors for the whole ~35 minute run. Clean order is honoured. The robot did the Main Hallway first, then the Kitchen, matching both the selection order and the CleanItem order field. An earlier run in the same session with rooms=[2, 7] (Main Hallway → Living Room) also returned SUCCESS (code=1) and ran in the order given, so the repeated-CleanItem path is confirmed across two different room pairs. Single-room selections have been running correctly here for several days. The payload omits field 4, matching the app capture you cited — so on this firmware the f4-required theory doesn't hold. |
|
Merged to Two independent hardware confirmations, on two different model and firmware lines:
@Zebble — thank you. Yours is the one this PR was actually waiting on: v01.08.03.07 is the firmware line #37's reporters run, and it's the line where the Conflict resolutionThis PR predated the 07-31/08-02 merges, so I resolved two conflicts rather than asking for a rebase:
175 tests passing, both client copies in sync, CI green on One thing still open
What isn't settled is whether Ships in v1.0.2. |
…1.0.2 breaking changes Rewrites the top-of-README warning block, which still said room cleaning was broken with no fix available. It is fixed and merged (#49, 05af870). - Replaces "Known broken in v1.0.1" with three blocks: what is fixed on master (with both hardware confirmations), what is still broken in the released v1.0.1, and the two breaking changes coming in v1.0.2 - Adds a Project Status section: the nine commits merged since v1.0.1, ranked next steps, and the four open protocol questions where help is wanted - Fan speed list updated to the app's own labels; notes the old values are kept as aliases but now map to a different tier - Corrects the cleaning-area sensor line, which still claimed a fixed 1.8 m² after #51 fixed it in v1.0.1 - Documents the new sensors from #52, the dock requirement for room cleaning, and the #73 frozen-state limitation - Flow and Flow 2 compatibility rows updated with the confirmed firmware Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shin906710's three app captures on a Freo Z10 Pro (#70) settle the fifth-tier question left open by #49. The app's top suction tier sends CleanParam tag 2 = 4 (DEEP), the tier below it 3 (STRONG), so the five-value enum is correct and SUPER (5) is unreachable from that app. Two changes follow: - The offered labels drop the "powerful" suffix: Quiet, Standard, Strong, Super, Ultra. The v1.0.2/v1.0.3 spellings "Super powerful" and "Ultra powerful" stay in FAN_SPEED_MAP as aliases, alongside the older lowercase quiet/normal/strong/max, so no automation breaks. - Ultra is model-gated. On AX26 it is not merely absent from the app — clean/set_fan_level carries SweepFanLevel, which has no SUPER, so the client already maps 5 -> STRONG. Offering it meant a picker entry that silently applied Strong mid-clean. fan_speed_list_for() withholds it there and leaves every other model untouched, including entries with no stored product_key. Adds tests/test_fan_speed.py — the fan block shipped in #49/#50 with no label coverage at all, so the rename passed a green suite on its own. Also records both answers in docs: CleanParam tag 8 is the coverage-precision toggle (1 = Standard, 2 = Meticulous), confirmed by the controlled capture pair where only 40 01 -> 40 02 moved. That closes the tag-8 unknown carried since Phase 9 (#25). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YAo9szBPifvDJrsag2oW6Y
Summary
Room cleaning was broken on Flow firmware — selecting rooms sent the robot off the
dock to wander instead of cleaning the selection. This switches to the correct
command and builds the clean parameters from named, reverse-engineered settings.
Fixes #25, #37.
Root cause
start_rooms()sentclean/plan/start, which on Flow firmware isStartWithPlan{planId, mapId}— it starts a saved plan by id and ignores any roompayload. The robot ran its last plan instead of cleaning the selected rooms.
Fix
clean/start_clean→CleanTask{map_id, [CleanItem{ZoneOption, CleanParam, order}], taskType}. Track the active map id (MapData.map_id,get_mapfield 2.1),which the CleanTask requires.
clean/start_cleanonly works docked; from STANDBY the robot returns a new code 4(
NOT_READY) — retry briefly while docked.Parameterized CleanParam
The CleanParam is built from named parameters with defaults at the call site:
Names and enums match the app's
CleanTaskproto:WorkMode(the app'srobot_work_mode_*selector):VACUUM / MOP / VACUUM_THEN_MOP / VACUUM_AND_MOP; its value is theCleanTask.taskTypethe robot executes.FanLevel/MopHumiditycorrected to the robot's real enum values;MopStrengthLeveladded.fan_speedlabels use the app's user-visible suction names:quiet / standard / strong / super powerful / ultra powerful.Enum class names and CleanParam field names are verbatim from the proto; the integer
values are live-validated on a Flow 2.
Changes
narwal_client/(+ embeddedcustom_components/narwal/narwal_client/copy): rewritestart_rooms/_build_start_clean_payload; addWorkMode+MopStrengthLevel,correct
FanLevel/MopHumidity; addMapData.map_idand track it inget_map.custom_components/narwal/const.py:FAN_SPEED_MAP→ the app's user-visible suction labels.Back-compat:
set_fan_speedstill accepts the pre-rename values (normal→ standard,max→ top); they are not offered in the fan-speed list.TOPIC_CMD_PLAN_START(clean/plan/start)and
TOPIC_CMD_CLEAN_TASK(clean/start_clean, was misleadingly…_LEGACY).tests/test_client_rooms.py: cover the CleanTask builder (taskType/mode dispatch,fan/water/strength encoding, settings forwarding) and
start_roomsdispatch(whole-house fallback, missing map id, NOT_READY retry, CONFLICT passthrough).
Testing
pytest tests/— all green (167).selected rooms (confirmed via
clean/current_clean_task/get).