b8ef430ed4a72daa05bf052d087cfb11a0e9a7b8
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0dd8bfb5fc |
laundry: fix four findings from the final review of guided setup
The one that could run the wrong program: guided setup accepted a name another program already had. The duplicate check only compared names within the form it was handed, and guided setup submits one program at a time, so it never saw the others. Two programs sharing a label resolve to whichever option comes first, so picking the second would have run the first one's payload -- the exact failure the check exists to prevent, working correctly in the bulk form and blind in the guided one. The taken set now includes every other named program, excluding the slot being edited so confirming an unchanged name doesn't reject itself. The rest: - The timeout screen's copy interpolates the same counters as the other two but was shown without placeholders, so it rendered literal braces. - The probe reports whatever payload is loaded, while observe() declines one whose slot the device doesn't advertise. Guided setup could reach the name form for such a slot, take a name, and silently discard it -- there is no record to hang it on and no payload to replay. It now waits instead. - The prefilled Download course came from the raw candidate list while the dropdown filters to courses the appliance still offers, so a stale candidate prefilled a value the selector rejects and the form failed validation on something the user never chose. |
||
|
|
9652a14f64 |
tests: stop the cloud write test leaving a debounced refresh behind
A write schedules a debounced refresh, which polled through a fake session that only implements post(), crashed on the missing get(), and left its timer running past the end of the test. CI's lingering-timer check caught it; it passed locally only by timing luck. Stubbed the same way test_coordinator_send_command's fixture already does, which is what this test should have copied to begin with. |
||
|
|
bb20eea191 |
laundry: a payload sitting there is not evidence of when it got there
The Download-course candidate came from "Course_ read while a non-sentinel one-time payload is loaded". On the first rep after any restart that is indistinguishable from a payload left over from a previous run, so an appliance holding cloud payloads while sitting on an ordinary course proposed that ordinary course as the Download one. Accepting the prefill would then make selecting a downloaded program start, say, a cotton wash. Only a transition actually watched counts now. "Never observed" is a distinct state from "observed, nothing loaded" -- absent-then-loaded is a genuine selection and still counts -- so a restored store deliberately re-enters the unobserved state, since a restart cannot tell the two apart. Payloads are still learned from that first rep either way; which programs exist is device fact regardless of when they were loaded. It is only the inference about which course means Download that needs the timing. Both corpus dumps taken off the Download course show the appliance clearing its one-time token to the FFFF sentinel, so this may never fire on these boards. That is a reason to expect them to behave, not to depend on it. |
||
|
|
0fbac7f14d |
laundry: guided setup left download cycles unselectable, and cleared the course
A program is only offerable once it has both a name and the Download course code that goes in the Course_ token. Guided setup collected names and never asked about the course, so a user could walk all nine programs, watch every name save, and end up with nothing in the cycle list. Worse, it actively cleared the course. _apply_cloud_course_names read download_course out of the submitted form; the guided name form has no such field, so it passed None and apply_cloud_courses stored that. Naming a program therefore removed every previously-named program from the list. Traced on the fixture: 87 -> name one -> None -> nothing offerable. Two changes. apply_cloud_courses now defaults download_course to "leave it alone" rather than None, so silence can't be mistaken for a clear, and the guided path forwards the field only when its form actually carried it. And the first guided name form now asks for the course, prefilled from what was just observed, dropping the field once confirmed. Asking there rather than up front is deliberate: it is the first moment there is evidence to prefill, since the user has just loaded a program and the course showing alongside it is the Download one. That makes the walk stand on its own, which is the whole point of offering it as the primary path. The bulk form's course dropdown now shares the guided one's builder. Translations for the guided-setup strings are in for cs/de/es/it/ko/nl, matching the vocabulary the earlier pass established. The new field on the name form reuses each locale's existing label from the bulk form rather than adding an untranslated string. |
||
|
|
78a545341b |
laundry: show the names assigned so far during guided setup
A nine-program walk is hard to keep your place in. The counter alone doesn't say what you've already done, and the programs still to do can't be listed -- they're unnamed by definition, which is the whole premise. So "named so far" is the only orientation available, and it now appears on all three guided screens. It doubles as duplicate avoidance on the naming form: a repeated name is rejected, so seeing the others while typing beats being bounced afterwards. Listed in the appliance's own advertised order rather than the order they were named -- a stable order either way, and not numbered: whether it matches the dial is plausible but unverified, and implying it would be worse than saying nothing. |
||
|
|
a4981f40a2 |
laundry: guided setup for download cycles
Naming downloaded programs from a list of hex slot ids was the weak part of this feature: it asked about programs in the abstract, long after the user had touched the appliance, and the per-slot fields rendered as raw keys because Home Assistant can't translate dynamic ones. Guided setup asks in the moment instead. It waits on a progress step while the user selects a program on the appliance, then asks for that one's name -- so the field is a single static key, and "which one is this?" is answered by the user having just turned the dial to it. The prompt also shows the appliance's own reported remaining time, which differs per program and is device-reported rather than decoded. Two things it has to get right: - It waits for a *transition*, not a state. After naming a program the appliance is still sitting on it, so a loop keyed on "a known slot is loaded" would re-offer the same one forever. Each round baselines on whatever is loaded when it starts. - Re-selecting an already-named program is not an error -- it is how someone checks their work -- so it gets the existing name pre-filled and the counter deliberately does not move, rather than a rejection. Names persist as they are entered rather than batching to the end of the flow, which makes closing the dialog a clean "save and exit" with nothing pending to lose, and makes the flow resumable: reopening picks up from the store. async_remove cancels an in-flight round, so walking away actually stops the probing instead of holding the session lock every few seconds until the timeout. /course/vs/0 is cold-tier, so passively a selection can take a whole poll interval to appear. async_probe_cloud_courses live-reads it through the normal apply path, keeping learning and persistence in one place. The bulk form stays, under its own step, as the way to rename things later -- which guided setup is bad at. New strings ship in English in every catalog and need translating. |
||
|
|
cef187b7af |
diagnostics: report discovered cloud cycles in full, names included
Trimming the names out of the dump last commit was the wrong call. Half of what goes wrong with this feature is a configuration question -- which programs got named, which Download course was confirmed, whether a payload was ever captured for a slot the device advertises -- and none of that is answerable from the payloads alone. A report saying "my download cycle isn't showing up" is exactly the case that needs it. The names are still the user's own words, so this block stays the one place they appear; they reach a dump only because its owner chose to download and share it. `resources` is unaffected either way -- it goes on reporting exactly what the appliance said, via device_resources(). |
||
|
|
2265c52c77 |
laundry: apply cleanup review to the cloud-cycle branch
Four parallel reviews (reuse, simplification, efficiency, altitude). The two that change behavior: - observe() could report "changed" on every poll forever, rewriting the config entry each time. If both tokens name the same slot with different payloads -- a downloaded program with its settings tweaked for one run is exactly that shape -- each pass wrote the default's blob then the one-shot's over it, so neither was ever already stored. On the SD-card installs this integration runs on, sustained entry rewrites are the one cost here that bites. The end state is stable, so "changed" is now start-vs-end, not per-assignment. - The write path copied every tracked href to read one rep, walking past the accessor added to avoid exactly that. New entity_rep() does the merge for a single href; cycle_write drops the resources parameter it never used. Structure: - device_resources() is a second accessor giving the pure device view, used by diagnostics and the debug read service. That deletes strip_synthetic, the _SYNTHETIC_KEY_PREFIX convention and the redact filter added last commit: "a dump is what the device said" is now which method you call rather than something every future exporter has to remember. - apply_cloud_courses() is the single mutation path. The flow was reaching past the coordinator into the store and relying on a later call to persist and invalidate for it; nine names are also now one entry write, not nine. - option_value/hex_pairs move to capabilities/common.py. The duplicate's stated reason -- that the coordinator shouldn't import from registry.capabilities -- was simply false; it already does, and so does learned.py. The real constraint is narrower: laundry.py imports cloudcourse, so the reverse would be a cycle. Dropped rather than kept: - The cloud-vs-translated-local-course name check, and catalog. translated_state_labels with it. The catalog this process can read is English while the dropdown is localized in the frontend, so it rejected "Cotton" for a German user seeing "Baumwolle" and missed the real collision when they typed "Baumwolle" -- wrong in both directions outside one locale, against an outcome option ordering already makes deterministic. The checks that survive compare strings that are the same in every locale: the user's own names, and the device's personal-course labels. - stored(), clear()/forget_cloud_courses(), blob(), download_course() -- no production callers. stored() was a template artifact whose docstring described a caller that cannot exist here. Diagnostics gains a cloud_courses block, which the store was missing next to learned_modes -- payloads and which slots are named, but not the names themselves, since those are the user's words and dumps get pasted publicly. Kept against one reviewer's advice: option_tokens (two others called generalizing option_write the right direction) and select._display's uncatalogued branch, which names a condition the old fallback-is-None proxy only got right by accident. Deferred: making the store per-subdevice. It is MAIN-only today and no device seen advertises cloud programs elsewhere; the limitation is now documented where it is made. |
||
|
|
b921bdbb28 |
laundry: fix six issues from review of the cloud-cycle branch
Also drops appliance-specific wording from the new user-facing strings. The setup step said "your washer" and told people to "turn the dial", which is wrong for the DW5000C dishwasher that advertises the same tokens. The two that could have caused a wrong wash cycle: - The Download-course candidate was counted on every poll that saw a loaded one-time payload, not on the polls where one was actually loaded. Since a stale token is never evicted, it keeps being reported through however long the appliance then sits on some ordinary course -- so "most frequent" ranked by dwell time. Reproduced: one poll on Course_87 then 200 on Course_1B suggests 1B, and accepting the suggestion makes picking a download program start a Cotton wash. Now only a change of the payload counts, which is the moment the device is known to accept a program. - The Download-course dropdown had custom_value=True, contradicting its own comment, so a typed-in code went into the Course_ token of a real write unchecked. Off now, plus a server-side check against the device's own course list where the value is stored. Two that quietly broke things beyond this feature: - cycle_select now always supplies a display_fn (to label cloud programs), which defeated select._display's "no state table and no fallback -> return raw" exit. Every dryer, dishwasher and air dresser on an unrecognized course table would have had its options and state reshaped from '0E' to '0 E', breaking automations and recorder history. The exit now keys off whether anything actually named the value, not whether a fallback existed. - The synthetic cloud field reached diagnostics, which reads canonical_resources -- publishing user-typed program names in a dump people paste into issues, directly against the comment claiming it never could. Dropped at the redaction boundary, with a matching strip for the debug read service, which wants device state unredacted but shouldn't present our bookkeeping as something the appliance said. And two smaller ones: - The repair fired on any device advertising slots, so the DW5000C -- four advertised, none ever loaded -- got a permanent warning nothing the owner did in Home Assistant could clear. It now waits until a payload has been seen, which is the only evidence that household uses downloaded programs. - The name-collision check read only the translation catalog, missing the device's own personal-course labels, which the select renders identically. |
||
|
|
f45bd6a72c |
laundry: discover and offer cloud "Download" cycles (issue #342)
A washer whose course table includes "Download"/"Downloaded" runs whichever program the SmartThings cloud last pushed down. Those programs are now selectable from the ordinary cycle select, so a downloaded Jeans or Sports cycle can be started without giving the appliance internet access. The device turns out to enumerate them itself. `CloudExtraCourse_` on /course/vs/0 lists one byte per downloaded program, and byte 2 of a program's payload is exactly that slot id -- verified against all nine programs on the reporter's WW5000C and against the WA55A7700AV dump already in the corpus. So nothing here is hardcoded: the appliance says which programs exist, the payloads are learned by watching what it reports, and the names come from the user. That last part is unavoidable rather than a shortcut. A payload is only visible while its program is loaded, and the appliance never reports a name for one. So cloudcourse.py persists what has been seen (same rationale as learned.py's mode store), a Repairs issue tells the owner how many programs are still unaccounted for, and an options-flow step collects the names. A program appears in the cycle select only once it is both learned and named. Selecting one issues the only two-token options write in the codebase -- the course token has to switch to Download in the same write, or the appliance accepts the program token and silently ignores it (confirmed on hardware). The Download course code is learned by observation but never applied until the user confirms it: tokens in this array are replaced by prefix and never evicted, so a stale program token can appear alongside an unrelated course, and acting on that would start the wrong wash cycle. For the same reason a stale token is never reported as the running program. Also of note: - There is no single "Download" course code. The WW5000C uses 87, the WA55A7700AV uses 17 -- same Table_02. Any per-table lookup would have been wrong on one of the only two devices available to check. - Payloads are replayed byte-for-byte and never decomposed or rebuilt. Bytes 5/7/9 do decode to temperature/rinse/spin on the WW5000C, 9 for 9, and produce nonsense on the WA55A7700AV -- so that decode is written up in docs/investigations/download-cycle.md and not shipped, and the read-only sensors it would have enabled were dropped. - The store reaches the registry as a namespaced synthetic field merged onto /course/vs/0's rep at read time, so exists_fn/rep_fn/options/write_fn all see it through their existing signatures. It never enters the state cache, so it can't be polled over, written to the device, or land in a diagnostics dump. - A name that would render identically to another cycle in the same dropdown is rejected in the flow: the select maps a chosen label back to a raw value by matching display text. Non-English catalogs carry the new strings in English for now; they need real translations. |