feat(clean): expose clean settings as HA entities (work mode / fan / water / mop strength / passes) - #50
Conversation
Greptile SummaryThis PR adds Home Assistant
Confidence Score: 5/5Safe to merge; the new entity layer is well-isolated, restore logic is straightforward, and the protobuf payload changes are covered by real-device validation and 167 passing tests. The change introduces a clean shared-state design (CleanSettings dataclass) that three independent entity types write to and the clean-start path reads from. The FanLevel enum realignment is intentional and correctly handled through both the backward-compat alias map and the _FAN_LABELS reverse map. The whole-house start rerouting through start_rooms is architecturally sound. No files require special attention. Important Files Changed
Reviews (3): Last reviewed commit: "fix(clean): whole-house start cleans all..." | Re-trigger Greptile |
…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>
cc8b075 to
225274e
Compare
async_start sent clean/plan/start (StartWithPlan), which replays the saved current plan — the last room selection — so a whole-house Start re-ran the previous room-subset clean instead of cleaning the house. Enumerate every cleanable room and clean via clean/start_clean (start_rooms), matching the app's allRoomIds() path; fall back to the saved-plan start only when no map rooms are known. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Tested feat/clean-settings on hardware — works. 🎉
All the new controls show up (work mode / water / mop strength selects + passes number, Each cleaned only the selected room, vacuum-only as set, and passes/fan tracked the entity Native French speaker, app in fr-FR. French labels: Work mode (Mode de nettoyage):
Mopping humidity (Humidité de la vadrouille):
Passes: the app splits this by mode — "Cycle d'aspiration" (vacuum tab) and Two things beyond labels:
|
|
@shin906710 — this is a second hardware confirmation and two protocol findings in one comment. Taking them in order of how much they change things. The test result
@jgus — #50 is queued directly behind #49, unchanged, and the merge order in #66 stands. French translations — taking them as givenThank you for pulling these from the app rather than translating them. "Vadrouille" over "Serpillère" is the kind of thing only a native speaker with the app open catches, and getting it wrong is exactly how a translation starts feeling machine-made. The passes label being split by mode — Cycle d'aspiration / Cycle de lavage à la vadrouille — is a real problem for us, because #50 exposes one Suction: four tiers or fiveYou're right that this needs resolving, and the current state is messier than either of us implied. Master today ships four levels, zero-based: QUIET = 0, NORMAL = 1, STRONG = 2, MAX = 3#49/#50 replaces that with five, one-based, read out of the APK proto: MUTE = 1, NORMAL = 2, STRONG = 3, DEEP = 4, SUPER = 5So it isn't simply "one tier too many" — the whole scale shifted by one, and if the five-value version is right then every fan speed master has ever sent was off by one tier. That's a bug worth landing regardless of how the count resolves. On the count itself: a proto enum having five values doesn't mean every model exposes five. The decisive test is the one @ken99999 introduced — capture the app's own task and read the integer it sends. In the Narwal app, set suction to Super puissant, start any clean, then capture with "Précision de la couverture" — this may be the last unknown fieldThis is the finding I'd put first, and I don't think you realised what you were looking at. There is exactly one field in the app's clean payload that we cannot account for. From @ken99999's capture on a Flow 2 (#25), decoded against #49's builder with matched settings: Everything else matches. Now look at what you found: a two-value toggle (Standard / Méticuleux), appearing in both the suction and mopping tabs, therefore not a mop parameter — sitting next to a field whose observed value is 2. If Standard = 1 and Méticuleux = 2, that's a complete explanation, and it means the app was in Méticuleux when @ken99999 captured. This is testable in one step, and it would close the question: capture One correction to your guess, though: it's almost certainly not @ken99999 — this narrows the ask I made in #25. Rather than varying every setting, the one to try is the coverage-precision toggle. Separately: your report drove How to run the captures@shin906710 — I referenced
Post the bytes rather than your decoding — I'd rather diff them against the two payloads above myself, and the field names in this protocol have already been misread more than once. |
|
@sjmotew — pulled the answer straight out of the APK (blutter decompile of the arm64 libapp.so; the generated protobuf BuilderInfo in package:app_protol/proto/core/CleanTask.pb.dart reconstructs the full message). CleanParam: message CleanParam { Tag 8 has a name: overlapLevel. Your Standard=1 / Méticuleux=2 guess is exactly right — OVERLAP_LEVEL_NORMAL=1, OVERLAP_LEVEL_DENSE=2 — and @ken99999's app was in Méticuleux when captured. The A/B capture is now confirmation rather than discovery, though still worth running to see whether the robot honors it: in my Flow 2 testing, omitting it made no observable difference (hence #50 not sending it). Suction: the proto is unambiguous — one-based, five tiers, MUTE=1 … SUPER=5, so master's zero-based four-tier scale was indeed off by one. Also worth noting: the Flow 2 capture itself carries fanLevel=4 (DEEP) on the wire, so tier 4 is sent by the app on at least that model. Whether the AX26 UI can reach it is still the open hardware question your capture test would answer. Cross-checked the reconstructed schema against both payloads above: mode=4 (SWEEP_MOP_SYNC), fanLevel=4, mopStrengthLevel=1, mopHumidity=3, sweepMopSyncTime=3, plus overlapLevel=2 in the app's. Every byte accounted for — no unknown fields remain in CleanParam. |
To be honest, I suspect the app uses Canadian French across all French locales. In France, "vadrouille" isn't used for "mop" or "mopping" (we'd say "serpillère" or "lavage"). That said, since the app currently uses "vadrouille", it might be best to stick with it for consistency so users aren't confused by different terminology. |
|
Three captures of the Narwal app's own clean command on a Freo Z10 Pro (firmware CAPTURE: Succion "super puissant" + "standard" CAPTURE: Succion "puissant" + "standard" CAPTURE: Succion "puissant" + "méticuleux" Let me know if you need anything more. Regards, |
@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
Exposes the room-clean parameters decoded in #49 as Home Assistant controls, so users can set the work mode, suction, water, mop strength, and pass count and have them applied to room cleans.
What it adds
All are backed by a single
CleanSettingsdataclass on the coordinator (the source the clean-start path reads).async_clean_segmentsthreads them intostart_rooms; water and fan also apply live while the robot is cleaning.Persistence
Values persist across restarts via HA's
RestoreEntity(RestoreSelect/RestoreNumber/RestoreEntity) — set once and kept. No manual storage;restore_statereplays the last value intoCleanSettingson startup.Labels
Option labels use the app's user-visible wording (same convention as #49's fan labels). Two caveats:
CleanParamfield the app otherwise auto-manages).Testing
pytest tests/— all green (167).