All notable changes to WashData will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
-
Import the power history you already had (#344): Advanced - Diagnostics - Import power history replays your sensor's existing history (an uploaded CSV, or read directly, up to 14 days) through normal detection, so past cycles appear in Cycles ready to name. It runs as a cancellable background scan and shows one reviewable row per candidate (date, duration, energy, peak, shape preview); nothing is written until you confirm, it reports what it skipped and why, and re-importing an overlap skips duplicates rather than doubling them. A raw HA history is change-based (a steady 0 W emits no rows), so a naive replay produced only force-stopped junk; the stream is instead cut into activity blocks and each is replayed through its own detector. Imported cycles are a distinct third category: they shape program matching once you label them, but never touch lifetime stats, the review queue, learning or store sharing. (Also fixed three defects this exposed, which also hit community-store downloads: "Cycle not found" when creating a program from an imported cycle, the missing envelope until a later maintenance pass, and the hidden Label button.)
-
Import history reads from a date you pick, and no longer refuses a valid export: The recorder read is bounded by a start date rather than a number of days, and reaches back up to 10 years instead of 14 - a recorder configured to keep full-resolution states that long is a real setup. It walks backwards from today and stops once the history runs out, so a wide range costs nothing extra. A CSV holding a single sensor is also no longer rejected when its entity id differs from the one this device is configured with (a renamed entity, or a template sensor in front of the plug): it is read, and the review step says which sensor it used. A file holding several sensors with no match is still refused, since guessing between appliances would corrupt detection.
-
Auto-labelling now names imported cycles too: "Auto-label cycles" (button, service and confidence setting) now sweeps imported history under the same safeguards - a program is assigned only on a confident, unambiguous match - and marks such labels as matcher-guessed so a wrong guess stays visible and reversible. With no programs yet, name one or two by hand first, then let auto-label do the rest.
-
New setting: which cycles shape a program (Settings - Matching - Profile Evidence): Choose which kinds build each program's curve and matching template - this machine's cycles, community-store downloads, and imported history - so imported data can be kept for reference without shaping your programs. Unticking a kind deletes nothing and never affects statistics; changing it rebuilds every program immediately; the default is all three.
-
New setting: Smart Termination Ratio (#393) (Settings - Detection - Cycle End): The fraction of a program's average duration a run must reach before Smart Termination may fire is now per-appliance. Lower it (e.g. 0.85) on a machine whose runtime varies a lot (variable inlet temperature, sensor-dry, load-dependent) so the early finish still fires; leave it empty for the unchanged default (0.98, or 0.99 for dishwashers). It can only ever end a cycle earlier, never later, and never on an ambiguous or low-confidence match.
-
The Playground tab opens without waiting on work it does not need: Its four setup fetches now go out together instead of in sequence, and the expensive suggestion computation moved to the background, dropping the blocking tab-open from ~34 ms to nothing (more on a Raspberry Pi).
-
The Settings tab no longer spends the community store's shared read budget: Two queries were 90.7% of all free-tier reads because the "Approved / Awaiting approval" badges downloaded the whole catalog on every Settings open. The badges now resolve as two point reads; the brand and device lists load only when a picker is opened (server-side prefix search, debounced, cached); one store client is shared install-wide; and a new Refresh catalog button (gear) drops the cache. List payloads now project only the fields the UI shows.
-
The Store tab now shows the appliances it should: Browse listed only the ~6 approved devices per type. It now includes pending entries, scoped to your brand plus this device's type, with your own model first (tagged Yours), then models with shared programs, then the rest; when no appliance is declared it asks for one. (Also relabels the brand-only search box and corrects the wiki/README flow.)
-
A release can no longer ship a stale panel bundle: A new
devtools/release_check.shverifies artifacts, WS types, version agreement, translations and the fast suite in one command; a Checks CI workflow verifies the generated artifacts on every push and Release Preflight enforces version agreement on a tag; and the local deploy script rebuilds first. (Adds the documented-but-missingrequirements-dev.txt.) -
Confirming an appliance counts again: The post-confirm read-back was served from the same one-hour cache the confirm had just written, so the count never reached the promotion threshold; the cache is now dropped between the write and the read-back.
-
Smaller store and panel fixes: a failed brand search backs off instead of retrying ~4x/second; a failed panel refresh shows the real error instead of a collapsed
Object; diagnostics report which frontend bundle is being served;./run_tests.shruns again (the render-smoke harness is now ESM); and store document ids are percent-encoded. -
Panel and card are minified and served pre-compressed: esbuild produces
*.min.js(served only while its recorded hash matches the source, else the readable file is served instead) plus an atomically-rebuilt.gzsibling. Panel 766 to 522 KB (-32%) to 131 KB gzipped (-83%); card 49 to 29 to ~8 KB. -
The learning / auto-tune pass no longer runs while idle (#394): It ran on a 5-minute timer around the clock although appliances are idle ~98% of the time, and it trained its cadence model on the idle reporting rate - oversizing the watchdog / timeout / match-interval suggestions. It now runs only during an active cycle (the detector still receives every reading), so those suggestions reflect real in-cycle timing.
-
Shipped defaults no longer trip the panel's own conflict checks (#396): Three default pairs broke rules the panel enforces. Fixed: the learning-vs-match rule was inverted (the ladder is unmatch < match < learning < auto-label); the per-appliance 2 s sampling for wet appliances was never wired up, so washers and dishwashers ran at the coarse 30 s default, and the watchdog / start-debounce defaults now derive from sampling; and a stale watchdog-default comment. The panel now validates against the device-resolved default, and a 3.9 to 3.10 migration heals older entries seeded with the old values.
-
Cycle graphs no longer drop the load peak when thinning the curve (#395): Decimation kept every n-th point, so a single-sample peak or a one-sample 0 W dip could vanish (measured in 14 of 49 curves). It now keeps each interval's minimum and maximum at the same payload size, and reports how much it thinned so a thinning gap is not mistaken for a sensor dropout.
-
A running wash is no longer cut in half at a shorter program's length (#364): The matcher can lock onto a shorter look-alike early in a longer wash, close the cycle at that length, and record the rest as a second cycle. WashData now checks whether the appliance is actually finishing (trailing power vs what the matched program draws at its own end) and scores a look-alike against its curve truncated to how far the wash has got. Both only ever delay an early finish, and program matching is provably byte-identical; across the reference library they catch 47% of splits for 6% of normal ends taking the slower path.
-
The "profile match threshold" setting does something again: Stored but never read for the whole 0.5 series, so raising it (a documented workaround) silently did nothing. It is now wired to the confidence gate it was always described as controlling; the default is unchanged.
-
The Playground could report an imported cycle as unmatched: It built its candidate programs from a narrower cycle set than the live matcher, so a program seeded only from imported history was left out. It now uses the same evidence set as the live matcher.
-
Time remaining no longer collapses to a minute during a dishwasher's passive dry (#386): A quiet drying window matched the envelope's trailing all-zero pad, pulling the scan to the end and the countdown to the 99% clamp. An uninformative window now declines to guess (the clock estimate takes over) and the zero pad is no longer a candidate alignment. Thanks to @andrei-marinache for the diagnosis and initial fix in PR #390.
-
Time remaining keeps counting down through a silent drying tail: The estimate was recomputed only when a reading arrived, so it froze when a publish-on-change plug went silent for the ~30 min dry. The active-cycle watchdog now refreshes the (wall-clock) estimate each tick, without injecting a reading or ending a cycle.
-
Envelope rebuild no longer OOM-kills Home Assistant (#388): A densely-sampled (1 s) cycle sized an uncapped DTW cost matrix at 1.81 GB, killing HA Core on a 4 GB Pi. A
MAX_ALIGN_GRID_POINTS = 2000cap (~32 MB) now bounds every alignment grid, with a pre-flight guard on the matrix allocation. -
The dashboard card no longer intermittently fails to load with "Custom element not found" (#384): The card's static route was registered as a fire-and-forget task after the Lovelace resource, so a browser could fetch the URL before the route existed and 404; a genuine registration failure was also swallowed. The route is now awaited before the resource is published and fails loud.
-
The card / panel cache buster always changes between releases: It came only from file mtimes, which some package managers preserve, so a URL could repeat across releases. It now folds in the
manifest.jsonversion (and reads mtimes in nanoseconds so same-second rebuilds differ). -
Help bubbles in the cycle Review tab are no longer cut off (#385): Fixed-width "i" popovers were clipped when their icon sat near a container edge; they now nudge back inside whichever box would clip them. Thanks to @jbvs01 for the report.
-
Reverting a setting that had never been saved no longer breaks the integration (#389): The Revert button sent back
null, which stored asNoneand crashed setup (TypeError) on every restart. A null option is now dropped on save and on import (and healed by migration for entries already broken). Thanks to @DennisGoss99 for the diagnosis and fix in PR #391. -
The device pill now shows calibrated tuning suggestions too: The pill counted only the classic suggestions while the Settings tab counted classic plus the Calibrated (ML) ones, so an all-calibrated device showed no badge. The pill now uses the same arithmetic (cached per device) and stops counting muted or no-op suggestions.
-
No more blocking-call warning when a cycle is matched (#328): The ML bridge imported its baseline model lazily on the event loop on the first match after a restart. The models are now preloaded once at setup on Home Assistant's import executor. (A latent inline-read fallback in the voice intents was fixed in the same sweep.)
-
Renaming a profile no longer appears to drop it from its group: The Profiles tab re-fetched only the profile list after a rename, not the groups, so the renamed member vanished from its group card. The tab now re-fetches both.
-
The Playground gets a settings control panel: load live settings, save/load presets, publish single values (
playground.py,profile_store.py,ws_api.py,ws_schema.py,const.py, panel,playwright-tests/): The Playground's detection settings block now opens with a Settings source panel. Every field starts from the device's live integration settings - read back from the running detector and matcher, so device-type defaults (a dishwasher's off-gap, a dryer's thresholds) are the real values the integration uses rather than a generic schema default, and a field that was never explicitly configured is no longer blank. A status line says either "Matches live settings" or "N changed vs live settings", and Load live settings re-reads them and discards every sandbox edit. Setups can now be saved as named presets per device and loaded back later, so you can keep "winter", "quiet nights" or "before/after tuning" side by side and switch between them without retyping (presets are stored with the device, survive restarts, are visible from any browser, and never touch live configuration; up to 30 per device). Finally, each edited setting that is a real integration option gets its own ↑ publish button that saves just that one value to the device, alongside the existing bulk "Publish N to integration" - the Stage 2-4 matcher scoring knobs (correlation weight, DTW blend, duration/energy weights and scales) deliberately have no publish button because they are sandbox-only experiment constants with no configuration option behind them. Previously the bulk save could write those sandbox-only keys into the device's options as dead entries, and it also cleared unrelated sandbox edits after saving; both are fixed. -
Playground can now load the auto-tuner's current suggestions as a starting point (
ws_api.py,ws_schema.py, panel): The Playground's Settings source control panel now shows a Load suggested (N) button when the auto-tuner has recommendations that differ from the current Playground values, and a Load Calibrated (ML) (N) button when ML-calibrated suggestions are also available. Clicking either stages those values as Playground overrides without touching the live integration — the same sandbox-only staging as a manual edit — so you can immediately run a simulation to see what effect the suggestion would have before deciding whether to publish it. Unlike loading a preset (which replaces all values), suggestion loading is additive: it only overwrites the keys covered by suggestions and leaves any other staged edits untouched. Both suggestion sets are computed on the server when the Playground tab opens, so the button labels already show the correct count with no extra round-trip on click. -
The bundled dashboard card is rebuilt as an adaptive, zero-config card (
www/ha-washdata-card.js,translations/panel/*.json,playwright-tests/): The old card was a fixed 56 px tile whose default configuration read the program and progress from sensor attributes that no longer exist on the state sensor (program/cycle_progress), so out of the box it showed only an icon, title and sub-state - program, percentage and time each needed a hand-wired entity. It is now a single adaptiveha-washdata-cardwith alayoutoption:tile(default, backward compatible) - the compact tile, now with a state-coloured progress bar and an adaptive detail line;detail- a richer card with a large time-remaining readout, a live phase chip, projected energy and cost, current power, an optional live power sparkline, and an amber "running long" overrun accent (visible-only, never a notification); andglance- several appliances as one at-a-glance list of state-dot rows. Point the card at any one WashData entity (or the discovered device) and it auto-wires every sibling - program, time remaining, progress, power, phase, and the pause/resume/end-cycle/record buttons - from the device registry by translation key, so no manual entity wiring is needed (the old per-entity overrides still work). State colours now come from the integration's singleSTATE_COLORSsource, so Paused, Clean, Finished and Interrupted are visually distinct, and an opt-in action-button row (pause, resume, end cycle, start/stop recording, program picker, open panel) wires straight to the existing button and select entities. The card's private, machine-translated string table has been removed: its chrome now flows through the same panel-translation pipeline as the rest of the UI (localised into all 35 languages), and appliance/program/phase values localise through Home Assistant directly. The detail layout also reserves the correct height in the new sections dashboard view, so its action buttons and sparkline are no longer clipped off the bottom. Existingtype: custom:ha-washdata-carddashboards are unchanged (they default totile). New Playwright coverage exercises all three layouts, auto-discovery, the progress bar, state-gated buttons and the editor schema. -
The dishwasher end-of-cycle drain wait is now configurable (#379) (
const.py,cycle_detector.py,manager.py,playground.py, panel): Many dishwashers finish with a long near-silent passive-drying phase followed by a short final drain. WashData releases the end-of-cycle drain wait once the cycle has passed its expected duration and then been quiet for a fixed window (previously a hard-coded 600 s). On a machine whose silent stretch is longer than that, the wait effectively collapsed to "release at expected duration", so as the true cycle length drifted up (colder winter inlet water = longer heating) the final drain landed just past expected and was missed - and because the recorded duration then fed back into the rolling average, the learned duration could no longer track the drift. A new dishwasher-only Passive-Dry Quiet Release setting (Settings -> Dishwasher, default unchanged at 600 s) lets you widen that window so the drain is caught and the profile can follow the drift. It only ever shortens the wait relative to the internal 30-minute end-spike cap, never extends it. Requires the detector to run the dishwasher path, fixed in #378. Thanks to @andrei-marinache for the analysis and closed-loop simulation. -
Repeat the "unload the machine" reminder until you open the door (#374) (
manager.py,const.py, panel): The post-cycle unload reminder was a single nag. A new opt-in "Repeat Until Door Opens" toggle (Triggers & Door -> Unload Reminder) re-sends it every "Unload Nag Delay" minutes until you open the door, tap a "Stop reminding" button on the notification itself, or the safety limit of 48 repeats is reached (approximately two days at the default one-hour interval). The reminder is delivered as an actionable, sticky notification on Home Assistant companion-app (mobile) targets so it can be dismissed with one tap; opening the door still clears it as before, and on non-mobile targets the door-open or the 48-repeat limit are the stop conditions. Default off, so existing setups keep the single reminder. Like the reminder itself, it requires a Door Sensor Entity (the Clean state that drives the reminder only exists when a door sensor is configured). -
Adjustable panel font size (
panel,store_account.py): The panel now has a font-size slider in My Preferences (the header gear menu, under Display). If the text feels too small - or too large - drag it to scale the entire panel up or down; it previews live as you drag and is remembered for your Home Assistant account on that device. Because the panel's layout is relative, everything scales together cleanly. The value is clamped to a safe range so it can never break rendering. -
Setup and settings dialogs now link to the panel (
strings.json): The initial setup, reconfigure, and options dialogs now include a direct link to the WashData panel (/ha-washdata), so it is obvious where to go to view cycles, tune detection, and manage profiles after adding a device (it also lives in the sidebar). -
Door-sensor support for dishwashers that auto-open at the end (#342) (
manager.py,const.py, panel): Many modern dishwashers pop the door open at the end of the cycle to dry (AirDry and similar). WashData treated that door-open as a mid-cycle pause and stranded the cycle in "paused by user" until you forced it to end. A new "Door Opens Automatically At End" option (with a "Door-Open End Dwell", default 60 s) changes this: on such a device a door-open on a running cycle arms a short dwell instead of the sticky pause, and if the door stays open past the dwell WashData finishes the cycle (completed) - while a brief open to add a dish closes before the dwell and is ignored. Opt-in and needs a Door Sensor Entity; devices without the option keep the previous pause-on-open behaviour. -
Smarter auto-tune on anti-crease devices, plus a per-setting mute (#343) (
suggestion_engine.py,profile_store.py,learning.py,ws_api.py, panel): The auto-tuner kept proposing stop/start thresholds just above the anti-crease tumble-pulse baseline - values that break end-detection - and it re-proposed them every cycle even after you dismissed them. Five algorithmic gaps are now fixed and a mute UI is added: (1) Stop/start thresholds are derived from the main cycle only - the post-cycle tumble-pulse tail is stripped before computing the minimum-active-power statistic (for both the single-cycle and multi-cycle paths). (2) Themin_powernoise-gate suggestion is now also stripped so it cannot be dragged below the real operating draw by the anti-crease baseline. (3) Theoff_delaysuggestion is now safe: the pause-based path strips anti-crease inter-burst quiet periods (up to 180-240 s on Miele/Bosch) before counting genuine intra-cycle pauses; the cadence-fallback path (p95 × 5) is suppressed entirely when anti-crease is enabled because the result often exceeds the burst interval and resets the end timer on every tumble burst. (4) The per-cycle end_energy suggestion is removed from the single-cycle path where it was hardcoded to 0.05 Wh - a value that falls below the anti-crease baseline energy accumulated over the off_delay window, making the end gate impossible to satisfy; the multi-cycle batch path derives a cycle-proportional floor from real false-end events and is unaffected. (5) Each suggestion can now be muted with a mute button on every suggestion card; muted settings are never re-proposed until you reset them, with a "N muted - Reset muted" banner in Settings. -
Every notification is now written to the log (
manager.py): WashData now records a log line each time it notifies you, so you can audit exactly what was sent and where. Discrete notifications (cycle start, finished, milestone, unload reminder, pause, and the rest) log at INFO asNotification sent (<event>), naming just the event. The delivery target (eachnotify.*service,actions, or thepersistent_notificationfallback) and the rendered message body are logged separately at DEBUG asNotification sent (<event>) via <target>: <message>, so the target list (which can contain a person's mobile-app entity id) and message text stay out of the INFO logs users paste into bug reports. High-frequency live-progress countdown updates - which are repeated in-place edits of one persistent card rather than distinct alerts - also log at DEBUG so they don't drown out the meaningful lines; enable debug logging forcustom_components.ha_washdatato see the target/message detail and live ticks. Notifications held back by quiet hours or the "notify only when home" presence gate log a DEBUG "deferred" line when queued and then the normal "sent" line when the hold clears and they are actually delivered.
-
Calibrated (ML) suggestions now surface as "tuning suggestions available" when you open Settings (
www/ha-washdata-panel.js,playwright-tests/): A device whose only recommendation was a Calibrated (ML) value - shown as the "Calibrated" pill beside a field, for example an off-delay calibrated from the machine's own cycles - lit the light-bulb dot on the Settings tab but then, once you opened Settings, showed no "N tuning suggestions available" banner and no dot on the section that held the suggestion. Only the classic (Observed) engine's suggestions counted toward those two indicators. The Settings banner and section-navigation dots now count Calibrated recommendations too, matching the tab bulb and the existing "Show only" filter, so the in-page surfacing is consistent. The banner's Apply all and Dismiss actions still appear only when there is a classic suggestion to act on (Calibrated values are applied individually with their own Use button); the shared key computation is factored into one helper so the four call sites can no longer drift apart. -
Auto-tune sizes the Min Off Gap from what your cycles actually need, not from how fast you reload (
suggestion_engine.py,devtools/min_off_gap_eval.py,translations/panel/*.json): Min Off Gap is the quiet time that has to pass before WashData treats power coming back as a new cycle rather than a continuation. It is squeezed from two sides: it must be long enough to sit through the longest quiet stretch inside a programme (a soak phase, or a dishwasher's passive drying before its final drain), and short enough that a load you start soon after the last one is still recorded separately. The recommendation was being computed from the second of those - it landed at 80% of the shortest gap you had ever left between two loads - which put it right against the point where two loads merge into one record, without any evidence that much bridging was needed. On one real washing machine that works out at about 29 minutes against a measured need of 21 and a real turnaround of 3, so back-to-back washes would be recorded as a single cycle. It now measures the quiet stretches your programmes actually contain and sizes the value from the longest of them plus a minute, using your shortest turnaround only as a ceiling. If your machine needs to bridge more than your turnaround allows, no value can do both and no recommendation is made, leaving your current setting untouched. Every recorded cycle across the reference data set was replayed through the real detector at the proposed values to confirm no cycle gets split and no two loads get merged. For a dishwasher with a long drying phase this typically replaces the blanket 1 hour default with roughly 36 minutes measured from the machine itself. -
Auto-tune no longer proposes an Off Delay and Watchdog Interval that stall end detection (
suggestion_engine.py,translations/panel/*.json): Two of the recommended values were sized far above what the measured data supported, and on a dishwasher the result was cycles that stayed "running" long after the machine had actually finished. (1) Off Delay is derived from the longest genuine pause inside your cycles, but the measured value was then clamped up to a fixed per-appliance minimum (30 minutes for dishwashers) that exists only as a starting guess for machines with no history yet. A dishwasher whose real pauses topped out around 4 minutes was still told to use 30. That mattered because Off Delay also sets how far back WashData looks when checking whether the appliance has gone quiet: over a 30-minute window, the small standby ticks a dishwasher emits minutes after it finishes were counted as activity and kept the cycle open. Once enough real pauses have been measured, the measurement now wins (floored at the generic 3 minutes); appliances with no measured pauses yet keep the conservative per-appliance value, and cycles are still held together by Min Off Gap exactly as before. (2) Watchdog Interval was suggested at three times the typical gap between sensor updates. The watchdog is only how often WashData wakes up to check on a running cycle - it never decides on its own that a cycle has stopped - so the multiplier bought no safety and only delayed the checks: a plug reporting every 30 seconds was told to look every 3 minutes. It is now suggested just above the observed update gap (about 1 minute for a 30-second plug), matching the documented default. -
Greek, Japanese and Korean panel translations repaired; the Playground tab is named consistently in four more languages (
translations/panel/*.json): A sweep of the short UI labels in the Greek, Japanese and Korean panel files found a large amount of old machine-translation residue where the translator had picked the wrong sense of an ambiguous English word, and 185 strings are corrected. Some were actively misleading: "Save" read as rescue (ko) or out/off (el), "Dismiss" as fire an employee (el, ko), "Idle" as lazy (ko) or unemployed (el), "Envelope" (the statistical band around a program) as a mail envelope (el, ko), "Recording" as audio recording (ja, ko), "Cleanup" as spring-cleaning the house (ja, ko), "Live Power" as live strength (el, ja), "spin" as rotation rather than the spin/dry phase (ja, ko), and "Export"/"Trim"/"Edit"/"Clear" taken in trade, grooming, publishing and "obvious" senses respectively. Unit markers lost in translation are also restored (a trailing "(s)" that had become a plural marker instead of seconds, and "min" read as minimum instead of minutes), along with a number of buttons left as dictionary infinitives, and Japanese now uses one consistent word for a program profile instead of two. Separately, the Playground tab was named with two different words within the same screen in Swedish, Icelandic, Estonian and Finnish (the tab label said one thing, the heading another, and two of them read as a children's play park); each language now uses a single term conveying "test area" throughout. -
Grouped program variants that differ by temperature or spin are told apart more reliably (#334) (
profile_store.py): When you group near-duplicate profiles (for example "Cotton 40" and "Cotton 60", or a 1000 vs 1400 rpm variant), WashData identifies the program from the group and then picks which member you actually ran. That member pick compared the cycle's whole-cycle mean power and peak against each member - but on real machines the peak is just the heating element's roughly constant draw, so it is nearly identical across every temperature and spin variant, and mean power is diluted because a hotter wash also runs longer. The member pick now compares integrated energy (the area under the power curve, which WashData already records), which separates temperatures cleanly because a hotter wash spends far more energy heating water. Measured on 196 real washing-machine cycles from the community catalogue, this lifts leave-one-out variant-pick accuracy from 63% to 73%, with the program-level match results left byte-for-byte identical. Washing machines and washer-dryers now also use this energy-based comparison at the main program-matching stage itself, so temperature and spin variants are told apart better even when you have not grouped them (measured on trusted community cycles: about +3 percentage points of matching accuracy with fewer false matches). Dishwashers and dryers deliberately keep the previous behaviour, where programs are already well separated by their length. -
The matcher callback no longer feeds a placeholder result into the detector on every tick (
manager.py,cycle_detector.py): The detector's match callback contract is "returnNoneto mean I have offloaded the real match to an async task and will callupdate_matchlater." The manager's wrapper documented that it "Returns: None" but actually returned the placeholder tuple(None, 0.0, 0.0, None). Because a non-empty tuple is truthy, the detector fed that placeholder straight intoupdate_matchon every match tick - even mid-cycle with a profile confidently matched - which (a) logged a spuriousupdate_match: invalid raw_expected_duration 0.0 (<= 0)debug line every time, and (b) momentarily reset the detector's_last_match_confidenceto 0.0 in the gap before the real async result arrived (a value Smart Termination reads). The wrapper now returnsNonein the async-offload and empty-reading cases, so only genuine synchronous results (the manual-program override) reachupdate_match. The stray debug line stops and match confidence stays stable between updates. -
Non-washing-machine appliances now run their own detection logic (#378) (
manager.py):CycleDetectorConfigcarries adevice_typethat the detector branches on in ~16 places (dishwasher end-spike wait,keep_tail, the passive-drying deferral, the 1800 s floor, and the rest of the issue-#43 machinery), but the manager built the detector without passing it and never set it on reload - so the field kept itswashing_machinedefault and every dishwasher, dryer, washer-dryer, bread maker and pump silently ran the washing-machine detection path. The manager's owndevice_typewas correct (its ghost-cycle suppressor, phase naming and per-device defaults all worked), and the detector unit tests construct the config directly with the right type, which is why the wiring gap went unnoticed. On a dishwasher the practical effect was that the cycle closed ~2 minutes early at the generic end ratio and the profile average ratcheted downward each cycle. The configured device type is now passed into the detector at construction and kept in sync on every options reload. Thanks to @andrei-marinache for the detailed diagnosis. -
A single duplicated sample offset no longer discards a whole cycle from the envelope (#377) (
analysis.py): The envelope builder rejected any cycle whose sample offsets were not strictly increasing. Stored offsets are rounded to 0.1 s, so two readings less than 0.1 s apart collapse onto the same offset - and one such duplicate pair anywhere in a two-hour trace silently dropped the entire cycle. On a fast-reporting plug every cycle could carry one, leaving the profile with no envelope at all (the caller deletes the envelope when no curve survives), while matching quietly limped along on the raw sample cycle so nothing looked broken. Exact-duplicate offsets are now collapsed (dropping the duplicate sample, not the trace) with a debug log; a genuinely out-of-order offset, which sorted storage never produces, is still rejected as before. Thanks to @andrei-marinache for the report. -
The end-energy gate can no longer silently override the stop-power gate (#376) (
www/ha-washdata-panel.js,suggestion_engine.py,cycle_detector.py): Ending a cycle passes two gates that decide the same thing in different units -stop_threshold_w(instantaneous power) andend_energy_threshold(energy over anoff_delay-long window). Because the energy gate implies a wattage (end_energy_threshold * 3600 / off_delay), whenever that implied value sits belowstop_threshold_wthe energy gate forbids what the power gate allows and the cycle can only close through a fallback path (smart termination, watchdog force-end) - surfacing only as an unexplained late completion, with the stock defaults already in this state for washers, dryers and especially dishwashers (implied 0.1 W vs a 2 W stop). The settings panel now flags this as a conflict (a new cross-parameter rule alongside the existing ones) with a one-tap fix, and the auto-tuner'sreconcile_suggestionscascade raisesend_energy_thresholdto the implied floor when a coupled suggestion would otherwise leave the pair inconsistent. The two values stay independent - only the silent contradiction is now made visible. (Also corrected a stray code comment that described the 0.05 Wh default as "50 Wh".) Thanks to @kdjkdjkdj for the analysis. -
A finished dishwasher no longer sits "running" for hours before force-stopping (#375) (
manager.py): On a dishwasher whose learned program envelope has a long near-zero "drying" tail baked in (from earlier force-stopped cycles), a genuinely-finished cycle could hang in the ending phase for hours and then close asforce_stoppedwith a badly inflated duration. Once the appliance went truly silent at the real end, the envelope alignment kept re-confirming the low-power tail and set an "auto-detected pause" lock; the early "Smart Termination" release needs the trace to map past 95% of the envelope, which was unreachable because the appliance went quiet before that learned tail ends. The lock then froze on, and every ending-phase finalize backstop (all of which deliberately respect a genuine pause) was defeated, so the cycle stayed open until the watchdog's multi-hour silence limit force-ended it (observed: ~6.7 h for a ~2 h cycle). The auto-detected pause is now released as soon as the cycle has reached its expected duration and has been continuously below the stop threshold for at least 10 minutes - the same sustained-quiet signal the ending logic already trusts - so the normal end path finalizes the cycle promptly. A real user-initiated pause is authoritative and is never released this way, and a genuinely longer program (which still has high-power phases that reset the quiet timer) is never cut short. -
Muted auto-tune suggestions are now truly silenced across restarts (
learning.py): Muting a suggestion with the 🔕 button removed it from the in-memory pending list but the backing store was only saved as part of the normal suggestion-apply flow. When every pending suggestion was muted, the apply flow was skipped entirely, so no save was triggered and the muted suggestions re-appeared from disk on the next HA restart. The pruning path now explicitly schedules a store save when it removes all suggestions, so mutes persist correctly. -
Import payload settings are no longer silently dropped or fatal when the device has reference cycles (
profile_store.py):async_import_data_selectiveparsed the import metadata (entry_optionsand other top-level fields) from the payload before looping over the device's existing reference cycles to build a dedup set, and the loop variable was namedmeta- the same name as the outer import metadata. After the loopmetaheld the cycle-level meta of the last reference cycle (which has noentry_optionskey), someta.get("entry_options")returnedNoneand the selected settings were silently discarded. When the last reference cycle had a non-dict meta field the call raisedAttributeErrorat exactly the point after the import data had already been saved to disk, so the caller saw an error banner on a successful import. The loop variable is renamed so the outer payload metadata is never overwritten. -
Exporting the device catalog no longer races with concurrent store mutations (
ws_api.py): Theget_export_inventoryhandler ranprofile_store.get_export_inventoryin a thread-pool executor, which iterated overprofile_store._data(profiles, cycles, groups) from a worker thread while the event loop could be concurrently mutating the same dicts - producingRuntimeError: dictionary changed size during iterationor silently skipping/double-counting items. Because the scan is pure Python (no I/O, no GIL release), the executor wrapper provided no benefit and only introduced the race. The call now runs directly on the event loop. -
Expanding a cycle group in the export/import wizard no longer accidentally selects all cycles in the group (panel): The expand-to-reveal-individual-cycles button (
▸) was nested inside the group's<label>element. Browser click-propagation rules cause a click on an interactive descendant of a label to also activate the label's associated checkbox, so clicking▸simultaneously toggled every cycle in the group. The button is moved outside the label, which now wraps only the checkbox and the group name span, so clicking the expand arrow is side-effect free. -
Stale door-end dwell timer no longer fires during a subsequent cycle (#342 edge case) (
manager.py): When an auto-open dishwasher cycle ended while a door-open dwell was already armed (the appliance opened the door during the running phase, as intended), the dwell cancel hook was not called from_on_cycle_end. If a new cycle started within the dwell window (typically 60 s), the stale dwell would fire and calluser_stop()on the new cycle. The dwell is now cancelled as part of cycle-end cleanup, matching the pattern used for the watchdog and other active timers. -
Splitting or merging cycles now always stores UTC timestamps (
profile_store.py, extending #369): The #369 fix normalized cycle timestamps to UTC on write in the detector, but the split, merge, and store-cycle-import paths created new cycle records using the timezone of the source datetime (HA's local tz), so those cycles could still carry non-UTC offsets. All three paths now wrap the computed datetime withdt_util.as_utc()before calling.isoformat(). -
Power readings are processed on the event loop again, restoring matching and notifications (#363, #329 regression) (
manager.py): The #363/#329 state_reported refactor extracted the power-sensor subscription into a new helper and, in the process, moved the@callbackdecorator off the power handler (doubling it onto the helper instead). Without@callback, Home Assistant infers the handler as an executor job and runs it in a worker thread, so every reading triggered a burst of "calls ... from a thread other than the event loop" errors and silently broke the loop-only work the handler drives - live profile matching, notifications, cycle-start/-end events, and the periodic active-cycle state save (each surfaced as a "coroutine was never awaited" warning). On Home Assistant 2026.8 these thread-safety violations became hard errors, so a busy device flooded the log on every sensor poll. The handler is@callbackagain (run on the loop, with the heavy NumPy matching still offloaded to an executor task), and a regression test asserts it stays that way. -
Merging or splitting cycles no longer leaves a phantom "needs review" count (#362) (
profile_store.py,ws_api.py, panel): After merging two mis-split cycles (or splitting one), the consumed cycles were removed but their pending review-feedback entries were left behind, so the attention badge kept showing "N to review" while the review list - which matches feedback against real cycles - was empty, with no way to clear it. Merge and split now prune the feedback for the cycles they remove, and the badge counts only feedback whose cycle still exists (which also self-heals any entries already stuck from before this fix). Separately, the German "cycles to review" label rendered its plural incorrectly ("Zykluss") and now reads naturally. Note: the merge action itself was never removed - it is the multi-select action in the Cycles tab (select two or more cycles, then Merge). -
Relabeling a review cycle now clears its red dot even without pending feedback (#331 residual) (
learning.py): The main #331 fix (relabeling resolves the detection-feedback request) shipped in 0.5.3, but a cycle that was in the review queue only because of a low-confidence quality label or a force_stopped/interrupted status had no pending feedback, so relabeling it left the red "needs review" dot - the user had to open the quality "Save evaluation" flow to clear it. A manual (re)label of such a cycle now also stamps it as reviewed (leaving the quality label itself intact), so the dot clears the same way. Normal cycles are untouched. -
The "needs review" count and list no longer disagree (#355) (panel): A cycle could carry both an unresolved detection-feedback request and a completed ML quality review at the same time. The header counted it (it still has pending feedback) but the Cycles review list hid it, because the list treated any reviewed cycle as done - so the badge said "N to review" while the filtered list was empty, with no way to clear it except a manual JSON edit. Unresolved pending feedback now always keeps a cycle in the review list (and shows the feedback badge) regardless of a separate quality review, so the count and the list agree and the cycle can be resolved normally.
-
The tile card now respects an entity's Display Precision (#354) (
ha-washdata-card.js): When the card's time entity pointed at a sensor carrying more decimals than wanted (or a sensor whose native unit is seconds), the card printed the raw state and always appended "min" - for example "16.2833333333 min". The card now formats the value through Home Assistant's own entity formatter, which applies the per-entity Display Precision set in the entity settings and the entity's real unit, so it shows a clean "16 min" (or the correct unit). Falls back to the previous behaviour on older Home Assistant versions that lack the formatter. -
Live progress notifications can be sticky and open a chosen screen on tap (#347) (
manager.py,const.py, panel): The Android companion app supports a persistent (sticky) notification and a tap target (clickAction), but the live-progress notification never set either, so tapping it dismissed the ongoing thread and always opened the app landing page. Two new opt-in Notification settings expose them: "Keep Live Notification On Tap" (sendssticky) and "Live Notification Tap Target" (sendsclickAction, e.g./lovelace/laundry). These are options on the existing live notification, not a new notification type, and both default off/blank so existing setups are completely unchanged. The keys are forwarded only tomobile_app_*targets. -
The power_profile attribute interval is now configurable (#367) (
sensor.py,const.py, panel): The per-profilepower_profilesensor attribute - a flat per-slot average-watts array that external planners such as EMHASS and tibber_prices read to pick the cheapest window to run an appliance - was fixed to a 15-minute bucket, which blurred short sharp power spikes. A new "Power Profile Interval" setting (Timing & Watchdog, default 15 min) lets you choose the bucket size; it is passed straight through to the existing resampler and only affects the exposed attribute, never detection or matching. Existing setups keep the 15-minute default. -
Smart Termination now leaves a diagnostic trail (#346) (
cycle_detector.py,manager.py): When a cycle finished late it was impossible to tell from the logs whether Smart Termination's fast end-path was blocked (ambiguous match, low confidence, or expected duration not yet reached) or whether the envelope-based pause release simply had not reached its threshold - both were silent. Two DEBUG lines now record these decisions: the detector logs why the fast end-path did not fire (throttled to reason changes so it never spams the log), and the manager logs how far the trace has mapped toward the pause-release threshold. Both are debug-level only, with no change to detection behaviour. -
Long low-power tails no longer make a cycle wait out the safety cap (#350) (
profile_store.py): The envelope-alignment check that decides whether a low-power stretch is an expected pause fed only the power values of the current trace to its matcher, discarding each reading's timestamp. Because the profile envelope lives on a fixed time grid, the mapped position then advanced one grid step per reading regardless of how much real time each reading covered, so a sparse standby tail (for example a plug that reports every few minutes) crept forward far slower than the clock and the "near the end, release the pause" check could take hours to trip, leaving the cycle to be force-ended by the safety cap. The current trace is now resampled onto the envelope's own time step before alignment (the same linear interpolation used when the envelope is built), so the mapped position tracks elapsed time. This also prevents the opposite error, where a densely sampled early part of a cycle mapped too far ahead. Works together with #348. -
Smart Termination could hang a finished cycle until the safety cap (#348) (
manager.py,profile_store.py): When a cycle sat in a verified low-power pause near its end, WashData decided whether to release the pause and finalize by comparing how far the trace had mapped into the profile against the profile's average duration (mapped_time / avg_duration > 0.95). But the mapped position is capped at the envelope's own time-grid span, while the average duration is a separately computed outlier-trimmed mean, so the highest ratio actually reachable wasspan / avg_duration. On any profile whose average ran more than a few percent longer than its envelope span (which force-ended cycles make progressively worse, since their standby overhang inflates the mean), the 0.95 threshold became arithmetically impossible to reach and the cycle hung until the multi-hour deferral cap force-ended it. The release now divides the mapped position by the envelope's own span, so the fraction is a true 0..1 value that reaches the threshold at the actual end of the cycle. -
The panel no longer traps you when the sidebar is set to always-hide (#359) (panel): On a wide tablet with the companion app's "Always hide sidebar" option, opening WashData replaced the whole Home Assistant header - including the hamburger that reopens the sidebar - while the panel's own sidebar-toggle button only appeared on narrow (below 870px) screens, so there was no way back to the rest of Home Assistant without restarting the app. The panel's hamburger now also appears whenever the sidebar is set to always-hidden, at any width, so the sidebar can always be reopened.
-
Trimming a cycle can no longer destroy it (#366) (
profile_store.py, panel): Setting the End time in the Trim tab's clock ("Uhrzeit") mode could collapse a cycle's duration and energy to zero, with no way to recover the original data - and each further trim then operated on the already-emptied cycle. Two problems combined: the backend overwrote the cycle's power trace in place with no lower bound, so a window that kept a single sample (or an inverted end-before-start window) produced a 0-second cycle; and the panel's clock-to-offset conversion treated an empty or unparseable time field as offset 0, dragging an endpoint to a degenerate position (made worse by the mixed timestamp offsets fixed in #369). The trim backend now refuses any trim whose kept window has fewer than two samples or a non-positive duration, leaving the stored cycle completely untouched and reporting a failure instead of destroying data. In the panel, an empty or unparseable time field is now treated as "no change" rather than collapsing the window. -
Trim now lands on the sample you aimed at, and an early clock entry can't collapse a cycle (#373) (
profile_store.py, panel): A follow-up to #366 closing three remaining gaps on the same trim path. (1) Whole-second inputs missed fractional samples. Trim boundaries were rounded to whole seconds, but sample offsets are usually fractional (a nominal 10 s cadence drifts, so an offset like 3132.3 is the norm), and the kept-window filter is inclusive - so a whole-second End landed just below the sample you clicked and quietly cut one sample short (about one in four possible trim targets was affected). Both the stored trace and the on-screen handles now snap to real sample offsets: the store snaps against the full-resolution trace immediately before the irreversible write, and the panel snaps the drag handles and number fields on release/commit so the shaded window matches what will be kept. (2) A clock time before the cycle's start no longer wraps to the end. In clock ("Uhrzeit") mode, entering a Start earlier than the cycle's actual start produced a negative offset that the past-midnight correction pushed forward a full day and clamped to the far end, collapsing the window to about one second; the +24 h correction is now applied only when the shifted time still lands inside the cycle, so an early Start simply means "keep everything from the start" (a genuine past-midnight time still shifts correctly). (3) A destructive trim now asks first. Because a trim cannot be undone, applying one that would keep less than half the cycle shows a confirmation. The backend guard from #366 (refuse any window that keeps fewer than two samples) still stands, so an accidental collapse is reported as a failure instead of silently destroying data. -
Stored cycle timestamps are now consistently UTC (#369) (
cycle_detector.py): Cycle start and end times were saved in whatever timezone the source datetime carried - readings taken from the power sensor produced HA-local offsets (e.g.+02:00) while trim and split operations already wrote UTC (+00:00), so the cycle history accumulated a mix of offsets, and a cycle straddling a restart could even carry different offsets for its own start and end. Every stored instant was still correct (nothing ever displayed the wrong moment), but the inconsistency is a hazard for cross-device transfer and the community store. Cycle timestamps are now normalized to canonical UTC on write, so start and end always carry a+00:00offset, with the actual instant preserved. Existing history is left untouched - older cycles keep their original offset and continue to parse and display correctly. -
Anti-wrinkle no longer stays stuck for hours when the power sensor goes silent (#339) (
manager.py): On a dryer or washer with anti-wrinkle mode and a publish-on-change plug (Shelly, many Zigbee devices), the meter sends one final 0 W reading after the last tumble pulse and then stops reporting entirely. WashData's anti-wrinkle idle-timeout and 2-hour safety cap only advance while power readings arrive, and the stuck-cycle watchdog is switched off for the whole anti-wrinkle tail, so with no further events the device was pinned in the anti-wrinkle state for hours - until the next cycle started. The 60-second state-expiry timer, which keeps running through the tail, now injects a synthetic 0 W reading once the real sensor has been silent for longer than the off delay, so the detector's own idle/safety-cap logic ends the mode on time. A genuine tumble pulse still resets the idle timer through the normal path, and the synthetic reading is never counted as a real sensor update, so a plug that is still reporting continues to drive the mode itself. -
Power sensors that re-report the same value are no longer missed (#363, #329) (
manager.py): WashData only subscribed to Home Assistantstate_changedevents, which fire only when a sensor's value actually changes. Many smart plugs periodically re-publish the same reading - Tasmota'sTelePeriod, or a Zigbee device's maximum reporting interval - and those unchanged re-reports firestate_reportedinstead, so they were invisible to WashData. The consequence: while a finished appliance sat at a flat standby draw no readings arrived, so the detector's end-of-cycle timer never advanced. A cycle then finished late - by up to the plug's full reporting interval (the ~15 min Zigbee lag in #329) - or a dishwasher idling at ~1 W never completed at all (#363). WashData now also subscribes tostate_reportedfor the configured power sensor and routes it through the same handler. Home Assistant fires exactly one of the two events per write (changed vs unchanged), so there is no double counting, and the reading still passes the existing sampling-interval throttle and low-power bypass, so a chatty plug cannot flood the detector. The diagnostic trace now timestamps each reading by the sensor'slast_reportedtime (which advances on every re-report) rather thanlast_updated(which does not), so the recorded curve no longer flat-lines with a stale timestamp during a standby tail. -
Two more blocking startup calls removed (#328, #335): 0.5.3 offloaded the
frontend.pydirectory scan to the executor, but a second blocking read remained:ws_api.pyparsedmanifest.jsonat module level, and because that module is imported lazily insideasync_setup_entrythe file read ran on the event loop every first restart. The version is now fetched once from HA's already-loaded integration manifest (cached, no file I/O) during setup and stored inhass.data, sows_get_constantsreads it from there without ever touching the filesystem. -
Notifications to
notify.*entity targets no longer crash with "extra keys not allowed" (manager.py): When a notify target was configured as anotify.*entity (e.g.notify.mobile_app_phone), WashData routed the call through HA's newernotify.send_messageentity service, which only acceptsmessage,title, andentity_id. Mobile-app enrichment keys (tag, channel, priority, iOS Live Activity fields, icon, progress) were still bundled into adatasub-dict and appended to that call, causing a voluptuous schema rejection (extra keys not allowed @ data['data']) and a "Task exception was never retrieved" error in the HA log -- meaning the notification was silently dropped. Anotify.*entity only ever registers thenotify.send_messageentity service, so entity targets are now always routed there and the unsupported enrichment keys are dropped (logged at debug) instead of poisoning the call; legacynotify.mobile_app_*services, which useNOTIFY_SERVICE_SCHEMAand acceptdata: dict, still receive every enrichment key. Previously the routing fell back to a legacynotify.<object_id>service whenever enrichment was present -- but that service does not exist for entity targets, so simply setting a notification icon made every notification fail withServiceNotFound. -
A telemetry dropout no longer counts as observed quiet time (
cycle_detector.py,manager.py): The end-of-cycle logic that waits for a machine to fall silent tracked "how long have we been below the stop threshold" by simply adding up the time between readings. If the smart plug dropped off the network for half an hour, that entire dark period was banked as quiet time the integration never actually observed -- so a dishwasher could satisfy its passive-dry quiet-release (and a frozen auto-detected pause could be released) purely because the sensor stopped reporting. A parallel gap-free tally now resets whenever the gap between two readings exceeds the device's own outage threshold (ten times its typical reporting interval, clamped to 1-60 minutes), and the quiet-release gates read that instead. The main end-of-cycle timer is unchanged, so soak-bridging behaviour is identical. -
Trimming a cycle no longer keeps data outside the window you selected (
profile_store.py): Trim boundaries are snapped to real sample offsets so a whole-second entry cannot land just short of the fractional sample you aimed at. That snap picked the nearest sample in either direction, so on a coarsely-sampled trace (a dishwasher reporting once a minute, say) a boundary could move outward by up to half a reporting interval -- keeping samples you had asked to remove while the recorded trim range claimed the wider window. Boundaries now snap inward, tolerating only the one-second rounding of the input itself, and a window that ends up containing fewer than two samples is rejected outright rather than silently expanded. -
The auto-tuner's end-energy floor is no longer lost to rounding (
suggestion_engine.py): When a suggested stop threshold and off-delay imply an end-energy floor that is not representable at the two decimal places suggestions are stored with (2 W over 60 s implies 0.0333 Wh), the value was rounded to the nearest -- 0.03 Wh, just below the floor. The consistency pass then found the value already set, made no further change, and returned a suggestion set that still contradicted itself in exactly the way the #376 rule exists to prevent. The floor now rounds up. -
Anti-wrinkle can no longer stay stuck after a Home Assistant restart (#339 edge case) (
manager.py): The keepalive that rescues a silent anti-wrinkle tail needs to know when the power sensor last reported, and that marker is only ever set by a live reading. Restarting Home Assistant while a machine sat in the anti-wrinkle tail with an already-silent plug therefore left it unset, so the keepalive could never fire and the mode stayed pinned until the next cycle -- the exact situation #339 was meant to fix. The restore path now anchors it to the saved state's timestamp. -
Auto-tune suggestion passes read their settings safely (
suggestion_engine.py,learning.py,ws_api.py): The suggestion generators run on a background thread but read the device's configuration directly, which is only safe to touch from Home Assistant's event loop, and the read merged two separate config mappings that could be swapped out mid-read. Configuration is now captured on the event loop immediately before each background pass, so every generator in a pass sees one consistent snapshot. -
A mid-length sensor dropout is no longer credited as quiet time (
cycle_detector.py): The gap-free quiet tally added above decides whether a reading gap is a real outage by comparing it against ten times the sensor's typical reporting interval. That interval was being updated with the current gap before the comparison, so a dropout inflated the very threshold meant to catch it - a two-minute dropout on a ten-second cadence lifted the threshold to about 155 s and slipped through as observed quiet. Very long outages were still caught, so the practical effect was limited to gaps roughly 10-25x the normal cadence. The threshold is now taken from the cadence as it stood before the gap. This also fixes a related blind spot: in the first few readings after a reset the threshold was derived from the current gap itself, which no gap can ever exceed, so no outage was detectable at all in that window. -
A restored cycle no longer inherits quiet time it never observed (
cycle_detector.py): When Home Assistant restarts mid-cycle, the saved state is restored. Snapshots written before this release carry only the older quiet-time counter, which can itself include outage intervals, and that value was being used to seed the new gap-free counter - so a dishwasher could release its end-of-cycle drain wait immediately after a restart without ever observing the configured quiet period. The gap-free counter now starts from zero on restore, which is also the honest value in general: the restart gap is itself unobserved time. Snapshots written by this release and later carry the counter through a restart unchanged. -
Playground simulations now honour the dishwasher drain-wait setting (
ws_api.py): The Playground built its simulated detector without passing the per-device Passive-Dry Quiet Release value, so a replay always used the shipped 600 s default unless you explicitly overrode it in the Playground itself. Simulations of a machine with a tuned value therefore did not match its live behaviour. -
"Clear all data" now also clears saved Playground presets (
profile_store.py): Playground presets survived a full data wipe, leaving watt- and second-scale values from the removed configuration behind. -
Dismissing a notification no longer shows a stray "clear_notification" message (
manager.py): WashData dismisses its own notification cards (the live-progress card, the clean-laundry reminder, the pause reminder) by re-sending them with a special marker body that the companion app understands as "remove the card with this tag". When notify entity targets were switched to Home Assistant'snotify.send_messageservice earlier in this release, that service turned out to be unable to carry the tag - so instead of dismissing anything, the marker was delivered to the user as a visible notification whose text readclear_notification. Entity targets now skip the dismiss send entirely (there is no way to dismiss such a card), and the marker can never fall through to a persistent-notification card either. Regular notifications to entity targets are unaffected. -
A Playground preset with a very long name can be deleted again (
profile_store.py): Saving a preset trimmed its name to the 60-character limit, but deleting one looked the name up untrimmed. A preset saved under a longer name was therefore stored under a shortened key that the delete never matched, so it could not be removed and permanently occupied one of the 30 preset slots. Both paths now derive the storage key the same way, and a name whose trim lands on a space no longer keeps a trailing space in the key. -
The repeating unload reminder now stops after a safety limit (#374) (
manager.py,const.py): The opt-in "Repeat Until Door Opens" reminder had no upper bound. Its "Stop reminding" button only exists on Home Assistant companion-app targets, so anyone whose only notification target is a different platform had the door sensor as their sole way to end it - and if that sensor never reported the door opening (offline, or nobody home for a while), the reminder re-fired forever and held the appliance in the Clean state indefinitely, which also suppressed power-based Off detection for as long as it lasted. The reminder now stops after 48 repeats, roughly two days at the default one-hour spacing and far beyond any useful reminder window, after which the appliance returns to Off normally. Opening the door or tapping "Stop reminding" still ends it immediately as before, and the counter resets for each new cycle. -
The integration version is populated even when HA's manifest loader fails (
__init__.py):ws_get_constantsreads the integration version fromhass.data; if theasync_get_integrationcall that populates it during setup fails, the field was left absent and the panel received an empty version string. The exception path now falls back to readingmanifest.jsonoff the event loop via the executor and only stores the result when the read succeeds, so a transient loader failure no longer leaves the version empty. Normal startup is unchanged; if both reads fail (practically impossible for a packaged integration), the version is still reported as empty. -
pg_match_defaultsis now declared in the WS response contract (ws_schema.py,www/ws-types.d.ts): Theget_constantshandler addedpg_match_defaultsto its response payload but the field was missing fromGetConstantsResponsein both the Python TypedDict and the TypeScript interface, so the contract validator flagged it as an undeclared field and typed consumers had no definition to rely on. Both schemas now include the field. -
The issue-validator log-evidence guidance states the 50-character minimum explicitly (
.github/workflows/issue_validator.yml): The validator rejected log sections of 50 characters or fewer, but the comment posted to the reporter only said "a few lines of error text are enough" - a reporter who pasted a single short error string would be rejected without understanding why. The guidance now says "more than 50 characters" so the requirement is explicit.
-
Export and import now let you choose exactly what to move (proposed by @sueha) (
profile_store.py,ws_api.py, panel): Export and import used to be all-or-nothing - export dumped the entire store and import replaced the whole thing, overwriting everything on the target device. Both are now guided wizards in Advanced -> Diagnostics with a hierarchical, tri-state selection tree (select all, per-category, or individual profiles/cycles). You pick precisely what travels: profiles (with or without their cycles - a profile exported without cycles still carries its learned shape, so it stays matchable), run cycles, imported reference cycles, detection & matching settings, phases, profile groups, matcher tuning, ML models, feedback/review labels, maintenance log, history logs, and lifetime totals. Mandatory device-type and version markers are always included; the GitHub token and transient in-flight state are never exported.- Import analyzes the file first, over WebSocket, before anything changes. Paste JSON or load a file and WashData immediately shows a manifest of what it contains and what can be imported into this device - per-category counts, per-profile cycle counts, name-clash flags, and a warning when the export is from a different appliance type (in which case device-specific settings and real-history import are disabled, but programs and cycles can still come in as reference data).
- Import merges instead of overwriting. By default imported data is added alongside your existing data and nothing is lost; when a profile name already exists you choose per-clash: import as a copy, keep yours, or overwrite. A Replace mode is also available (each ticked category is wiped and replaced from the file; unticked categories are left untouched).
- You decide where imported cycles land. By default they become reference cycles that only sharpen program matching and never touch your usage/energy/count statistics; opt into "real history" (which feeds stats) when you are genuinely migrating one appliance to a new install. The reference-vs-history isolation is preserved end to end.
- The old whole-store export/import remain available as "Quick export everything" and an "Advanced: replace all from JSON" fallback, and previously exported files (including older versions and Home Assistant diagnostics downloads) import through the new wizard unchanged. Four new WebSocket commands back this:
get_export_inventory,analyze_import,export_config_selective, andimport_config_selective(all full/admin-gated, like the existing export/import).
-
Playground can now simulate what happens when an appliance idles after a cycle (
playground.py,ws_api.py, panel): Previously the Playground's "Simulate" mode appended a silent 0 W tail after the recorded cycle, which never represents how real appliances behave (a display, pump controller, or WiFi module typically holds 1–5 W in standby). There was no way to know whether WashData would correctly stop the cycle at a device's actual standby draw. A new "Test idle termination" toggle in the detection settings column appends a synthetic standby continuation instead. The integration automatically estimates the standby floor from the last 60 seconds of the real recording (using the 7th percentile, which picks the between-burst baseline rather than a contaminated mean), or you can type an explicit idle level in watts to override it. The simulation runs through the real detector - if the standby draw is below the stop threshold, the cycle ends normally and WashData tells you how long it took and which detection path fired; if the draw sits above the stop threshold, the cycle holds open until the 8-hour safety cap and an alert explains why. The synthetic region is shaded on the timeline graph. The feature uses sparse 30-minute steps after a dense 20-minute pre-fill, so even an 8-hour worst-case scenario completes in under a second. -
Anti-wrinkle: the gap allowed between two tumble pulses is now a setting (
anti_wrinkle_idle_timeout, "Max Pulse Gap"): Anti-wrinkle mode ended after a hardcoded 120 s of quiet, which is shorter than the pulse spacing of some dryers. On a Bosch heat-pump dryer the pulses sit 130-660 s apart (18 pulses measured in one recorded cycle), so the mode dropped out right after the first pulse and every later pulse surfaced as an aborted false start (off -> starting -> off, 3-14 times per cycle across three recorded cycles) instead of staying attached to the finished cycle. The tolerance is now a normal setting in the Anti-Wrinkle section, defaulting to 120 s so existing behaviour is unchanged; dryers with wider pulse spacing can raise it. The exit still also honours the dynamic end threshold, so nothing gets shorter than before. -
The Playground now shows anti-wrinkle:
anti_wrinklewas mapped to "Idle" in the simulation state band, so the mode was invisible in the very view used to tune it. It now renders as its own band segment, and all fiveanti_wrinkle_*options (enabled,max_power,max_duration,exit_power,idle_timeout) are editable as Playground overrides and honoured by the simulator, which previously ignored them and always used the stored values.
-
Simulating a very long cycle no longer pegs the CPU for minutes (#311): 0.5.2 made the Playground "Simulate" run cancellable and off the main request, but the underlying replay of a multi-hour cycle (for example a ~235-minute dishwasher program) still burned several minutes of solid CPU and could saturate a small host. Profiling traced almost all of it to the phase-progress estimator, which slides the current power window across the profile's entire time grid once per progress update: for a very long cycle that grid holds thousands of points, so each update ran thousands of correlations and a whole simulation ran over a million. That per-update search is now vectorized with NumPy instead of a Python loop - about 40x faster on a long cycle (a ~235-minute dishwasher simulation drops from ~130s to ~3s of compute) with byte-for-byte identical results (verified across the full simulated timeline). Because the same estimator runs live every few seconds, this also cuts the per-update CPU cost of the running-cycle progress/time-remaining estimate on low-power hosts.
-
Rebuilding profile envelopes is much faster on long cycles (#311): Every operation that rebuilds a profile's power envelope - reprocessing history, splitting, merging, or trimming a cycle, and the "Rebuild Envelopes" button - aligns cycles with dynamic time warping, and that alignment was a pure-Python double loop over the full-length traces (tens of millions of
min/abscalls for a multi-hour cycle, dominating the whole rebuild). The cost matrix is now filled with a vectorized NumPy anti-diagonal sweep instead, giving up to ~8x faster envelope rebuilds on long dishwasher traces with byte-for-byte identical results (verified across every recorded cycle dataset, plus a 468-case fuzz against the original). These paths already ran in the background, so this is a CPU/throughput win rather than a responsiveness fix, most noticeable on low-power hosts with long cycles. -
Program-matching DTW is ~3× faster (
analysis.py): The bounded DTW used to score every profile candidate during program matching (the live matching pass that runs every 5 minutes and the Playground simulate path) iterated the dynamic-programming band cell-by-cell in Python, each step boxing and unboxing NumPy scalars. The inner loop now operates entirely on Python-native floats (converted via a single.tolist()per row) and writes results back as one slice assignment — eliminating per-element NumPy boxing overhead without changing the algorithm. Results are byte-for-byte identical to the original (verified with a 537-case fuzz covering shapes from 1×1 to 300×300, multiple band widths, and both normal and derivative modes). The anti-diagonal vectorized fill used for full-path envelope DTW is intentionally not reused here because its per-diagonal Python setup cost dominates for the small fixed-size arrays used in matching (n=200 both sides), where it is actually 2× slower. -
Phase-regime run-length encoding is ~17× faster (
phase_segmenter.py): The_runsfunction that converts the per-sample regime array (idle/active/high) into a list of contiguous runs used a nested Python while-loop, visiting every sample individually. For a 2-hour cycle at 5-second resolution (1 440 samples) that is 1 440+ Python loop iterations. The function now usesnp.diff+np.flatnonzero(both C-level) to find run boundaries, then a short list comprehension over only the boundaries (~10–30 for a real cycle). The speedup is ~17× on a realistic cycle trace, ~1.3× on the pathological fully-alternating case, with byte-identical output across 313 fuzz cases. Phase regime segmentation is called on every envelope rebuild and, whenenable_phase_matchingis on, on every live matching trigger. -
The community store no longer re-queries the brand and device catalog on every panel open (
store_client.py): The online-features panel fetches the store's brand list and device search results whenever the Settings or Store tab is (re-)opened, and those reads were sent to the store's Firebase backend every single time with no caching. On the store's free tier (a fixed daily document-read budget shared by every install) that brand-list query alone was by far the largest read source, and repeated panel opens could exhaust the daily budget. The integration now caches these public, slow-changing catalog reads (list_brands,search_devices) in memory for 15 minutes, and theconfig/siteread (used to resolve the community confirm-threshold on every device confirmation) for one hour. Because the store client is a single long-lived instance per device, the cache survives panel reloads, so a burst of panel opens now costs at most one Firestore query per catalog key per window instead of one per open. Contributing a brand or device immediately clears the affected cache entries, so a freshly-added entry still appears right away for the user who added it. Only successful reads are cached, so a transient network failure never pins a stale or empty result.
-
Panel registration on multi-device installs no longer causes a blocking startup call or a registration race (#328): On systems with more than one WashData device, every
async_setup_entrycall issued aos.scandir(blocking filesystem call) directly on the event loop and then raced to register the panel sidebar entry, with each device winning the race overwriting the last. Both issues are fixed: the scandir is now offloaded to an executor thread, and a sharedasyncio.Taskkey on thehassobject ensures only the first setup call registers the panel while all subsequent ones await the same in-flight task, so registration happens exactly once regardless of how many devices load simultaneously. -
The live power chart no longer appears frozen at the last active reading during end-of-cycle detection (#329): While a cycle is in the ending phase (waiting out the minimum off-gap soak-bridge before finalizing), the detector's sampled power trace can lag the raw sensor by up to one sampling interval because some readings are throttled. The
get_power_historyWebSocket handler now overlays any rawdiag_bufferreadings that are strictly newer than the last trace point before returning the live chart data, so the power-drop is visible immediately rather than waiting for the next throttle-pass or cycle finalize. Additionally, therunning_dead_zonesetting has been retired: this configuration option existed for several releases but was never wired to any detection logic (tuning it had no effect whatsoever). The field has been removed from the panel, the settings schema, the suggestion engine, and the Playground; a 3.7 -> 3.8 config migration strips it from existing entries. Its documented purpose -- guarding against false stops in the first few minutes of a cycle -- is correctly served by the Start Duration Threshold and the energy gate on the STARTING state. -
Re-labelling a cycle that is awaiting review now clears it from the "must be reviewed" queue (#331): When a cycle finished with an uncertain match it entered a review queue backed by a pending-feedback record holding the (often wrong) auto-detected program. Correcting it the obvious way -- opening the cycle and using the "Label" button to assign the right program -- changed the label but never touched that pending feedback, so the cycle stayed in the queue indefinitely, still tied to the stale original detection, with no visible control to resolve it (only the
submit_cycle_feedbackservice could). A manual (re)label is now treated as the user's answer to "did WashData detect the right program?": labelling a review cycle to the same program records a confirmation, labelling it to a different program (or creating a new one) records a correction, and removing the label records a rejection -- in every case the pending feedback is resolved and the cycle leaves the review queue. This applies to the panel's Label button, bulk relabel, the Review-mode label field (which previously stamped the cycle as reviewed while silently leaving the feedback orphaned), and thelabel_cycleservice. The Confirm / Correct / Ignore controls are also now shown directly in the cycle's Inspect view, not only inside Review mode, so a "needs review" cycle exposes its resolve actions the moment you open it.
-
Back-to-back washes with an anti-crease tail no longer merge into one giant cycle (#296): A washing machine in anti-crease mode (e.g. Miele "Knitterschutz") holds a low baseline plus periodic low-power tumble bursts after the wash finishes, until the door is opened. Because those bursts recur faster than the off-delay, they kept reviving the cycle, so it never finished into the anti-wrinkle state that is designed to absorb the tail. If a second load was started before the door was opened, the whole sequence (wash → tail → wash → tail) merged into a single multi-hour "cycle". The earlier 0.5.1 backstops only caught a flat standby plateau, not this bursty tail. WashData now recognises the anti-crease tail directly: once a confidently-matched, genuinely-hot cycle is past its expected duration and has settled into a low-power tail with no reading above the anti-wrinkle power threshold, it finalizes the wash into the anti-wrinkle state (which then absorbs the tail and splits off the next wash on its first heating burst). It also freezes program re-matching on the growing tail, so the match can't drift to a longer profile and stall the finish. Opt-in via the existing anti-wrinkle setting, gated on being past the expected duration so a genuine mid-wash low-power phase (a washer spends most of its cycle below that threshold) is never cut short. Verified against a real Miele export.
-
Machines that hold a small standby draw after finishing no longer run indefinitely (#296): Some washers and dryers finish a cycle but keep drawing a small, flat "anti-crease" or display standby load that sits just above the stop threshold. Because the power never actually dropped below the stop threshold, the cycle never progressed to the ending phase and would run until the 8-hour safety cap (and anti-wrinkle handling couldn't engage, because it only starts once a cycle has finished). WashData now detects a flat, low, sustained plateau (at most 10% of the cycle's peak power, held for at least 10 minutes, past twice the expected duration) on washers and dryers and finalizes the cycle as a normal completion — which also lets anti-wrinkle handling engage afterwards. The plateau must be genuinely flat, so a cycle still doing real work is never cut short, and appliances with legitimate low-power holds (bread makers, pumps, air fryers) are excluded.
-
Cycles with an uncertain program match no longer hang for hours after finishing (#296, #311): The early "Smart Termination" that ends a cycle as soon as its matched program is done is deliberately skipped when the program match is ambiguous (to avoid splitting one wash into two records). Such cycles fell back to power-based end detection, which a low but non-zero standby draw (below the stop threshold, but enough to keep tripping the energy gate) could hold open until the 8-hour safety cap. A new duration-anchored backstop now finalizes a matched cycle that has sat in the ending phase well past its expected duration (twice the expected length) and been continuously quiet for at least 10 minutes. It only ever shortens a stuck wait, never ends a cycle early, and the sustained-quiet requirement means a genuinely longer program (which still has high-power phases) is never cut short.
-
A cycle paused during start-up no longer stays pinned in the "Starting" state forever (
cycle_detector.py): Pausing an appliance while WashData was still confirming the cycle start (the brief "Starting" phase) correctly held that state waiting for a Resume — but if the appliance was then switched off entirely instead of resumed, nothing ever moved the detector back to Idle. It would remain stuck in "Starting" until the integration was reloaded. The detector now watches for sustained truly-off power (below the stop threshold) during a paused start-up and returns to Idle after 5 minutes. A genuine pause, which holds standby power above the stop threshold, is unaffected and still waits indefinitely for a Resume. Also fixed: on appliances with a slower sensor polling rate (readings every 30+ seconds), one below-stop reading with a large preceding interval could be credited as the full "off duration" and immediately trigger the 5-minute cancel threshold. The timeout now measures from the first observed below-stop reading rather than accumulating each interval, so only genuinely observed quiet time counts. -
Setting suggestions no longer shorten the off-delay in a way that splits soak pauses (#311): When reconciling suggested settings, the rule that keeps the minimum off-gap at least as large as the off-delay could resolve the conflict by lowering the off-delay instead of raising the gap. A shorter off-delay makes end-of-cycle detection more aggressive, so a genuine multi-minute soak pause could be mistaken for the end of the cycle and split one wash into two records. The reconciliation now always raises the minimum off-gap and never lowers the off-delay. To prevent the larger gap from delaying end-detection, the washing-machine / washer-dryer Smart-Termination quiet-time requirement is now capped at 10 minutes (the shipped defaults of 4 min / 5 min are unchanged; the cap only bounds unusually large hand-set or suggested gaps that previously inflated it to 15+ minutes and starved end-detection).
-
Splitting a cycle no longer silently deletes it on failure (#323): When a split was attempted on an older cycle whose power data had been stripped by the retention policy (or had an unparseable timestamp), the split operation removed the source cycle from the history list before checking whether it could actually be split. The early-exit path left the cycle permanently deleted, and the next unrelated save committed the loss. Validation now runs before the cycle is removed, so a split that cannot proceed leaves the history intact. Two additional edge cases were also fixed: (1) all segment bounds (not just the source timestamp and trace) are now validated before the source cycle is removed — a malformed segment encountered mid-loop after the pop had already happened would previously leave partial children and no source cycle; (2) a split request with fewer than two valid segments is now rejected before the pop, so an empty or single-segment call can no longer delete the source cycle and create zero or one orphaned children.
-
Splitting a cycle no longer permanently blocks all subsequent edits (#323): If the split operation raised an exception after acquiring the per-entry write lock, the lock was never released -- causing every later trim, merge, split, reprocess, and settings-save to block forever waiting for it. The lock is now always released in a
finallyblock, matching the pattern already used by trim, merge, and all other write-locked operations. -
Trim and merge now update cycle energy after editing (#323): Trimming or merging a cycle rewrites its power trace but previously left
energy_wh(and for trim,max_power) unchanged, so the stored value reflected the pre-edit cycle. The per-cycle energy is now recomputed from the new power data using the sameintegrate_whpath used at detection time. Trim also updatesmax_powerfrom the kept window. Profile average-energy statistics derived from cycle history are therefore correct after an edit. Edge case also fixed: when an aggressive trim leaves exactly one sample,energy_whis now set to 0 andmax_powerreflects that sample's power value; previously both retained their pre-trim values. -
Cycles stuck in the ENDING phase no longer run indefinitely (#296, #311): The detector's 8-hour hard-stop cap applied only to the RUNNING state; a cycle that entered the ENDING phase (waiting for sustained low power before officially finishing) had no equivalent cap. With a constant standby baseline above the stop threshold, the cycle could sit in ENDING for 16+ hours. ENDING now has the same 8-hour absolute cap as RUNNING.
-
Unmatched cycles no longer run indefinitely on devices with no profiles (#296): The manager watchdog that kills zombie cycles only fired when a profile had been matched (
expected > 0). A cycle on a device with no profiles, or one that never matched, had no manager-level time limit and would only be stopped by the detector's 8-hour hard cap (which relies on a quiet sensor). A chatty sensor that stays above the start threshold kept these cycles alive indefinitely. Unmatched cycles are now force-ended by the manager watchdog after 4 hours. -
Background tasks (split, trim, merge, reprocess, ML training, Playground) no longer get stuck showing "Running" after a reload (#323): When Home Assistant cancelled a background task during a reload or shutdown, the
asyncio.CancelledErrorwas aBaseExceptionand bypassed theexcept Exceptionhandler, sotask_registry.finish()was never called. The task stayed in therunningstate indefinitely, making its header pill spin forever even after reload. All nine task runners now explicitly catchCancelledError, mark the taskcancelled, and re-raise so asyncio behaves correctly. -
Tasks and write locks no longer leak across
reload_config_entry(#323):async_unload_entrynow explicitly marks any still-running tasks for the entry as cancelled in the task registry, and releases the per-entry write lock. Previously both could survive across a reload, causing task pills to reappear in the panel and new operations to block forever on a held lock. Also fixed: the cancelled WS-spawned tasks are now awaited (asyncio.gather) before the write lock is removed, so theirfinallyblocks (which release the lock on the old reference) complete before the lock entry is discarded and a new setup can start. -
An in-progress cycle-end task no longer races a freshly-loaded ProfileStore (
manager.py): When_on_cycle_endspawned its post-processing task and areload_config_entrycompleted before the task finished, the task would write the new cycle to the old in-memory ProfileStore. The new store (loaded from the file before the write) would then overwrite the file on its next save, silently discarding the cycle. The task handle is now stored and cancelled inasync_shutdown; the task is not created at all if shutdown has already begun. -
Translation updates from community contributors now take effect immediately (
frontend.py): The panel and per-language translation files were cache-busted using only theha-washdata-panel.jsmodification time. A translation-only update (e.g. a GitLocalize merge) didn't change the JS file, so browsers kept serving stale cached translations even after a HA restart. The cache-buster now uses the newer of the JS file mtime and thetranslations/panel/directory mtime, so a translation-only release correctly invalidates the cache. -
Playground what-if simulation now uses the correct default config (
ws_api.py): The Playground fallback config (used when the live detector config isn't yet available) hardcodedmin_power=5.0andrunning_dead_zone=0, while the canonical defaults inconst.pyare2.0Wand3srespectively. Therunning_dead_zonedivergence was reachable because the config migration never set a default for it. The fallback now readsDEFAULT_MIN_POWERandDEFAULT_RUNNING_DEAD_ZONEso Playground simulations are faithful to the live detector. -
Start notification no longer re-evaluates on every 5-minute match tick (
manager.py): When only the HA event bus was used for start notification (no notify service, no action),_notified_startwas never set toTruebecause the notification service path never ran. The matching fallback in_async_do_perform_matchingtherefore re-entered the start-notification block on every 5-minute match tick for the entire cycle's lifetime._notified_startnow tracks only whether the start push has been sent (event delivery is tracked separately by_start_event_fired): an event-only config marks the start handled as soon as the event fires so the re-check is skipped, while a config that has both events and a push service keeps_notified_startclear until the push actually dispatches, so it still receives its start push. -
Phase label no longer drops to the last-defined phase during a gap between user-drawn ranges: When a cycle's elapsed time fell in a gap between two consecutive phase ranges in the profile (for example, between a Heating phase ending at 20 min and a Rinse phase starting at 25 min), the fallback previously returned the absolute last phase name in the list rather than the phase that had most recently applied. It now returns the phase whose start was last passed, which is consistent with "still in the previous phase" semantics. Also fixed: phase ranges with a missing or zero
endvalue are now skipped (instead of silently matching everything at time 0), and aNonestart value no longer raises aTypeErrorduring the sort. An additional robustness fix: a non-dict entry in a profile's saved phase list (possible from legacy or corrupted storage) would raiseAttributeErrorduring the sort and crash live phase matching for the cycle. Such entries are now silently filtered out before sorting. -
Detector state is fully reset between cycles (
cycle_detector.py): After a cycle completed,reset()cleared the matched profile name but left_expected_duration,_last_match_confidence,_match_ambiguous, and_match_prefix_ambiguouscarrying over from the previous cycle. A stale non-zero_expected_durationcould mislead the dishwasher end-spike gate (which uses it to decide whether to arm), potentially blocking the fast-close path for the second cycle. All four fields are now explicitly zeroed inreset(). -
Idle sensors no longer flood the power-reading path (
manager.py): When an appliance is switched off, smart plugs typically publish a 0W reading on every polling interval (often every 1-5 seconds). Because 0W is belowmin_power, the sampling-interval throttle was bypassed unconditionally, causing the detector and all downstream estimators to be called up to 60+ times per minute for a device that was clearly idle. The bypass now only applies when a cycle is actively in progress (RUNNING, ENDING, or PAUSED state) or when the previous reading was abovemin_power(a genuine power-drop event). Devices at idle are throttled by the normal sampling interval. -
ML quality/conformance gate now accepts numpy scalar scores (
learning.py): The auto-label downgrade logic that catches suspicious cycles usedisinstance(x, float)to guard the threshold comparison. In NumPy 2.0+numpy.float64is no longer a subclass of Pythonfloat, so ML model scores (which are numpy scalars) silently bypassed the check and suspicious cycles were never downgraded to feedback requests. The gate now usesfloat()with exception handling so numpy scalars, Python floats, and non-numeric sentinel values are all handled correctly. -
Playground Test-on-history and Parameter Sweep rebuild match snapshots once per run, not once per chunk (
playground.py,ws_api.py): The chunked Playground history runner split work into 2-cycle chunks to keep the event loop responsive, but each chunk independently re-derived the full set of profile match snapshots (decompressing sample cycles and applying group collapsing). For a 10×10 parameter sweep grid, this rebuilt the identical snapshot set 100 times. Snapshots are now built once before the loop and passed through, cutting snapshot-rebuild overhead from O(chunks × profiles) to O(profiles). -
DTW refinement no longer resamples the current-cycle trace once per candidate (
analysis.py): In ensemble DTW mode the current trace was resampled to the DTW grid length twice per candidate (once for the L1 component and once for the derivative component); in other modes it was resampled once per candidate. Since the current trace does not change within a single match pass, it is now resampled once before the candidate loop and the pre-resampled array is passed to each candidate's scoring call. Matching results are byte-identical. -
ENDING energy-gate no longer scans the entire growing trace per reading (
cycle_detector.py): When the off-delay elapsed and the energy gate was active, the gate built its recent-window list by filtering the full_power_readingshistory with a time comparison on every entry. For a long cycle (2800+ readings on a 4-hour dishwasher cycle), this was O(n) on every below-threshold reading while in ENDING. The filter is now a tail-walk that breaks as soon as it steps outside the gate window, making it O(window-size) regardless of cycle length. -
ML cycle quality scoring no longer has a data race on the past-cycles list (
manager.py):_compute_cycle_quality_scoreruns in an executor thread and iterated the livepast_cycleslist, which the event loop can append to concurrently. This is now a snapshotted copy, eliminating the race. -
Task results from one device can no longer be evicted by another device's task activity (
task_registry.py): The finished-task retention cap (_MAX_FINISHED=30) was applied globally across all devices. In a multi-device install, a device running frequent ML training could evict another device's Playground result before the panel read it. The cap is now applied per device entry. -
find_best_alignmentis now safe against empty or single-point inputs (analysis.py):np.correlatefollowed bynp.argmaxcrashes on a zero-length array. While the snapshot builder guarantees at least 2 points today, a defensive early-return is now in place so future callers are safe. -
Navigating away from and back to the WashData panel no longer loses live cycle push updates (#323,
panel.js): HA caches custom panel elements between navigation events. When the element disconnected from the DOM, all WebSocket event subscriptions (cycle-started/ended push events and the background-task registry push) were torn down but never restored when the element reconnected. Returning to the panel left it relying only on the 30-second fallback poll: live cycle state transitions no longer updated immediately, task progress pills stopped updating, "Cancelling…" pills could never clear, and the modal Escape/Tab keyboard handler was dead. All subscriptions are now re-established inconnectedCallback()when returning to an already-initialized panel; the keyboard handler is also re-attached. -
Task cancel pills no longer get permanently stuck in "Cancelling…" (#323,
panel.js): After requesting a cancel, the task ID was held in a_cancellingTasksset and removed only when a task-registry push event confirmed the task had stopped. If the task finished while the WebSocket subscription was down (during a reconnect), the confirmation never arrived and the pill stayed "Cancelling…" disabled forever. The pill renderer now prunes stale IDs from_cancellingTasksbefore rendering — any ID whose task is no longer in the running list is silently cleared. -
Playground Simulate timeline no longer ships thousands of points over WebSocket (
playground.py): A 4-hour dishwasher cycle at the 5-second Simulate cadence produces ~2,800 per-step timeline points, each a 12-key dict. These were stored uncompressed in the task result (up to 30 retained per device) and serialized over WebSocket on demand. The timeline is now capped at 600 points at finalize time via uniform thinning, which preserves the overall shape for the panel's chart without a measurable fidelity cost. -
On-device ML regression training promotion gate fixed (
ml/training_task.py): The gate that decides whether a newly-trained regression model beats the naive baseline usedfloat(metrics.get("mae") or 1.0). Python'soroperator treats0.0(MAE of a perfect regressor) as falsy and replaces it with1.0, so a perfect MAE would be seen as the worst possible score and promotion would be rejected. Changed to an explicitNonecheck. -
Chart hover no longer causes per-frame jank on long cycles (
panel.js): Every pointer-move event over a chart triggered a full synchronous canvas redraw, and on every redraw the canvas's width and height attributes were reassigned unconditionally — which clears the canvas and forces a GPU-side backing-store reallocation even when the size hasn't changed. On the spaghetti chart with 100+ cycles and 10k+ data points this caused visible main-thread stalls. Two fixes: (1) hover redraws are now rAF-coalesced — only one redraw per animation frame regardless of how many pointer-move events fire, and (2) the canvas backing-store is only reallocated when the computed size actually changes (clearRectclears it instead). -
Panel strings now go through the translation system (
panel.js): Five user-visible strings were hardcoded in English and bypassed the_t()translation function: the "Remove"aria-labelon entity-list pill remove buttons, the "Toggle Home Assistant sidebar"aria-labelon the hamburger button, the "Drag to resize"titleon the log-drawer resize handle, and the "Remove this many seconds from the start/end" field hints in the recording trim modal. All five are now translated. Addedlbl.drag_to_resize,msg.head_trim_hint, andmsg.tail_trim_hinttotranslations/panel/en.json -
Trim, split, and merge tasks now respond correctly to cancel requests (
ws_api.py): When a user requested cancellation of a running trim, split, or merge task,cancel_taskreturned{"cancelled": true}and set the cancel flag, but the task runner never checked the flag and the operation completed anyway — leaving the user with a visible "cancelled" confirmation but a result that still landed. All three runners now checktask.cancel_requestedimmediately after acquiring the write lock, and abort cleanly if a cancel was requested while the task was waiting in the queue. -
Notification action
Scriptobject is now compiled once and cached (manager.py): Every notification delivery call (cycle-start, progress update, cycle-end, quiet-hours flush) reconstructed the HAScriptobject from the raw action sequence JSON, which re-validates, re-parses, and re-compiles the action list on each call. The compiled script is now cached on first use and invalidated only when the action sequence changes (on options reload), eliminating the redundant per-call construction for the common steady-state case. -
Suggestion engine no longer runs 4 full cycle-history scans on every cycle end (
learning.py): The model-suggestion and detection-suggestion passes (which each scan up to 200 cycle power traces) were triggered unconditionally on every cycle end — including unlabeled, noise, and interrupted cycles where nothing in the training data changed. These passes are now gated on the labeled-cycle count: they run only when at least one new labeled+completed cycle has appeared since the last run. The single-cyclerun_simulationpass is also now skipped for noise, unlabeled, or interrupted cycles. -
Removed vestigial
abrupt_drop_watts/abrupt_drop_ratiosettings (panel.js,translations/panel/): The detector code for abrupt-drop detection was removed in an earlier release, but the two settings fields remained visible in the detection settings panel (where they do nothing) and their translation strings remained in all 35 language files. Both fields are now removed from the settings schema, diagram key map, diagram SVG renderer, and diagnostics table in the panel, and their translation keys (setting.abrupt_drop_ratio,setting.abrupt_drop_watts,pg_desc.abrupt_drop_watts) have been removed from every panel translation file. -
Dead sync
match_profilemethod removed (profile_store.py): A 56-line synchronous matching method that predated the full Stage 1-5 pipeline was still present inprofile_store.py. It omitted Stage-5 profile groups, golden cycle selection, and the envelope-based snapshot pipeline — diverging fromasync_match_profile— and had no callers. Removed to eliminate the maintenance risk of it being re-wired and producing subtly wrong results. -
On-device ML models with stale feature columns now fall back to baseline (
ml/engine.py): If a future release changes any model'sFEATURE_COLUMNS, a previously-promoted on-device spec stored inml_model_versionswould score on a mismatched feature schema, silently overriding the correct baseline.resolve_scorerandresolve_regressornow compare the spec's storedfeature_columnsagainst the current model's expected columns at load time; a mismatch logs a one-time warning and falls back to the baseline (or inert, for regressors with no baseline). -
Cycle retention now runs before the split result is saved (
profile_store.py): When a cycle was split into segments, the retention enforcement was scheduled as a fire-and-forget async task rather than being awaited. This meant the newly-created segments might be saved without first checking whether they pushed the history past the retention limit; the trim would only apply on the next unrelated save.apply_split_interactivenow usesasync_add_cycle(which awaits retention) instead of the syncadd_cycle, so the retained cycle list is always consistent at save time. -
Cycle-ID collisions no longer corrupt history (
profile_store.py): Cycle IDs are derived from a 12-character SHA-256 prefix ofstart_time + duration. Two cycles with identical raw (pre-normalization) start times and durations — most likely during bulk reference-cycle imports — would hash to the same ID, causing the wrong cycle to be returned or deleted by any ID-based lookup._add_cycle_datanow checks for ID conflicts in the destination list and appends an incrementing suffix to the hash input until the generated ID is unique. -
Playground "Simulate" is ~6x faster for long cycles (
playground.py): The series-point throttle constant (_SIM_SERIES_THROTTLE_S) was 5.0 seconds — matching the typical 5-second sensor reading cadence, so the throttle dropped nothing and every reading triggered a full O(trace-length) estimator pass. A 233-minute dishwasher cycle at 5s cadence therefore ran all ~2,800 estimator passes in serial, each doing more work than the last. Raising the throttle to 30.0 seconds keeps the series well underMAX_SERIES_PER_CYCLE(600) for any realistic cycle, reducing estimator calls approximately 6x and making the per-chunk executor work proportionally shorter. -
"Process history" can now be cancelled between steps (
ws_api.py): The five-step reprocess task (async_reprocess_all_data→ golden backfill → suggestions → ML training → health recompute) only checkedcancel_requestedbefore the ML training step; the first three steps ran regardless of a pending cancel. Cancel checkpoints (with partial results) are now inserted before the first step and at each step boundary, so a cancel request issued while matching cycles completes or while suggestions run is honoured at the next safe exit point. -
Profile store now applies per-device duration-ratio default at startup (
manager.py): TheProfileStoreconstructor was given the globalDEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO(0.10) as the fallback, rather than the per-device override inDEFAULT_PROFILE_MATCH_MIN_DURATION_RATIO_BY_DEVICE. This diverged from theCycleDetectorConfigconstruction which correctly uses the per-device dict. Currently inert (dishwasher's entry equals the flat default), but would silently produce the wrong Stage-1 gate after the first power cycle if a device type with a genuinely different default were added. The initialization now uses the same per-device-with-fallback pattern as the detector config.
-
Phase-aware time remaining (phase-segmented matching): Temperature and spin variants of the same program (Cotton 30 °C vs Cotton 60 °C, say) look almost identical overall but spend very different amounts of time heating - so a single whole-cycle duration model mis-estimates the time remaining for whichever variant is not the median. WashData can now budget each cycle stage separately. An unsupervised segmenter (
phase_segmenter.py) breaks each of a profile's cycles into idle / wash / heating / spin regions and learns a per-stage duration + energy prior (cached on the profile envelope); during a live cycle a per-stage estimator (phase_match.py) blends a phase-resolved estimate into the proven time-remaining math (leaning on the per-stage budget early and the whole-cycle estimator late). It is opt-in per device via the new "Use phase-aware time remaining" setting (currently available for washing machines and washer-dryers), and it refines time remaining only - it never changes which program is matched. With the toggle off, the estimate is byte-for-byte identical to before. A companion data-hygiene advisory flags a profile whose cycles are internally inconsistent on heating (a sign two different programs were accidentally labeled as one). The what-if Playground uses the exact same estimator, so its replay stays faithful. -
Adoption guidance system - Setup Card: Every new device now gets a persistent Setup Card on the Overview tab that walks you through setup one step at a time. The card shows exactly one focused message and one primary action, adapts to where you are in the setup journey, and consolidates five previously-scattered guidance surfaces (the old Getting Started card, the coverage-gap banner, the Recommendations banner, the profile advisories section, and group-suggestion chips) into a single cohesive flow. The card moves through five phases: Phase 0 (no profiles yet - just watching on power thresholds, with a "Start Recording" nudge tailored to your appliance type), Phase 1 (first profile saved - via recording, labeling, or a Community Store download), Phase 2 (reactive nudges when unmatched cycles cluster or your last cycle went unmatched), Phase 3 (tuning suggestions, profile groups, and phase configuration), and Phase 4 (a small green chip: "Device setup complete"). Nudges can be snoozed for 14 days or dismissed permanently. A "Hide guidance" link collapses the whole card to a chip from Phase 3 onward; clicking the chip expands it. If a new unmatched cluster appears after the card has gone quiet, it turns amber and resurfaces automatically.
-
Threshold mode when no profiles exist: When a device has no real profiles (a profile with at least one associated cycle - a name-only stub does not count), WashData now skips the 5-minute profile match polling and suppresses the "No profile matched yet" live-cycle notification. Both were misleading and wasteful when there is nothing to match against. Detection runs normally on power thresholds from day one; the Setup Card explains the state instead of a confusing notification.
-
Config flow simplified: The "Create First Profile" step has been removed from the setup wizard. It created a name-only stub with no cycle data that made users think they had a working profile when they did not. The Setup Card's Phase 0 replaces it with clear guidance and direct actions. Existing installs that already have an
initial_profilekey from this step are silently cleaned up on the next HA restart (config migration 3.6 to 3.7). -
Per-profile power profile for price-based scheduling (#272): Each learned profile's count sensor (
sensor.<name>_<profile>) now carries apower_profileattribute: the profile's average power in watts per 15-minute slot, as a flat array like[2200, 2200, 800, 800, 1500, 500, 400, 200](with a companionpower_profile_interval_min: 15). This is exactly the shape external planners consume, so you can feed it straight into, for example,tibber_prices.find_cheapest_blockto run the appliance in the cheapest window:action: tibber_prices.find_cheapest_block data: duration: "02:00:00" search_scope: next_24h power_profile: "{{ state_attr('sensor.washer_cotton_40_c', 'power_profile') }}" response_variable: result
It is derived from the profile's learned envelope and refreshes as the profile does (the envelope is rebuilt on each new labelled cycle), and it stays empty until the profile has enough data to have an envelope. Unlike the running-cycle
reference_profileattribute (a time/watt shape on the program sensor, only present mid-cycle), this is a fixed 15-minute average exposed per profile so it can be read for planning before a cycle starts. -
Optional external energy meter as the cycle energy source (#316): Integrating a slow-reporting power sensor systematically under-counts each cycle's energy (the area between reported samples is lost), often by 20 % or more. Many smart plugs also expose a cumulative energy counter that does not suffer this loss. You can now point WashData at one via the new optional Energy Meter Entity setting (Settings → Energy). When set, each cycle's reported energy is taken from that counter's start-to-end delta, and the cycle records where its energy came from (
energy_source: "meter"vs"integration"). It is strictly opt-in: with no meter configured, behaviour is byte-for-byte unchanged. The integrated value is still always computed and stored, so profile matching, ML and the anomaly/drift statistics keep using one consistent internal figure - only the user-facing numbers (per-cycle cost, the Energy-dashboard lifetime total, the finish notification, and the panel) prefer the more accurate meter reading. Every guard falls back to the integrated value and never breaks cycle end: unknown/unavailable/non-numeric readings, an unrecognised unit, a non-positive delta (counter reset or a stuck plug), or the meter entity being changed mid-cycle. The meter snapshot survives a Home Assistant restart mid-cycle. Thanks to @kdjkdjkdj for the proposal and a reference implementation.
-
Panel no longer freezes on custom themes / Safari (#314, #315): With a theme (e.g. graphite) or Home Assistant accent colour that defines the primary colour in
rgb(...)form rather than hex, every chart that drew a shaded area fill crashed - the code built the translucent fill by appending a hex-alpha suffix to the colour (rgb(238, 147, 0)became the invalidrgb(238, 147, 0)55), which made the canvas throw and left the panel stuck on a loading spinner on every tab except the Overview, and made opening a cycle for review fail with "The string did not match the expected pattern." Fills now apply transparency in a colour-format-agnostic way, so hex,rgb(), andrgba()theme colours all render correctly. -
Manual recording no longer shows a confusing mixed state (#313): While a manual recording was running the panel could show "Recording", "Recording (Off)", and a "Start Recording" button all at once. Recording was actually working the whole time; the display was reading the recording state from the wrong places. During a recording the underlying detector is intentionally idle, and that raw "Off" state was leaking into the status badge, while the Manual Recording widget trusted a separate value that was never populated on first load. Both now follow the single authoritative recording flag, so the badge reads "Recording" and the widget shows a Stop button as soon as the panel opens. As a bonus, an idle device no longer shows a redundant "(Off)" next to its status.
-
Playground no longer stalls Home Assistant on long cycles (#311): Simulating a single long cycle (for example a ~4-hour dishwasher eco program) previously ran the entire faithful replay - thousands of iterations of the real detector, matcher and progress estimator - in one uninterrupted worker call, which held Python's global lock long enough to slow the whole Home Assistant instance to a crawl until it finished. The single-cycle "Simulate" replay now runs as a detached, cancellable background task that is processed in small chunks, yielding the event loop between each so Home Assistant stays responsive. It shows a progress pill in the panel header just like Test-on-history and Optimize already did, and the simulated timeline is byte-for-byte identical to before.
-
Splitting and trimming a cycle no longer freeze the instance (#311): Applying a split rebuilt every profile's envelope one after another on the event loop - redundant work that could block a Raspberry Pi for dozens of seconds. A split now rebuilds only the envelopes of the profiles it actually touched (the original plus any profile a segment was labeled with), and both split and trim run as background tasks with a header progress pill, so the UI stays responsive while they finish.
-
Lighter matching and end-detection on low-power hosts (#311): Two further responsiveness fixes for slower hardware. Profile matching no longer rescans your entire cycle history once per profile on every match (it now builds the lookups a single time), which noticeably speeds up the "auto-label cycles" action on devices with a large history. And the opt-in terminal-drop end-detection baseline, which reads back every completed cycle's power trace, is now computed off the main thread instead of during a live power reading. Both changes are behaviour-preserving - matching results are identical.
-
Merging cycles and rebuilding envelopes no longer freeze the instance (#311): Like split and trim, merging cycles rebuilt every profile's envelope on the event loop even though only the affected profiles changed; the redundant rebuild is removed and the merge now runs as a background task. The Rebuild Envelopes button also runs as a background task now, rebuilding one profile at a time with a header progress pill (and it can be cancelled) instead of holding the whole instance while it works through every profile.
-
Apply suggestions actually applies all suggested settings: Five settings that
reconcile_suggestionscould recommend (profile unmatch threshold, power-off threshold, anti-wrinkle exit and max power, pump stuck duration) were silently dropped by the "Apply Suggestions" action because they were missing from the internal allow-list (_SUGGESTION_KEYS). They are now applied correctly. -
Stale
auto_label_cyclesconfidence threshold default corrected: The service description listed a default confidence threshold of0.70, while the Python handler actually used0.75. Both now agree on0.75. -
Duplicate "Configuration reloaded" log entry removed: Reloading a device's settings logged the confirmation message twice. The earlier, premature log line (before state restoration completed) has been removed.
-
Dead detection code removed -
_abrupt_dropflag and its config fields: Three config fields (abrupt_drop_watts,abrupt_drop_ratio,abrupt_high_load_factor) that once drove an abrupt-drop detection branch were wired through options, manager, detector config, and the Playground override map, but the branch they guarded had been replaced long ago and was never reached. All six constants and every reference across five files have been removed. -
Duration-ratio Stage-1 fallback now uses canonical defaults: The fallback values in
analysis.pyfor the Stage-1 duration-ratio gate were0.07/1.30, diverging from the authoritativeconst.pydefaults of0.10/1.50. The fallback now readsDEFAULT_PROFILE_MATCH_MIN/MAX_DURATION_RATIOdirectly. -
Dead
WashDataStoremethods with blocking I/O removed: Two duplicate methods (get_storage_stats,async_clear_debug_data) on the internalWashDataStoresubclass usedos.path.getsize()(blocking disk I/O on the event loop) and were never reachable by any caller - the callers useProfileStoreversions of these methods, not theWashDataStoreones. -
Legacy Playground batch path removed: The old
run_playground_batch/_simulate_onecode path (~215 lines) and itsrun_playground_simulationWebSocket command were superseded by the faithful history/sweep runner in 0.5.0 but never removed. The dead code and all associated WS schema entries, type definitions, and tests have been cleaned up. -
Sweep result now includes direction for heatmap colouring:
finalize_sweep_1dandfinalize_sweep_2dnow include alower_is_betterboolean in their return payload, letting the panel colour the heatmap consistently without re-deriving the direction from the objective name. -
event_densityzero-variance ML column removed fromCycleSignature: Theevent_densityfield onCycleSignaturewas permanently0.0since the event detector was removed. It wasted a column in every ML feature vector and was a zero-variance input that provided no discriminative value.
- @kdjkdjkdj made their first contribution in #316
The biggest release in WashData's history. It retires the 180-plus-tunable options dialog in favour of a full-screen management panel, overhauls matching accuracy, rebuilds actions on native Home Assistant automations, and adds an experimental, fully-gated, NumPy-only ML subsystem that runs alongside the proven detection/matching engine. On top of that panel it also sharpens what WashData notices and how it talks to you: detection flags cycles that finish suspiciously early or draw unusual energy, energy tracking grows into a full Home Assistant Energy dashboard sensor with per-profile cost, notifications gain quiet hours and celebration milestones, the panel picks up a phase timeline and a genuine power-user Playground, and you can now simply ask your voice assistant whether the washer is done. It also opens WashData up to the community for the first time with the optional, off-by-default WashData Store: adopt program profiles and reference cycles that other owners of your exact appliance have already recorded, and share your own back, so you no longer have to teach every program from scratch.
-
Underrun and energy anomaly flags: Alongside the existing overrun signal, a cycle that finishes under 55% of its profile's typical duration is now flagged as an underrun (a ⚡ badge in the Cycles list), and each finished cycle's energy is compared against that profile's own history - a run more than 2.5 standard deviations off the norm is flagged energy high (🔺) or energy low (🔻). The two signals are independent (a cycle can be both an overrun and an energy spike), pure statistics, and purely visible - they never send a notification and never stop a cycle.
-
Create a profile straight from look-alike cycles: The Profiles coverage-gap banner now clusters your recent unmatched cycles by shape and duration, and when it finds a tight group (average correlation of 0.75 or better) it offers a one-click Create profile from N cycles button - turning "I keep running a program WashData doesn't know" into a labelled profile in a single step.
-
Profile warm-up guard: A brand-new profile with fewer than 5 labelled cycles is still learning its shape, so WashData no longer auto-labels against it - matches route to a quick manual confirmation instead, and the profile card shows a Still learning (n/5) badge until it has enough examples to be trusted. Existing, well-populated profiles are unaffected.
-
Shape drift detection: Profile health now watches for a profile's power shape slowly changing over its lifetime (comparing the average curve of its earliest cycles against its most recent ones). A profile whose shape has drifted apart raises a maintenance advisory in its detail view - often an early sign the appliance's behaviour has changed and the profile is worth re-recording.
-
Home Assistant Energy dashboard integration: A new
sensor.<name>_energy_totalreports the appliance's lifetime energy in kWh as a proper total-increasing energy sensor, so you can drop it straight into Home Assistant's Energy dashboard and track consumption over time. It accumulates once per completed cycle and is persisted across restarts. -
Per-profile average cost: Profile cards and the profile panel now show each program's average and total energy cost in your Home Assistant currency, computed from the price frozen onto every member cycle - so you can see at a glance that a hot cotton wash costs more than an eco run.
-
Richer finish notifications: The finish-message template gained three new placeholders:
{time_finished}(the clock time the cycle ended),{vs_typical}(a plain "12% longer than usual" comparison against the profile's median, empty when unknown), and{cycle_count}(your appliance's lifetime completed-cycle count). See NOTIFICATIONS.md. -
Peak-rate tip on start: If you have configured a price per kWh and set a peak-rate threshold, WashData appends a short informational line to the start notification when a cycle begins while the current price is at or above that threshold (default "Running at peak rate ({price}/kWh)."). It is purely advisory - WashData never schedules or delays your appliance.
-
Quiet hours: Set a do-not-disturb window (start and end hour, windows crossing midnight supported) under Notifications. Finish, clean-laundry, pre-completion, and milestone notifications that would fire inside the window are held and delivered the moment it ends; live-progress ticks and start notifications are never delayed.
-
Cycle milestones: WashData can now celebrate the appliance's lifetime achievements - a one-off notification fires when the completed-cycle count crosses a milestone (default 50, 100, 500, and 1000), with a customizable message (
{device}/{cycle_count}). Respects quiet hours; no new notification type or entity. -
iOS Live Activity enrichment: Live-progress notifications sent to
mobile_app_*targets now carry the extra fields an iOS Live Activity needs - a program subtitle, a structuredcontent_state(state, progress, ETA, program, device), and start/end lifecycle markers - so a companion-app Live Activity can show a live washing-machine widget on the lock screen. Android and other targets are unaffected. -
Live phase timeline: The Status progress bar now sits above a color-coded phase timeline - each phase of the matched program rendered as its own segment with a live cursor, so you can see at a glance whether the machine is washing, rinsing, or spinning. Falls back to the plain bar when the matched profile has no phases defined.
-
Duration sparkline on profile cards: Each profile card shows a mini sparkline of its recent cycle durations (last ~10 runs), a quick visual cue for whether a program's run time is steady or drifting. Hidden until a profile has at least three cycles.
-
Cycles list: pagination, undo, and bulk relabel: Long histories now paginate with a Load more button instead of loading everything at once. Deleting a cycle or a profile shows a 10-second Undo toast and only commits the delete once the window closes. The multi-select toolbar gained a Bulk relabel action to reassign many cycles to one profile at once.
-
Settings change history: Every settings save is now recorded (which field, its old and new value, and when). Changed fields show a small dot with a "Changed from X to Y on {date}" tooltip, and a Settings history table lists the recent changes - so you can see what you tuned and when, and remember what a value used to be.
-
Maintenance log and service reminders: A new Maintenance sub-tab (under Advanced) lets you log real-world upkeep - descaling, filter cleaning, drum cleaning, bearing service, or a free-form note - as a dated timeline. Set per-event reminder intervals (defaults: descale every 30 cycles, filter clean every 50, drum clean every 100) and WashData surfaces a
maintenance_duelist on the state sensor and a panel banner once you are due. Logging a relevant maintenance event also suppresses the matching drift/health advisory for 30 days, so fixing the appliance quiets the nag. No new notification type. -
First-run onboarding: A fresh device with no cycles and no profiles now shows a friendly onboarding card on the Status tab - "just run your appliance normally; after 3 cycles matching begins" - with an N/3 progress meter and a Skip link. Once three cycles are in, it prompts you to name your first program, turning the empty-panel cold start into a guided path.
-
Basic / Advanced settings: A Basic | Advanced toggle now sits atop the Settings tab. Basic shows roughly the dozen highest-impact fields for a clean first pass; Advanced shows everything. It is purely a visibility filter (hidden fields keep their values) and the choice is remembered per account.
-
Playground tab (power users): A new top-level Playground tab for people who want to see exactly what the engine is doing - all driven by the real detection, matching and estimation code, not an approximation. It is one unified workbench: a single interactive power graph is always the centrepiece, and a shared detection/matching settings panel and a bottom "Across your cycles" drawer both feed it. Pick any stored cycle (matched by auto-detect by default, so you see what the matcher itself would pick) and press Run (or Cancel) to replay it exactly as the integration would - the real detector state band, phase bands, and event pins whose heads sit above the graph on stems down to their real time (detected → match committed → notification points → finished); hover a pin for a plain-language explanation of what that event did. A side rail of alerts (overrun, did-not-finish, unmatched, ambiguous, …) now also flags how the cycle ended - when it stopped only because power stayed low for the off-delay timeout (rather than smart end-prediction), or would have run indefinitely - so you can see whether an auto-detected cycle actually ends on its own. The simulated match now mirrors the live integration faithfully: it reports the committed program (persistence-gated, held) rather than the raw per-interval top candidate, so mid-cycle profile flips that never happened live are absent from the simulation; and the analysis panel separates Match confidence (the real matcher score, same as the readout strip) from Envelope fit (how the shape sits inside the saved envelope - a different lens), making the two independent numbers easy to read without confusion. A live readout strip shows model-estimated time-left, progress, match confidence and current phase. The graph is fully interactive: hover to read time-from-start / time-to-end / power at the cursor, scroll to zoom, drag to pan, double-click to reset. Drag the start/stop threshold lines, or edit any setting, and the simulation re-runs. Every knob in the settings panel is a real, user-settable option (all detection knobs are fully wired; the matching group exposes the two duration-ratio gates you can configure), and Save to settings copies everything you edited straight into the device's live settings in one click - no retyping. The drawer holds two lenses that drive the same graph in place (no back-and-forth): Test on history replays your recent cycles into a per-cycle results table with a before/after diff (which cycles became correct, which regressed, whose end-timing changed) - click any row to load that cycle into the graph above; and Optimize finds the value of any setting that best meets an objective (match accuracy, end-timing accuracy, false-end rate, duration off-target, ambiguity) as a 1D curve, or sweeps two settings together as a 2D heatmap, then applies the best value in one click. Both run as background tasks (see below) with a live progress bar, ETA and a working Cancel, so the panel stays responsive and keeps loading other data instead of freezing while a batch runs. The graph still overlays the profile's power envelope and the DTW alignment with the full Stage-2 / DTW / Stage-4 score breakdown, so you can see why a cycle matched (or didn't).
-
Background tasks you can actually see: long operations - the Playground's Test-on-history and Optimize, plus Process history (reprocess) and on-device ML training - now run as tracked background tasks on the server instead of being tied to the open tab. A header activity pill shows each running task (device and what it is doing), and - crucially - a task survives a backgrounded tab or a dropped connection: reopen the panel and it re-attaches to the running task and still shows its state and, when finished, its result. The Playground's batch runs (Test-on-history and Optimize) are chunked, so their pill shows a live progress bar and ETA and a Cancel that stops the run promptly; Process history reports phase-by-phase progress (matching → golden → suggestions → training → health) and cancelling it stops at the next safe checkpoint, skipping whatever phases remain (matching, golden, suggestions, training or health, depending on when the cancel lands); ML training itself runs to completion in one pass (no interim percentage). Test-on-history and Optimize also keep a Recent runs list so you can reopen a past run's results without recomputing them. No more work churning invisibly with no way to see it or get the result back.
-
Ask Home Assistant if it's done: WashData now answers natural-language questions through Assist (voice or text) - "is my washer done?", "how long until the dryer finishes?" - with a live, plain-language reply ("still running, about 20 minutes left" / "finished 5 minutes ago" / "not running"), disambiguating by appliance name when you have more than one. Because Home Assistant does not let a custom integration inject sentences into the built-in voice agent at runtime, you wire the trigger phrases with a small
custom_sentencespack (one line to copy; see NOTIFICATIONS.md); the intent itself works immediately from automations and the Assist pipeline. -
Per-cycle energy cost: The Cycles list has a new Cost column. Cost is frozen onto each cycle when it finishes (energy × the price per kWh in effect at that moment), so later price changes never rewrite historical costs. Shown in your Home Assistant currency; set a static price or a price-entity under Settings to populate it.
-
ML end-detection guard (experimental, opt-in): When ML models are enabled for a device, the cycle-end model acts as an anti-premature-stop guard - if it judges a low-power lull to be a pause rather than the true end, it defers the normal completion and keeps the cycle running until the model agrees the cycle has really finished. It is one-directional (it can only delay a finish, never end a cycle early), bounded (a wrong model adds at most 30 minutes, never hangs a cycle), only engages once the matched profile is confident enough to trust, and never touches the force-stop / smart-termination / manual-stop paths. Prefers a device's on-device-trained end model, falls back to the shipped baseline. Off by default; the proven power/energy end detection is unchanged when disabled.
-
Terminal-drop fast finalize (experimental, opt-in): The counterpart to the end-guard. Normally a cycle whose power drops to zero is held open for several minutes (up to 8 for washing machines, an hour for dishwashers) before it is finalized, because that quiet period could be a legitimate soak / drying pause - the integration can't tell a pulled plug from a soak just from "0 watts". When ML models are enabled, WashData now learns, from the device's own completed cycles, the earliest point at which that appliance has ever legitimately gone quiet. A hard power cliff-to-zero that happens earlier than that is treated as a genuine stop (plug pulled, program cancelled) and the cycle is finalized in ~1–2 minutes instead of waiting out the full soak window. Like the end-guard it is strictly one-directional (it can only ever shorten the wait, never end a normal cycle early), needs a few completed cycles before it will act, ignores interrupted cycles when learning the baseline, and is completely inert when the ML opt-in is off. As a safeguard it also defers on unfamiliar cycles: if the running cycle is drawing power unlike anything the appliance has produced before (so it might be a brand-new program you're running for the first time, whose normal behaviour just isn't learned yet), the fast finalize stands down and the proven slow detection takes over. The cycle is still recorded as Interrupted - this only makes the integration react to it sooner.
-
Full-Screen Management Panel: A new WashData entry in the Home Assistant sidebar opens a dedicated panel with Overview, Cycles, Profiles, Settings, ML Training, and Advanced tabs, backed by a new WebSocket API. This is where you manage WashData now: the integration's Configure dialog is reduced to onboarding essentials only (device type, power sensor, minimum power, and an optional first profile), and everything else - detection tuning, profiles, phases, notifications, diagnostics, export/import, access control - moved into the panel. The layout is mobile and touch friendly (responsive grids, larger touch targets, portrait support) and auto-busts its browser cache on every update.
-
Overview dashboard: The panel's landing tab. Live state, power, progress, and time remaining, with a device bar across the top for switching between appliances, and attention cards that surface pending tuning suggestions and cycle-review items and jump straight to them. The Manual Recording widget lives here too - hit Start Recording before you run a new program to capture a clean reference cycle. A labelled live power chart, served from the integration so it survives a browser refresh (and the in-progress cycle trace survives a restart), with a cursor-following readout showing time from start, time to end, power, and envelope range. When a profile is matched its full expected curve is overlaid in faint orange; an optional Raw socket overlay draws the configured power sensor's recorder history for the running cycle side by side with the integration's processed and sampled trace. An optional Live Match Debug card shows confidence, match ambiguity, and the top candidate profiles.
-
Program selector on Overview: Choose the running program, or return to auto-detect, directly from the dashboard. The selection is tagged (auto-detected) or (manually selected) and drives the same manual-program mechanism as the program-select entity, so the panel, the entity, and automations stay in sync.
-
Interactive Cycles tab (#272, formerly History): A compact, history-sorted list. Click a cycle to inspect its full power curve and label, delete, trim, or split it. The per-cycle inspect view exposes the complete power trace for every stored cycle - directly addressing requests to retrieve a cycle's power profile for use with external tools (e.g. Tibber pricing windows). Trimming uses draggable handles with a seconds-from-start or clock-time input toggle. Splitting auto-detects idle gaps or lets you place split points on the graph, assigning a profile per segment. A selection mode adds multi-cycle Compare, Merge, and bulk delete, and retroactive auto-label moved into the toolbar. Compare overlays the selected cycles on one graph - each cycle has a show/hide toggle (colour-matched to its trace), and a second set of toggles overlays any learned profile's expected envelope, so you can eyeball how several runs line up against each other and against a program.
-
Profile control panel: Open any profile for statistics (cycle count, durations, average and total energy, consistency, last run), its average envelope graph, an interactive phase-range editor with draggable boundaries over the average curve, history cleanup (every member cycle overlaid; hover to identify a curve and click or tick it to delete outliers), and rename / rebuild / delete. Phase-catalog management (adding, editing, and removing the phase labels themselves) also lives in the Profiles tab.
-
Profile phases now drive estimation, not just labels: The phase ranges you draw on a profile (Pre-Rinse, Wash, Rinse, Soak, Spin, Dry, ...) are no longer a purely cosmetic overlay - they are wired into the phase-aware progress and time-remaining estimator. The current-phase readout and the remaining-time estimate are derived by indexing your phase ranges with the (ML-blended) progress fraction, so both stay correct when a cycle runs longer or shorter than usual. One phase definition powers the editor, the live phase readout, and the estimate.
-
Inline tuning suggestions: Suggested values appear beside the relevant settings fields with a one-click Use and an Apply all banner. Each setting has a hover explainer with a small diagram drawn in the browser, and the Settings layout was reworked to be roomier and grouped.
-
Advanced tab (#281): One home for the power-user and admin surfaces, split into sub-tabs. My Preferences holds per-account display options (default landing tab, cycle date format, whether to show the expected-curve overlay and the Live Match Debug card). Diagnostics shows storage stats plus maintenance actions - Process History (re-run matching and ML retraining across all stored cycles after a batch of reviews), Clear Debug Traces, and Wipe All Data - and config export / import (paste JSON or load a file; import also accepts a Home Assistant diagnostics download). Panel Settings lets an admin set the default landing tab for everyone and hide tabs from non-admins. Access Control is the per-user RBAC screen (below). (Phase-catalog management moved to the Profiles tab, and learning-feedback resolution is handled from the Overview attention cards and the per-cycle Review view.)
-
Role-Based Access Control: Not everyone in the house needs to change settings. RBAC lets an administrator hand a partner or a roommate simple read access - so they can check whether the machine is done without being able to touch its configuration - while keeping edit or full access for themselves. Set a per-device level (none, read, edit, or full) per Home Assistant user, with a per-user default, from Advanced → Access Control. Access is enforced on every WebSocket command on the server, not only hidden in the UI: read users get a mostly view-only panel with hidden devices removed (they may still pick the running program, a live-only action), edit users operate normally, and destructive or export actions require full access. Administrators always have full access. Access control is off by default, so existing single-user setups are unaffected.
-
Panel language override: Under Advanced → My Preferences, a Panel language dropdown forces the WashData panel into a specific language for your account only. If a translation is incomplete or reads awkwardly, switch it to English and the panel re-renders in English immediately - no Home Assistant restart, no effect on other users, and no effect on Home Assistant's own language (only the WashData panel is affected).
-
Logs: A live view of the recent WashData log stream, read from an in-memory ring buffer, with export. Filter by level, device, and component (e.g. show only the Playground or the matcher) and search the messages - all applied instantly client-side. It opens as a slide-in drawer that sits side-by-side with the rest of the panel instead of navigating away, so you can read logs and click through the panel at the same time; drag its edge to resize, and the width and open/closed state persist across refreshes. The drawer auto-refreshes while open.
-
Higher matching accuracy: The profile-matching pipeline was re-tuned end to end - the correlation/MAE similarity split, the duration and energy agreement terms, and the DTW refinement stage, which gained a derivative-DTW (DDTW) "ensemble" mode. In leave-one-out evaluation on real-world data this lifted top-1 program identification substantially (from about 62% with DTW disabled into the low 70s, with further gains for grouped near-duplicate programs). The MAE term is now expressed relative to each cycle's peak power, so the same proportional error scores equally on low- and high-power appliances. All scoring constants remain centralised in
const.py. -
Profile groups (#288, work in progress): Group near-duplicate profiles (e.g. the same program at different temperatures, or lower-spin variants that run longer) so the matcher treats them as one candidate and then picks the best-fitting member. Cohesion is measured with a duration-tolerant comparison of peak-normalised envelopes, so only genuinely similar profiles are collapsed; loose groups are left as individual profiles. The panel suggests near-duplicate clusters and provides a group control panel showing the aggregate envelope with each member overlaid, plus its cohesion. Safeguards keep an over-generic group from overpowering a genuinely better single profile (an ambiguous group win falls back to the uncertain/feedback path, and the chosen member must fit reasonably on its own). This is aimed at amplitude variants - profiles whose power shape is identical but peak amplitude differs (different temperature, different spin speed). Grouping, the clustering suggestions, and member selection are new and still maturing; expect them to get sharper in the coming releases.
-
Cycle Timers with auto-pause (#294): Configure one or more timers that fire at specific minutes into a cycle - e.g. "5 minutes in: add bleach softener" or "120 minutes in: resume the soak". Each timer sends a notification to your configured services at the set offset; enabling Auto-pause on a timer also pauses the cycle automatically at that point and delivers an interactive notification with a Resume Cycle action button. The cycle can be resumed from the panel, via the
ha_washdata.resume_cycleservice, or directly from the notification. All time metrics (elapsed time, time remaining, and progress) exclude time spent paused so the estimates stay accurate across a multi-hour soak. -
Notifications built on native automations (#287): The old bespoke "notification actions" editor never had a home in the new panel - a custom in-house action builder isn't something the panel could sensibly surface or edit - so notifications are now driven by ordinary Home Assistant automations instead. Settings → Notifications → Automations lists the automations that already reference this device (deep-linking to the automation editor) and offers a New Automation split button with two ready-made templates: one prefilled with a cycle started trigger and one with a cycle finished trigger for this device. WashData fires
ha_washdata_cycle_started/ha_washdata_cycle_endedbus events and exposes entities, so any side effect is built exactly like any other automation (see the Notifications & Automations guide). Notify-service and people targets are compact entity-pill pickers with a domain-filtered add box and one-click removal. Any custom actions carried over from an older setup keep firing and are not dropped; they will be resurfaced so you can migrate them into automations (or remove them). This change also resolves the'str' object has no attribute 'async_render_to_info'error that complex actions withchoose/if/repeatblocks could throw, since notification actions now run through Home Assistant's standardScriptengine. -
Experimental ML subsystem + dedicated ML Training tab (opt-in, work in progress): A new NumPy-only ML subsystem, off by default and gated behind feature flags, runs alongside the proven detection and matching code and never replaces it. It ships embedded baseline models (cycle-end detection, curve quality, live-match commit), and all ML management is consolidated in a dedicated ML Training tab: a plain-language status card (personalized vs built-in, with a data-readiness bar), two independent toggles (apply models during a live cycle / let WashData retrain), a manual "Train now", a "what WashData has learned" list with a fit chip per capability, and program-matching fine-tuning (with revert). Per-cycle ML health / review stays inline in the Cycles tab and ML-calibrated tuning suggestions appear beside the classic ones. Optional on-device retraining only promotes a model when it beats the shipped baseline on held-out data. When the feature is off, the UI and logic stay inert. This whole subsystem is an early foundation - enable it if you want to help it learn, and expect its accuracy to improve substantially over subsequent releases.
-
On-device matcher tuning: When on-device training runs, WashData also tunes how much the matcher weighs curve shape versus duration and energy agreement, and how much its DTW ensemble leans on the derivative (DDTW) component - using the same held-out promotion discipline as the models. Device-specific weights are adopted only when they beat the shipped defaults on held-out cycles, and only bounded scoring weights can change (never structural matching behaviour). A Matching Tuning card in the ML Training tab shows the defaults, the in-use weights, when they were tuned, and a Revert to defaults button.
-
Richer cycle and profile review: The cycle-relabel view can overlay any of the device's profiles for quick visual comparison, per-cycle ML quality / golden review labels feed back into on-device training, and multi-select on the Cycles page uses a reliable per-row checkbox.
-
Remaining-time prediction that learns your appliance (experimental, opt-in): A new on-device regression model predicts how far through a cycle you are from the power curve's shape rather than assuming every run takes the profile's median duration, so time-remaining stays accurate when a program runs longer or shorter than usual. It blends gently into the existing phase-aware estimate (never wholly overriding it) and is inert until on-device training promotes a model that beats the naive estimate on your own cycles. There is no shipped baseline, so behaviour is unchanged until then.
-
Projected energy & cost for the running cycle: The progress sensor now exposes
projected_energy_kwhandprojected_costattributes for the in-progress cycle, derived from energy consumed so far and the (ML-blended) progress, using the same price you configured for completed-cycle cost. Present only once a reliable estimate exists; no new entity. -
Profile recommendations: The Profiles tab shows a Recommendations panel with actionable, plain-language advice derived from each profile's health and drift - e.g. "this profile's cycles are running progressively longer, consider re-recording" or "low fit score, review its cycles." Pure statistics, surfaced in the existing tab (never a notification).
-
Anti-wrinkle mode now available for washing machines (#296): The anti-crease / anti-wrinkle state was previously gated to dryers and washer-dryers only. Washing machines with a post-cycle tumble mode (e.g. Miele "Knitterschutz") exhibit the same pattern - a low baseline draw with occasional short motor bursts until the door is opened - so the feature is now enabled for washing machines too. Enable Anti-Wrinkle Shield in Settings; the detection logic and the
STATE_ANTI_WRINKLEstate are identical to the dryer implementation. Real-world setup note (tested on Miele washers and dryers with a constant ~2.5–3.2 W anti-crease baseline): set Stop Threshold above the anti-crease baseline (e.g. 4 W), use a shorter Off Delay (~150 s, less than the burst interval), and lower End Energy Threshold to ~0.25 Wh so the energy gate doesn't absorb the constant baseline draw. The Anti-Wrinkle Exit Power threshold (default 0.8 W) must sit below the anti-crease baseline (it marks the true-off state, not the baseline itself); the default value is correct for most machines and should not be raised above the baseline. -
New device types: "Other (Advanced)" and "Threshold Device" (#228): Two catch-all types for appliances that aren't one of the named categories. Threshold Device (#228) is for truly uncategorised appliances where you only need simple threshold-based cycle detection: it does no profile matching or learning, and ships deliberately generic detection defaults that you tune yourself to your appliance. The new
generic"Other (Advanced)" type is instead for predictable appliances that do have a reproducible power signature but don't fit a named category (washing machine, dryer, dishwasher, air fryer, bread maker, pump): it ships neutral defaults and takes part fully in profile matching, learning, and the ML features. (What used to be labelled "Other (Advanced)" - internal keyother- is now the Threshold Device; the internal key is unchanged, so existing entries keep working exactly as before.) -
Live panel - no more manual refresh: The management panel now refreshes automatically whenever Home Assistant entity states change (via
set hass()) as well as on a 5-second background poll, so the dashboard, cycles, and profiles stay current without any manual page refresh. The Refresh Interval setting has been removed from Panel Settings. The panel also subscribes toha_washdata_cycle_startedandha_washdata_cycle_endedevents, so cycle completions appear within seconds. -
Power-based Off detection (#284, opt-in): A new Power Off Threshold (with a Power Off Delay) under Settings, in Detection, lets WashData return a finished machine to Off when it is actually switched off rather than after a fixed timer. Once a cycle has finished, if power stays below the threshold for the delay, the state returns to Off and the Clean overlay clears, giving "laundry is waiting until I switch the machine off" automations a real off event to trigger on, especially on machines without a door sensor. Set the threshold above the appliance's true switched-off floor and below its finished-but-on standby draw (it is ignored unless it sits below the Stop Threshold). It is evaluated only in the terminal state, so a mid-cycle soak can never be mistaken for off, and it is disabled by default (threshold 0), leaving existing behaviour unchanged. When enabled, the machine stays in Finished/Clean until it is genuinely powered off (the progress-reset timer no longer forces Off); a machine whose standby never drops below the threshold stays Finished by design, so choose the value with that in mind.
-
HA restart gap detection: When Home Assistant restarts mid-cycle, the integration now records the dark period (from the last saved timestamp to restoration time) as a restart gap on the cycle. Restart gaps are surfaced in two places: a blue ↻ badge in the Cycles list (hover for the gap duration), and a shaded region on the power trace in the cycle detail view with a plain-language summary below the graph. Matching always uses only real sensor readings; no synthetic fill is injected into the power trace, so matching accuracy is unaffected. The metadata is available to automations and external tools via the
restart_gapsfield on the cycle record. -
Cycle anomaly signals (work in progress): WashData now flags irregularities without ever nagging you. A running cycle that materially exceeds its matched profile's typical duration gets a soft overrun signal (a
cycle_anomaly/overrun_ratioattribute on the state sensor and a ⏱ badge in the Cycles list). Separately, each finished cycle's power trace is scanned for transient artifacts - a pause (e.g. a door opened mid-cycle), or a sustained out-of-band dip or spike - which are shaded on the cycle graph and listed in its Inspect view with a ⚠ badge in the Cycles list. These are purely visible signals: they never send a notification and never end or force-stop a cycle (the existing safety timeouts are unchanged). Anomaly detection is new and deliberately conservative; it will get more accurate as the underlying heuristics and models mature. -
Revert on-device models to baseline: The ML Training tab gained a Revert models to baseline button that discards all on-device-trained models and falls back to the shipped baselines (training may re-promote them later), mirroring the existing matcher-tuning revert.
-
Forward-looking reference power curve (#304): While a cycle is running and matched,
sensor.<name>_programnow carries areference_profileattribute - a compact[[offset_s, watts], ...]curve of the matched program's expected power over time (plusduration_sandcycle_count). Home energy managers can slice it from the live progress position to anticipate later load (for example a heating spike still to come) and make a smarter "ride out a dip vs. throttle another load" decision than a scalar time-remaining allows. It is read-only exposure of data WashData already learns - no detection or matching behaviour changes. The curve is downsampled to stay under ~1 KB, is present only once a real profile is matched (absent while detecting or unmatched, so consumers can tell the difference), and is excluded from the recorder database since it is a live forecast with no historical value. -
WashData Store - community catalog (opt-in): A new optional online feature set (disabled by default, enabled under Settings → Store) connects your integration to the WashData Store, a community-maintained catalog of appliances, programs, and reference power cycles. The store is entirely opt-in: your device and profile data is never uploaded unless you explicitly share it. Enabling online features does make outbound network requests (catalog browsing, downloading reference cycles, and GitHub authentication when contributing) — no device or cycle data leaves Home Assistant without explicit sharing, but network activity begins as soon as online features are turned on. Key capabilities:
- Browse and adopt: Declare your appliance's brand and model in Settings (with brand and model dropdowns fed from the live catalog). Once declared, WashData can download community-contributed reference cycles for your device and import them as imported profiles - giving you a head start on program recognition without recording every wash yourself.
- Share a device package: A share tree in the Settings tab lets you select which profiles and their reference cycles to upload. Settings (tuned detection thresholds) and phase maps can optionally be included per profile. Shared packages are uploaded as pending contributions and become searchable by other users once the community confirms them.
- Import from a share link: Paste a share link from another user to import their device package directly. Adoption brings in brand, model, profiles, and optionally tuned settings and phase maps - one click to bootstrap a new device with community data.
- Sign in with GitHub: The store requires a GitHub account for contributions (community voting, uploading). Browsing and downloading are always anonymous; your GitHub identity is used only for uploads and for the community confirmation vote.
- Community quality control: Uploaded cycles are community-voted. Once a device or cycle accumulates enough confirmations from distinct users (default threshold: 5), it is auto-promoted to approved. Admins can adjust the threshold and approve or remove content through a dedicated admin panel.
- Identity cleared on opt-out: Disabling online features clears the stored store account from WashData's data, so no identity is silently retained after opting out.
-
Setting conflict validation with coherent cascade: The Settings panel detects and highlights incompatible setting combinations inline, before saving. When two settings conflict (e.g. Start Threshold below Stop Threshold), a red error row appears under each affected field with a clickable Use X button. Clicking it now cascades automatically: if fixing Start Threshold means Stop Threshold must also drop, and Stop Threshold means Min Power must also drop, all three are adjusted in one click - fields in the current section update their DOM inputs; fields in other sections are updated via
this._optsand tracked in_cascadePendingso the next Save includes them. Clicking a tuning suggestion's Use button also triggers the same cascade immediately, so staging one suggestion auto-fixes any downstream settings that need to move with it. Saving is blocked while any conflict is unresolved. Conflicts in the saved settings are surfaced in the Overview tab (red attention card, ⚠ tab-label indicator, red section-pill dot). The backendreconcile_suggestions()is now direction-aware and cascade-creative: it determines which setting is the intended anchor (e.g. a lowered Start Threshold) and cascade-creates consistent entries for dependent settings (Stop Threshold, Min Power) so the full suggestion set is jointly valid before it's ever shown. A fixpoint loop (up to 8 iterations) ensures multi-step chains converge. A new Revert changes button sits next to Save and Refresh: it snapshots the settings state before each save and can restore it in one click, even after saving - disabled until the first save in the session. Conflict rules are now device-type-aware: the anti-wrinkle constraints (exit power vs stop threshold, max power vs start threshold) only fire for washing machines, dryers, and washer-dryer combos; the pump-stuck-duration constraint only fires for pump devices. This prevents spurious conflict errors on dishwashers and other appliances where those features don't exist. Conflict-suggestion coherence: when a conflict fires and a pending tuning suggestion for the affected setting would resolve it, the panel shows "Stage the pending suggestion (X) below to fix this" in place of the generic Use X fix button - directing the user to the already-computed, data-backed value rather than a mechanical minimum. Fully localised across all 35 supported panel languages. -
Suggestion quality gates: The suggestion engine now applies two gates before surfacing a recommendation. (1) Minimum significant delta: a suggestion is only stored when it represents a real change - at least 8% relative change or a key-category absolute minimum (0.3 W for power settings, 5 s for time settings, 0.02 for ratios and confidence scores). Noise changes like 0.67 → 0.68 are silently discarded before they can appear. (2) Post-apply cooldown: after the user applies suggestions, no new suggestions are surfaced for the next 3 completed cycles. This prevents the engine from immediately re-suggesting a slightly-different value based on a single new cycle, and stops the conflict-resolution / suggestion-engine feedback loop where applying a conflict-mandated value (e.g. raise Stop Threshold to 2.0) could cause the suggestion engine to immediately suggest lowering it again.
-
Clearer suggestion labels with per-setting impact explanations: When the Settings panel shows a tuning suggestion, the generic "Classic" and "ML" labels are replaced with Observed (what your appliance's data statistically shows) and Calibrated (what would improve detection accuracy based on past outcomes). When both engines agree within 5%, they collapse into a single WashData recommends recommendation - no ambiguity. When they diverge, a one-line tradeoff note appears below the two values explaining exactly what choosing each will do for that specific setting. For example, for Stop Threshold: "💡 Ends cycle faster - may close during a pause · 🤖 Waits longer to end - fewer premature completions". Impact explanations cover Stop Threshold, Start Threshold, Off Delay, Min Off Gap, Auto-Label Confidence, Profile Match Threshold, Watchdog Interval, No-Update Timeout, Learning Confidence, Min Power, Duration Tolerance, End Energy Threshold, End Repeat Count, and Running Dead Zone. Fully localised across all 35 supported panel languages.
-
ML suggestions surfaced in overview: ML-calibrated setting recommendations appear beside the yellow suggestion dot and the attention card in the Overview tab, the same way classic statistical suggestions do.
-
Built-in phases fully editable: All phases in the per-profile phase editor - including the default/built-in ranges - can be edited. They remain un-deletable (only custom phases can be deleted), and built-in phases display a "Built-in" tag with a note that changes override the default.
-
Settings switches & tooltips: Boolean settings render as switches instead of checkboxes (matching the label styling of every other setting), tooltips are positioned correctly, every setting now has an explanatory tooltip, and the Triggers & Door section is split into clearer subcategories (External End Trigger / Door & Pause / Unload Reminder).
-
Live suggestion state: Accepting a tuning suggestion immediately updates the category indicator and the "N suggestions" count in the panel (reverted on refresh if not saved).
-
Training status: The training indicator is per-device (starting a run on one appliance does not affect the display for others) and reflects the real backend state after a page refresh.
-
Profile health, drift, and coverage insights (pure statistics, no ML): the Profiles tab surfaces per-profile fit/consistency health, duration/energy drift trends, and a coverage-gap hint when many recent cycles go unmatched (with a create-profile shortcut). These also power the new Recommendations panel.
-
Envelope conformance as a second safety check: at cycle end WashData measures how much of the trace falls within the matched profile's power band; a poorly-conforming cycle downgrades a high-confidence auto-label to a review request (complementing the ML quality gate), catching matches that fit in shape but not in power level.
-
live_matchmodel retrains on-device: compact per-cycle match-ranking snapshots accumulate during normal matching and become the training set for the live-match commit model, so it can personalize to your appliance like the other on-device models. -
Train nowand status show every model: the ML Training tab lists each promoted on-device model with when it was trained and its held-out metric (including the new remaining-time regressor). -
Conflict badge on all device cards: The device selector bar shows a red
⚠ Nbadge next to any device that has N unresolved setting conflicts, not only the currently selected one. Conflict counts are derived from the same rules used in the Settings tab and applied to each device's options already present in the WebSocket payload, so no extra round-trip is needed. -
Conflict banner and jump-to-section in Settings: When any setting conflict is active, a red banner appears at the top of the Settings tab stating the conflict count and offering a Go to first button that switches directly to the first section containing a conflicted field. This makes it easy to locate and fix a conflict in a section that is not currently visible without manually clicking through each section tab. New translation keys:
conflict.settings_banner,conflict.settings_banner_btn.
-
Playground and background-task commands are available without a manual restart: WebSocket handlers re-register on every setup call so all commands work immediately after a plain integration reload. The panel's restart prompt self-clears once the commands are live, and a regression test now fails if any registered handler is missing its decorator.
-
Panel updates are push-driven: state changes arrive via Home Assistant's entity-state push plus cycle/task event subscriptions, with a coalesced refresh and a slow ~20s safety heartbeat. No busy-polling or unnecessary background chatter.
-
Logs view captures activity at the configured log level: at Home Assistant's default INFO level, cycle lifecycle events appear without any extra setup. An empty view distinguishes "nothing logged yet" from "filtered out." Raise the integration's log level to capture more detail.
-
Back-to-back cycles no longer clobber each other: when one cycle's end-of-cycle processing overlapped the start of the very next cycle, the cleanup could reset the newly-started cycle back to Off mid-run. Cycle-end processing now detects that a new cycle has begun and leaves its live state untouched.
-
No more phantom "cycle finished" alerts from noise: a very short, near-zero-energy power blip is now fully suppressed (the way dishwasher pump-outs already were) instead of being stored as a cycle and firing a finish notification.
-
Interrupted cycles survive a restart with their trace intact: a cycle resurrected after a Home Assistant restart mid-run no longer loses its pre-restart power trace (a timestamp-format mismatch had silently dropped it), and a stale verified-pause flag can no longer carry into the next cycle.
-
Accessibility: keyboard focus rings on all controls,
prefers-reduced-motionsupport, screen-reader roles on toasts and dialogs, and text labels on every chart canvas. Status/health colour tints now render correctly against custom themes. -
On-device ML training: the quality model trains on real per-cycle anomaly-flag counts, and held-out evaluation groups rows by source cycle to prevent same-cycle data leakage - a personalised model is promoted only when it genuinely generalises to unseen data.
-
Panel language and date-format preferences are saved per user: the Panel language and Cycle date display choices under My Preferences are persisted and restored per Home Assistant user account.
-
Auto-pause timer sends the Resume notification only after the pause has taken effect: if a pause cannot apply (wrong state, or a switch that fails to cut power), no Resume notification is sent, so there is never a stuck interactive notification with a non-functional button.
-
Tuned settings survive a reconfigure: the config dialog now consistently reads your current (panel-edited) options first, so re-running setup can no longer fall back to stale defaults for device type, power sensor, or minimum power.
-
Security hardening: destructive WebSocket actions (wipe history, import/export config, reprocess history, clear debug data, trigger ML training) now require an administrator, and configuration export/import is restricted to Home Assistant's allowed paths and refuses to overwrite an existing file. Fired cycle-ended events are also trimmed to stay safely under Home Assistant's event-size limit. When per-user access control is enabled, the background-task commands (watch progress, cancel a task, read a finished result) now resolve each task's owning device and check the caller's access to it, so a restricted user can no longer read or cancel tasks on a device they aren't authorized for by omitting the device id; and the integration-wide community-store preferences now require an administrator, matching the connect / online-toggle actions.
-
Warm-up guard is consistently enforced: a still-learning profile (under 5 labelled cycles) always routes to a manual confirmation regardless of confidence-threshold configuration; the confidence shown is always the real match score.
-
Broad stability pass: an extensive automated code review across the whole 0.5.0 branch tightened dozens of edge cases in cycle detection, profile matching, notifications, and the ML subsystem (input validation, restart handling, and background-task safety), with no change to the tuned matching behaviour.
-
Cycle milestone notifications start from your existing history: the lifetime completed-cycle counter is seeded from your stored cycle history on first run, so an established appliance does not announce the 50 / 100 / 500 milestones it has already passed.
-
Community Store sharing uploads correct cycle stats: when sharing a program, imported and recorded cycles upload their stored duration, energy, and signature. Re-adopting a device package reapplies phase ranges even when there are no new cycles to import.
-
A configured price of exactly 0 is handled correctly: the running cycle's projected cost shows 0.00 (a free or zero-cost tariff) rather than blank.
-
Clearer configuration errors: the options dialog now shows a localized message when the minimum power threshold is not greater than 0, and a failed Assign profile service call now reports the actual reason instead of a generic "cycle not found."
-
Playground Optimize sweep progress is accurate: the progress bar and percentage correctly track the run from start to finish.
-
Serbian (Latin) card labels corrected: the WashData Lovelace card's Serbian (Latin) locale was showing Cyrillic, machine-translated text; it is now proper Serbian in Latin script.
-
Dishwasher Smart Termination uses a fixed 300-second confirmation window: the window is independent of
off_delay, so a large tuned or suggestedoff_delaydoes not delay cycle finalization. A matched dishwasher ends at its real cycle end and the finish notification arrives on time. (Smart Termination is still gated on reaching the expected duration, so the shorter window cannot end a cycle early.) -
off_delaysuggestions are no longer inflated by the drying phase: the classic ("Observed") and ML ("Calibrated") off-delay recommendations counted a resumed intra-cycle pause whenever any activity followed a quiet stretch, so a dishwasher's terminal pump-out or vent tick (a few dozen watts, a sample or two) right before the cycle ends turned the whole ~35-min drying tail into a giant "pause", pushing the suggestedoff_delayup toward 2000s. A low stretch now only counts as a real pause when the appliance resumes into sustained activity, so the recommendation reflects genuine mid-cycle pauses. -
Process History reconciles cycle duration data: cycles whose stored
durationandend_timedo not agree with their power trace are corrected so all three values are consistent. Healthy cycles - including dishwashers with a legitimate near-zero drying tail - are left untouched. -
Smart Termination could split one wash into several cycles when two similar programs exist (#288): When a device has a short program ("Quick 40°C", ~46 min) and a longer program whose first half looks almost identical ("Normal 40°C", ~88 min), the matcher would lock onto the shorter one during the opening minutes of a Normal wash - because the traces really are the same up to that point. If the machine then went quiet during a mid-cycle soak, Smart Termination could fire at the short program's expected duration and close the cycle early, leaving the remaining 30+ minutes to be re-detected as one or more extra cycles. WashData now recognises this "prefix ambiguity" - a plausible longer program that an ongoing soak could still be inside - and in that case blocks Smart Termination, falling back to the power-based timeout so a Normal wash is no longer chopped into pieces. For a genuine Quick cycle this only delays completion by a few minutes.
-
CleanState Never Shown for Washing Machines (#282, #283): A completed cycle ends inSTATE_FINISHED, butcheck_state()only surfacedSTATE_CLEANforSTATE_OFF, so with a Door Sensor configured theCleanstate was unreachable andsensor.<name>_stateshowedFinished.check_state()now reportsCleanfor finished cycles too. Reported and fixed by @Olen. -
Signal and other strict-schema notify platforms rejected extra keys (#286): Notification payloads previously included mobile-app-specific fields (
tag,timeout,channel,priority,actions,sticky) for every configured notify target, causingvoluptuous.error.MultipleInvalid: extra keys not allowed @ data[tag]on platforms such as Signal Messenger and Pushover whose schemas do not accept those keys. These fields are now added to the payload only when the target service starts withmobile_app_; all other services receive the basemessage/titlepayload only. -
Notify entities silently dropped notifications (#297): When a modern Home Assistant notification entity (e.g.
notify.iphone_*) was configured as a notification target, notifications were never sent. The dispatch path correctly detected the entity, then fell through to a legacyhass.services.async_call("notify", "iphone_*", ...)call that silently fails; modern notify entities are not registered as legacy services and must be called vianotify.send_messagewithentity_idinstead. Fixed: the dispatch now callsnotify.send_messagefor any target that resolves to a notify-domain entity in the HA state machine. -
Notify Group entities missing from the notification target picker (#290): Notify groups created through the Home Assistant UI (e.g.
notify.group_home) are registered as standard HA entities, not as legacynotify.*services, so they were absent from WashData's notification target autocomplete list. The Settings panel entity picker now merges both legacy service names (fromhass.services.notify) and entity-registry notify entries (fromhass.states), so all notify targets (legacy services, mobile app entities, and UI-created notify groups) appear in the suggestion list. -
Entities named in the server's language, not the viewer's (#279): WashData resolved its entity names in the Home Assistant server (system) language, so a user whose own profile was set to English could still see the entities named in whatever language the server was configured in. Entity names now resolve in the user's language context instead of the server context, so they follow the language of whoever is viewing them.
-
Dishwasher eco cycle truncated: tail not captured (#291): On eco programs with a long passive-drying tail and a small power bump at the very end, the cycle was being closed before the tail completed, producing a shortened cycle record with missing end data. The dishwasher passive-drying deferral in
_should_defer_finishnow keeps the cycle inSTATE_ENDINGuntil a confirmed end-spike (the pump-out / final heat burst) is seen, or the 30-minute safety bound releases it, so the full cycle including the tail is captured in the recorded cycle. -
Dishwasher: short cycles overran ~30-44 min and could be mislabelled: A dishwasher cycle that ran a few minutes shorter than its profile's average, and whose terminal pump-out landed just before the drop into
ENDING(so no in-ENDINGend-spike ever armed), could not satisfy Smart Termination's 99%-of-expected gate and sat in the pump-out wait until the fallback timeout - finishing ~30-44 min late. Worse, as the trailing idle tail grew, the live match drifted from the correct program to a longer near-duplicate (e.g. "65°" being overtaken by "50°") and the cycle was stored under the wrong label (or unmatched). Two coordinated fixes: (1) the pump-out wait now also releases once the cycle has reached its expected duration and stayed sustained-quiet for 10 min afterwards (gated on reaching expected, so a genuinely-late pump-out after a long passive dry is still caught by the end-spike arm); (2) live re-matching is frozen once the dishwasher is inENDINGand sustained-quiet, so the completed active-phase match is not corrupted by the growing idle tail. Verified on a 31-cycle real dishwasher export: before → 2 cycles finished by timeout at ~180 min, unmatched/mislabelled; after → all 31 detected as one cycle each, matched to the correct profile, and smart-terminated (median overrun 0 min, max +13.5 min). No behavioural change for non-dishwasher devices, unmatched cycles, or cycles whose end-spike already arms normally. -
Settings documentation inaccuracies corrected (#285): Three inaccuracies in the integration's documented behaviour were fixed: (1)
progress_reset_delaydefault is 1800 s (30 min), not 2.5 min or 5 min as stated in older docs; when the timer fires it also resets the detector toSTATE_OFF, not only the progress display; the Settings tooltip now reflects both side effects. (2)anti_wrinkle_exit_powerwas undocumented: it is the true-off exit threshold for the anti-wrinkle detector (must sit below the anti-crease baseline, not above it; the 0.8 W default is correct for most machines) and now has a full tooltip in Settings. (3)sensor.<name>_statepossible values now correctly enumerateIdle,Detecting…,Running,Paused,Ending,Finished,Clean,Anti-Wrinkle, andOffin the documentation. -
Settings values are preserved across section switches and saves: pending edits in one section are retained when switching to another. Cross-section conflict checks use the live in-progress form state, so off-screen fields never silently block a save, and every Save includes all in-progress edits.
-
Manual recording is available as a device action in the automation editor (#305): Record Cycle Start and Record Cycle Stop ship as button entities alongside Force End / Pause / Resume, so recording appears in the device-action picker exactly where you look for it. Record Cycle Start is available only while no recording is in progress; Record Cycle Stop only while one is active. (The underlying
record_start/record_stopservices are unchanged; captured recordings are turned into labelled profiles from the panel.) -
User pause stays active until you press Resume Cycle (#306): Pressing Pause Cycle holds the appliance in Paused by user until you explicitly resume, for as long as the appliance stays paused (its standby power keeps drawing). A genuinely switched-off appliance is the one exception: sustained truly-off power returns it to Idle via the safety timeout rather than waiting for Resume forever. Three paths are hardened: (1) the periodic profile matcher skips its envelope-alignment heuristic entirely while a user pause is active (on a dishwasher, without this guard the 1-hour minimum off-gap timeout would eventually finalize the cycle and flip the state to Interrupted); (2) pausing during the cycle detecting phase is respected - the low-power false-start check skips its abort when the user has set a pause; (3) after an HA restart the pause lock is re-asserted on restore, so a paused cycle does not finalize on the first timeout after restarting.
-
Community Store: no misleading zero-energy uploads: Sharing a cycle that has no computed energy figure (older cycles, some recordings) no longer uploads
0 Whas if it were real - the energy statistic is now omitted when unknown, so it cannot drag the store's per-program average energy down. -
Disabling online features clears the stored credential: Turning the community-store online switch off is now a full opt-out - the stored refresh token is dropped from disk (previously it lingered until an explicit Disconnect), and re-enabling simply reconnects. Logging into a different store account now fully replaces the previous one instead of merging, so no field from a prior account (e.g. a stale user id) can persist across an account switch.
-
end_repeat_countsuggestion no longer over-counts brief blips: The end-confirmation-count recommendation applied the same too-loose "resumed pause" test that the off-delay heuristic had, so a terminal pump-out or drying tick after a quiet stretch could be counted as a mid-cycle pause and push the suggested confirmation count higher than needed. It now requires the appliance to resume into sustained activity (the same shared rule already used for off-delay), and a sensor dropout mid-cycle is no longer mistaken for a merged "mid-cycle restart" when selecting clean cycles for tuning - a valid cycle that briefly lost its plug still contributes to learning. -
Localized background-task and suggestion text: The header activity pill now shows the current sub-step of a Process-History run (matching → golden → suggestions → training → health) in your language rather than a generic English label, and the "excluded N mis-detected cycles" note on tuning suggestions is fully localized (each exclusion reason translated) instead of always appearing in English.
-
Event-loop responsiveness and store reliability: The opt-in per-cycle ML quality score and the community-share trace downsampling now run in an executor thread instead of on the event loop, so neither can stall Home Assistant on a long trace; whole-device community downloads bound their per-profile fetch concurrency to avoid tripping the store's rate limiter on appliances with many profiles; and the profile-group cohesion cache is guarded against concurrent access from executor threads.
-
Trimming an overnight cycle no longer rejects valid start/end clock values (#330): When a cycle starts before midnight and ends on the following calendar day, the panel's clock-to-offset converter would compute a negative offset for any end-time past midnight and reject the trim with "Start must be before end" even though the proposed trim points were correct. The converter now adds 86 400 seconds when the computed offset is negative (a past-midnight clock entry), so overnight cycles can be trimmed normally.
-
Release validation (hassfest) fixed: the voice-assistant intent responses were stored under a top-level
intentkey that Home Assistant'shassfestvalidator rejects (extra keys not allowed), which was failing release validation. They now live in a self-servedtranslations/intent/{lang}.jsondirectory (the same pattern the panel translations use) and are loaded directly, so all 35 languages stay localized andhassfestpasses. -
Notification & upload hardening: milestone lists now accept genuine positive integers only (a stray
50.5ortruecan no longer fire an unconfigured milestone); the panel's HTML escaper also escapes single quotes, closing an attribute-injection edge for values containing an apostrophe; and community uploads validate their inputs instead of raising, honouring the "never raise" contract. -
Playground fidelity: the what-if simulator now matches the live notification contract exactly (start notifications are never held by quiet hours; milestone notifications are), and the Optimize → end-timing accuracy objective now scores each cycle's detected end against that cycle's own recorded length rather than the program median, so a legitimately long or short cycle is judged fairly.
-
Type-safe WebSocket API contract + generated reference: A new
ws_schema.pyis the single source of truth for the panel's WebSocket surface - a request registry (WS_COMMANDS) plus a typed response (TypedDict) for every one of the ~70 commands. A debug-only response validator runs whenHA_WASHDATA_WS_CONTRACT=1(zero overhead otherwise), a sync test fails if a command is added or removed without updating the contract, anddevtools/generate_ws_types.pyregenerates both the TypeScript types (www/ws-types.d.ts) and an auto-generated command reference atdocs/WS_API.md. -
New modules:
playground.py(headless detector/matcher/estimator replay behind the Playground tab),intents.py(theHaWashdataStatusconversation intent;conversationadded tomanifest.jsondependencies), andprogress.py+notification_rules.py- the single source of truth for the cycle progress/remaining/phase/energy math and the notification decision predicates.manager.pynow calls those pure modules (thin wrappers), and the Playground'sSimRunnercalls the same functions, so the panel's what-if replay is byte-for-byte what the live integration computes; the former client-side detection copy in the panel was deleted. Behavior of the extraction is locked by a golden before/after snapshot plus the progress/notification test suites. New WebSocket commands (run_playground_simulation,run_playground_cycle_detail,run_playground_history,run_playground_sweep,get_dtw_debug) require a full Home Assistant restart to register. -
Panel localisation reworked: All panel UI strings now resolve through the panel's
_t()system, sourced from the per-language files intranslations/panel/. The integration serves that directory directly at/ha_washdata/panel-translations/{lang}.jsonand the panel fetches only the user's language plus theenfallback on demand, so the browser downloads roughly 90-180 KB instead of a single ~3.6 MB all-languages bundle (which, with no cache-buster, was re-downloaded on every panel open). Per-language files are cached per release and busted on upgrade. This removes the client-side bundle (www/panel-translations.json), itsbuild_panel_translations.pygenerator, and the CI freshness check - there is no build step to run after editing panel translations. Scope covers section titles, sub-group headers, field labels and tooltip/doc strings, artifact detail messages, button labels, dynamic chart labels, and the new-profile input label. The shipped language set was trimmed to the languages that can be kept accurate and maintained (see Removed); a revised translation workflow is in progress and will ship in a follow-up release. The config-flow translation files were audited to match the reduced onboarding flow (obsolete multi-step-flow keys removed, keys added for the reconfigure and simplified setup steps). -
New WebSocket command surface (
ws_api.py) backing the panel (settings, profiles, cycles, phases, recording, feedback, diagnostics, suggestions, curve and envelope data, merge / split / trim, logs, panel config, and RBAC), plus commands for profile groups, ML shadow-comparison / per-cycle review, ML training status / trigger, and matcher-tuning revert. Every async handler is registered with@websocket_api.async_response, and all commands pass through a single server-side RBAC guard. -
ProfileStore.get_storage_stats()no longer runs blocking file I/O or serialises the full dataset on the event loop, which could stall the diagnostics view. -
manifest.jsondeclares the panel's prerequisites:frontend,websocket_api, andrecorderare added asafter_dependencies(the recorder is optional and used only for the raw-socket overlay). -
New NumPy-only
ml/package: embedded standardized-logistic baselines with golden parity fixtures,feature_extraction.py,engine.resolve_scorer(the single bridge that prefers an on-device trained spec over the embedded baseline),trainer.py(NumPy logistic training, byte-compatible with the embeddedscore()),training_task.py(on-device orchestration with held-out promotion), andmatching_tuner.py(executor-safe leave-one-out scoring-weight tuning). Nothing beyond NumPy ships at runtime. -
Storage bumped to v8 with a deterministic, non-destructive migration. New store keys:
profile_groups,matching_config, per-cycleml_review, on-deviceml_model_versions, and a "golden" flag on recorded cycles. The migration tags historical manually-recorded cycles as golden references on upgrade so hand-recorded cycles made before the golden flag existed are recognised (kept through cleanup, seed matching, count as clean for training). Recent recordings are matched by the recorder'smeta.source == "recorder"/meta.original_samplesmarkers; the v8 step also recovers older recordings that predate those markers (theirmetais empty) by their structural signature - a completed cycle that the manual recorder built directly and so never stampedmax_power/termination_reasonon, whereas every auto-detected cycle always carriesmax_power. Idempotent, runs once. -
Suggestion engine reworked:
select_clean_cycles()filters mis-detected cycles before analysis,reconcile_suggestions()enforces cross-parameter invariants, and an ML-calibratedMLSuggestionEngineis available behind its flag. The Diagnostics Process History action now reprocesses cycles, backfills, refreshes suggestions, retrains (if enabled) and recomputes per-cycle health in one pass. -
New services:
pause_cycle,resume_cycle, and (behind the ML-training flag)trigger_ml_training. -
A headless panel render smoke test (
devtools/panel_smoke.js) now runs before pytest inrun_tests.sh, catching load-time (TDZ) and template errors that a syntax check alone cannot. -
ML subsystem grew a regression head:
trainer.py/engine.py/training_task.pynow handle astandardized_linearhead (fit_ridge,build_regression_spec,predict_*,resolve_regressor) alongside the logistic classifiers, used by the remaining-time model; the on-devicelive_matchandremaining_timecapabilities train from accumulated data with the same held-out promotion discipline.ProfileStoregained match-ranking-history storage and the pure-statisticscompute_profile_trends/suggest_coverage_gaps/compute_envelope_conformance/compute_profile_advisorieshelpers, surfaced viaws_get_profiles. -
Terminal-state to Off transition consolidated (#284): The two previously independent paths that returned a finished cycle to Off (the manager's wall-clock state-expiry timer and a duplicate hardcoded 30-minute auto-expire inside the detector's
process_reading) are now a single owner,manager._reset_terminal_to_off, driven from_handle_state_expiry. This removes a split-brain window where the detector could flip to Off while the manager's progress/Clean bookkeeping still lingered, and is the foundation for the opt-in power-based Off detection.ANTI_WRINKLE -> Offkeeps its own idle/timeout logic. -
Config-entry schema bumped to 3.6: a deterministic, non-destructive migration remaps any entry still on a removed device type to
other, preserving all tuned options. -
Dead-code and duplicate cleanup (no behaviour change): removed ~900+ lines of unused code - the config-flow-era server-side SVG/chart generators and markdown-table helpers (the panel renders charts client-side), assorted orphaned methods/constants, an orphaned feedback-detail WebSocket endpoint, and the unused
apply_suggestionsoption. Consolidated duplicated logic behind single helpers: one gap-aware trapezoidal energy integrator (signal_processing.integrate_wh, used by both persistence paths), a sharedprofile_expectations, a single ambiguity-margin helper, and one power-trace decompression path. Verified byte-identical on the real-data replay suite.
-
Coffee Machine, Electric Vehicle, Heat Pump, and Oven device types have been removed. They were deprecated because each fails one of WashData's appliance fit tests (a user-selected discrete program, a reproducible power signature, a clean return to OFF), so profile matching and time-remaining estimation produced noise rather than signal. Existing config entries on any of these types are automatically migrated to Threshold Device on load, with all stored options and learned profiles preserved - WashData keeps working, it just stops layering appliance-specific defaults it never had good values for. Their device-type-specific defaults, phase catalogs, translations, and the associated deprecation UI (the "(deprecated)" picker labels and the "hide deprecated device types" panel option) were all removed.
-
Unmaintainable language packs removed (60 → 35 languages). Rather than ship translations that could not be kept accurate, the language set was cut back to 35 maintained languages. If your language is no longer bundled, WashData falls back to English; the per-account Panel language override lets you pick English (or another shipped language) explicitly. A revised translation workflow is in the works and will restore broader coverage in a follow-up release.
-
WashData-initiated persistent notifications removed: WashData no longer posts unsolicited Home Assistant persistent notifications for its own learning/tuning activity. Three notification paths were removed: the "tuning suggestions ready" notification, the cycle-verification feedback notification (previously gated by the
suppress_feedback_notificationsoption), and the ghost-cycle auto-tune notification (the "detected ghost cycles / suggested min_power change" message). All of these signals are now surfaced exclusively through the panel: tuning suggestions and the auto-tune min_power suggestion appear in the Settings suggestions banner / per-field pill widgets, and cycles needing verification appear in the Cycles review queue. Thesuppress_feedback_notificationsoption has been removed (it is stripped from existing config entries on migration, no data lost), along with its panel setting and thesuggestions_ready_notification_*,feedback_notification_*, andauto_tune_*translation keys instrings.json,translations/en.json, and all other supported language files. Your configured start/finish/timer notification channels are unaffected.
- After updating, perform a full Home Assistant restart (not just an integration reload) so the new WebSocket commands register, then hard-refresh the panel.
- This release sets a minimum Home Assistant version of 2026.5.0.
- The raw-socket overlay needs the Recorder to be retaining history for the configured power sensor; it degrades gracefully if the recorder is unavailable.
- If your device was set to Coffee Machine, Electric Vehicle, Heat Pump, or Oven, it is migrated to Threshold Device automatically (your tuned settings and profiles are kept). Since Threshold Device ships intentionally generic defaults, review the detection thresholds under Settings for that device.
- @Olen made their first contribution in #283
- rename ProfileStore method (#278 ): init.py: rename profile_store.auto_label_unlabeled_cycles(...) → profile_store.auto_label_cycles(...) in handle_auto_label_cycles. The ha_washdata.auto_label_cycles service handler calls manager.profile_store.auto_label_unlabeled_cycles(...), which does not exist - the actual ProfileStore method is auto_label_cycles(confidence_threshold, overwrite=False). The service registers fine, so the bug is invisible until invoked, where it raises AttributeError and surfaces as HTTP 500.
- @agjendem made their first contribution in #278
-
Recorded Power Trace Undersampled for Wet Appliances (#270): The previous 30s global sampling-interval default discarded the rapid 0↔150W motor/heater oscillation of washing machines, washer-dryers, and dishwashers, so WashData's stored power trace looked mostly flat with a few isolated spikes while the Home Assistant sensor showed full activity, degrading cycle and program detection. New setups for these three device types now default to a 2s sampling interval, and the advanced-settings tooltip documents the per-device default. Existing devices keep their saved interval so lower Sampling Interval in advanced settings to benefit. Original investigation and fix by @rysiuwroc (PR #271).
-
Translation Placeholder Validation Errors on Startup (#269, #275): After startup, Home Assistant logged "Validation of translation placeholders for localized (<lang>) string … notify_pre_complete_message failed" for the Live Update Message Format description (reported for German and Italian, but present in every language). The English source gained a third
{program}placeholder and was reworded to describe the recurring live progress updates, but the localized descriptions still carried only{device}and{minutes}with the old "sent before completion" wording, so their placeholder set no longer matched the source and the loader rejected them. The description has been retranslated across all 60 languages so each one again matches the English placeholder set ({device},{minutes},{program}) and reflects the current wording. -
New Cycle Now Detected While Still Showing "Finished" (#267): Starting a new run while the device was still in the Finished (or Clean) display window could leave WashData pinned in that state until the progress-reset window expired (~30 min), only then dropping to Off and detecting the new cycle. The cause was the manual-stop lockout that arms after a user/external cycle-end stop: it only cleared once power dropped below the start threshold, so a machine that kept drawing power (a back-to-back load, or standby/door-lock draw) was never released. The lockout now also releases once power stays at or above the start threshold for longer than any plausible spin-down, so a genuinely new load is detected immediately and resets everything as a fresh cycle. The brief post-stop spin-down is still ignored, exactly as before. The options/UI also leaves the Finished overlay and cancels the reset timer the moment a new cycle starts, so the state and the "still inside" reminder no longer lag behind.
-
PR Validation: Duplicate Comment Storm Fixed (PR #271): The issue-link validator posted its "automatically closed" notice ten times on a single PR. The
Validate Pull Requestworkflow re-runs on everypull_request_targetedit/synchronize event (including bots such as CodeRabbit editing the PR description), and the issue-link job commented and closed unconditionally with no idempotency check, so each re-trigger added another identical comment, and overlapping runs raced one another. The workflow now serialises per PR through aconcurrencygroup (cancelling superseded runs), reuses a single bot comment instead of posting duplicates, and skips PRs that are already closed. The changed-file and comment lookups are now paginated so a PR with many files or comments is classified and de-duplicated correctly rather than only seeing the first page. -
Contribution Gating: Graduated Instead of Immediate Close: Previously a PR with no linked
acceptedissue was closed the instant it opened. The gate is now staged so contributors get a chance to follow the process: a PR with no linked issue is labelledneeds accepted issue, told what to do, and given a 3-day grace before the scheduled job closes it; a PR that references an issue which does not yet carry theacceptedlabel is parked under anawaiting maintainerlabel and is never auto-closed, with the maintainer pinged once in that issue to review it; a PR linked to an accepted issue passes and any earlier warning label/comment is cleared. None of this merges anything: the automation only comments, labels, and closes, so the decision to accept and merge stays entirely manual. -
Localization PRs Exempt by Asset, Not Just Directory: The accepted-issue requirement is waived for localization-only PRs. A UI-text change necessarily edits
strings.json(the source of truth) and regenerates everytranslations/*.jsonfile, so the exemption now recognisesstrings.jsonas a localization asset alongside thetranslations/directory, instead of treating a one-line UI string fix as a code PR because of the retranslation churn it drags along. PRs that also touch Python or other code still require an accepted issue. -
Stale-PR Closer Generalised and Anchored to Label Time: The scheduled
Close Incomplete PRsworkflow was renamedClose Stale PRsand now closes bothneeds description(5-day grace) andneeds accepted issue(3-day grace) PRs from a single rule list. Each grace period is measured from when its label was actually applied (read from the PR's label events) rather than from PR creation, so a PR labelled long after it was opened gets the full grace the warning promises instead of being closed early. The workflow was also granted theissues: writepermission it needs to comment, alongside its existingpull-requests: write. New repository labelsneeds accepted issueandawaiting maintainerback this flow.
-
Card Tap / Hold / Double-Tap Actions: The WashData tile card now supports the standard Home Assistant gesture actions. Three new visual-editor options - Tap Action, Hold Action, and Double Tap Action - let each gesture trigger
more-info(the previous and still-default tap behaviour),toggle,call-service/perform-action,navigate,url, ornone. The card switched from a singleclicklistener to pointer-event gesture recognition (500 ms hold, 250 ms double-tap window, with a small movement tolerance so a scroll or drag does not register as a tap); a single tap fires immediately unless a double-tap action is configured. Existing cards are unaffected - the default tap action ismore-info, exactly as before. Originally submitted as PR by @kingpepe85 but did not satisfy the requirements to merge. -
Notification Lifecycle Is Now One Thread (#248): Cycle start, live progress, the pre-end reminder, and the finished alert now share a single per-device notification
tag, so each one replaces the previous on the mobile companion app instead of stacking up. The finished notification replaces the live progress card in place (rather than the old behaviour of dismissing the live card and posting a separate finished card). For setups with no notify service configured (the persistent-notification fallback shown in the Home Assistant Notifications panel), the same identity is reused as a stablenotification_id, so the lifecycle collapses to a single card per device instead of accumulating one card per cycle. The clean-laundry "still inside" nag keeps its own separate tag because it can fire up to an hour after the finished alert. -
Notification Auto-dismiss Timeout (#248): A new Auto-dismiss After (seconds) option forwards a
timeoutto the companion app so WashData notifications vanish automatically after the configured time (e.g. 3600 = one hour). Default0keeps the previous behaviour (notifications persist until dismissed). Applies to every notification type. -
Per-type Notification Channels (#253): Two new options (Notification Channel (status/live/reminder) and Finished Notification Channel) let each device route its notifications to dedicated Android companion-app channels. Status, live, and reminder notifications use the first channel; the finished alert (and the reminder + laundry-waiting nag) use the second, so a cycle finishing can have its own distinct sound while status updates stay quiet. Both default to empty (the companion app's default channel). Note: a channel's sound/importance is configured in the companion app the first time the channel name is used. WashData only sets the channel name.
-
Optional Device Link (#242): A new Device Link section in advanced settings adds an optional Linked Device picker. When set, the WashData device is exposed as "Connected via <device>" in the Home Assistant device registry, so it can reference the smart plug or the appliance device it monitors. WashData keeps its own device card and entities (this is a reference link, not a merge); clearing the selection removes the link, and a linked device that is later deleted is treated as no link rather than a dangling reference. Defaults to empty - standalone behaviour, unchanged for existing setups.
-
Suggested Thresholds Rejected for High-power Appliances (#257): Applying suggested settings on a high-power device (e.g. a domestic well pump with a ~914 W active-power floor) failed with "Value 1096.68 is too large for dictionary value @ data['detection_section']['start_threshold_w']" and the form would not save. The detection-section Start/Stop Threshold (W) selectors were hard-capped at 500 W / 100 W, ceilings tuned for washers and dryers, so the form rejected the very values the suggestion engine produced and also locked the user out on re-open once such a value was saved. The two ceilings now expand automatically to admit the currently-saved value and any pending suggestion (rounded up to the next 100 W for headroom), while keeping the friendly 500 W / 100 W bounds for typical appliances.
-
Pre-end Reminder No Longer Looks Like It Fires "5 Minutes Before End": The "X minutes before end" reminder previously reused the same message text as the recurring live progress updates ("Less than N minutes remaining"), so a routine live update near the end was indistinguishable from the reminder, making a reminder set to 20 minutes appear to fire at ~5 minutes. The reminder now has its own dedicated Reminder Message Format (default
{device}: about {minutes} minutes left.), fires exactly once at the configured time, shares the lifecycle tag so it updates the thread in place, and is routed to the finished channel without the livealert_onceflag so it makes a sound once. The live-update message field has been relabelled Live Update Message Format to reflect what it actually controls. -
Time Remaining Sensor Now Always Declares Its Unit (#261): The Time Remaining and Total Duration sensors declared their
minunit through a state-dependent property that returnedNonewhile the appliance was idle. Opening the entity settings while the machine was off therefore showed no unit, so Home Assistant did not offer the duration display-format options (e.g. switching the display to hours/minutes/seconds). The unit is now declared statically on the entity description (matching the Elapsed Time sensor), so both sensors are always recognised as duration entities and the display-format options are available regardless of state; the value still reads unknown while idle. -
Config & Options Screens Now Follow Each User's Profile Language (#258): The options menu and the Device Type picker were rendered in the Home Assistant system language (Settings > System > General) for every user, instead of following each signed-in user's own profile language. The options menu now hands its entries to the frontend as option IDs so the labels resolve per user, and the Device Type dropdown exposes its choices for per-user resolution instead of fixed labels. The conditional deprecated-device-type warning and the phase-catalog summary wording were routed through the shared localized-text helper, and several dead or duplicated form placeholders were removed. Text that WashData assembles in Python (notification bodies, and the deprecated-type warning, which is conditional) still follows the system language, because Home Assistant does not expose the requesting user's profile language to integrations.
-
Clean-Laundry Reminder Now Clears When the Laundry Is Removed: The "your wash has been sitting there" reminder uses its own notification tag rather than the cycle lifecycle tag, so nothing replaced it once the clean state resolved. Opening the door (or otherwise leaving the Clean state) previously only purged any still-queued clean notifications from the internal queue but left an already-delivered reminder stranded on the mobile companion app. WashData now also issues a best-effort
clear_notificationfor the clean tag whenever the laundry is taken, so a delivered reminder is dismissed in place instead of lingering. A clear for a non-existent tag is harmless, so this runs whenever a clean/finish delivery target or notification action is configured. -
Card Gestures: Scrolling No Longer Registers as a Tap: When a pointer drifted past the movement tolerance (a scroll or drag started on the card), the card cancelled the pending hold but the subsequent pointer-up still fired the configured tap action. The pointer-move handler now also marks the gesture as cancelled, and pointer-up bails out without firing a tap, so dragging or scrolling over the card no longer triggers
more-info(or any other configured tap action). Affects the tap / hold / double-tap gesture support added earlier in this release.
-
Auto-Label Old Cycles: Readable Profile List: The auto-label screen previously listed every matching profile as a single comma-joined run-on line. Profiles are now shown one per line as a bulleted list.
-
Manage Phase Catalog: Shorter Landing Page: The phase-catalog landing page listed every phase of every device type, making it very long to scroll. It now shows the current device type's phases in full plus a one-line count of the other device types that have phases; creating, editing, and deleting phases still works across all device types.
-
New Notifications & Events Guide (#252): The notification options were under-documented, and the multiple ways of sending a notification (per-event target lists, notify entities, Notification Actions, and bus events) were a common source of confusion. A new NOTIFICATIONS.md guide explains all three delivery paths and when to use each, documents every notification option in a single table, describes the cycle notification lifecycle (start -> live -> reminder -> finished and how they replace each other), lists the message placeholders and the companion-app payload keys, and folds in the full Events reference plus useful entity attributes. The README's inline Events section was condensed into a short summary that links to the new guide.
-
Notifications Guide: Language Behaviour Note: Added a short note to NOTIFICATIONS.md clarifying that notification text WashData composes (the default message templates and the learning / auto-tune prompts) follows the Home Assistant system language rather than each user's profile language, and pointing to the editable message templates and the bus-event automations as the way to localize notifications per recipient.
-
Bug Report: "Debug Logs" Renamed to "Logs / Error Evidence": The field previously labelled Debug Logs in the bug report template has been renamed Logs / Error Evidence and its description rewritten to make explicit that any log or error evidence is acceptable - a single error line from the HA logbook, a warning trace, or a full debug session all count. Several reporters were filling logs into the issue description body while marking the dedicated field as empty because they read "Debug Logs" as requiring HA-level debug output. The preflight checkbox, the issue-validator bot error message, and the heading regex in
issue_validator.ymlwere all updated to match the new field name. The validator acceptsN/Awith a reason exactly as before.
-
Notification Delivery (Behaviour Note): On cycle finish, WashData no longer sends a service-level
clear_notificationto live-progress targets; same-tag replacement by the finished notification handles dismissal. Custom notification action templates still receive theclear_notificationmarker for backward compatibility. Config entries migrate to schema3.5(new options are populated with defaults; no user data is touched). -
Contributor PR Flow - Issue-First with Owner Acceptance: Non-translation pull requests now require a linked issue carrying the
acceptedlabel before they are accepted. The intended flow is: open a bug report or feature request → check the new "I plan to submit a PR" checkbox → wait for the maintainer to add theacceptedlabel → then open the PR referencing that issue. A newvalidate_pr.ymlworkflow enforces this on every PR open/edit/reopen event: translation-only PRs (where every changed file is undertranslations/) and PRs opened by the repository owner are exempt; all others that do not reference an accepted issue are closed immediately with an explanatory comment. A companionprotect_accepted_label.ymlworkflow fires onissues: labeledand removes theacceptedlabel if it was applied by anyone other than the repository owner, then leaves a comment explaining the restriction. Bug report and feature request issue templates each received a new optional "Contributing a Fix / Implementation" checkbox section at the bottom to signal contributor intent. The PR template gained a prominent[!IMPORTANT]callout at the top and a dedicated Linked Accepted Issue field. Two new repository labels are required:acceptedandneeds description(see below). -
PR Quality Gate with 5-Day Auto-Close: The same
validate_pr.ymlworkflow also checks whether the PR description template has been meaningfully filled in: it verifies that the Description section contains content beyond placeholder HTML comments and that at least one Type of Change checkbox is checked. Incomplete PRs receive a warning comment (updated in-place on re-check) and theneeds descriptionlabel. A separateclose_incomplete_prs.ymlworkflow runs daily at 11:00 UTC and automatically closes any open PR carrying that label for more than 5 days. PRs that are later completed have the label and warning comment removed automatically. Both checks are skipped for the repository owner. -
Profile & Phase Names Escaped in Option-Flow Summaries: User-supplied profile and phase names are free text but were embedded raw into the markdown summaries rendered by the auto-label and phase-catalog screens, so a name containing markdown metacharacters could inject emphasis, code spans, links, or table cells, or break list rendering with embedded newlines. A new
_escape_markdownhelper collapses whitespace runs (including newlines) into single spaces and escapes\`*_[]~|before the name is interpolated, so names render literally everywhere they appear in option-flow text. -
Card "Open URL" Action Hardened Against Reverse Tabnabbing: The card's
urlgesture action opened the link with a barewindow.open(url, "_blank"), which leaves the opened page with a livewindow.openerreference back to the dashboard tab. The call now passes"noopener,noreferrer"so the destination cannot navigate or otherwise reach back into the Home Assistant tab. -
Deprecated-Type Warning Device Label Now Localized: The interpolated device-type name inside the deprecated-device-type warning was taken from the raw English
DEVICE_TYPESmap, so a localized warning still showed the English type name. The label is now resolved through the same selector translations the rest of the form uses, falling back to the English value only when no translation exists. The trailing blank line that separates the warning from the form was also moved out of the translation string into Python, since the Home Assistant translation validator rejects values with leading/trailing whitespace. -
CI: Pinned
github-scriptand Least-Privilege PR Validation: Allactions/github-scriptreferences in the PR-automation workflows (validate_pr.yml,protect_accepted_label.yml,close_incomplete_prs.yml) were pinned from the floating@v7tag to a specific commit SHA to harden the supply chain.validate_pr.yml(which runs on the elevatedpull_request_targettrigger) now defaults tocontents: readand grantsissues: write/pull-requests: writeonly to the individual jobs that comment on, label, or close a PR, instead of handing the elevated token write scopes for every step.
- New "Other (Advanced)" Device Type: A new generic bucket has been added to the Device Type dropdown for appliances that do not match any of the supported types (Washing Machine, Dryer, Washer-Dryer Combo, Dishwasher, Air Fryer, Bread Maker, Pump). Unlike the supported types, Other (Advanced) ships intentionally generic defaults that are not tuned for any specific appliance class: there is no curated phase catalog, no device-type-specific runtime branching, no per-class fallbacks for active-timeout, off-delay, DTW band ratio, or completion threshold. The contract is explicit: WashData provides the state machine, profile-matching pipeline, and entity layer; the user provides the tuning. This is the right pick for users who want to experiment with appliances WashData does not have first-class support for (instant pots, kettles, blenders, etc.) and accept that defaults will not work out of the box. It is also the runtime fallback when a hard-removed deprecated device type orphans an existing entry (see Deprecations below).
- Four Device Types Deprecated: Electric Vehicle, Heat Pump, Coffee Machine, and Oven are now marked as deprecated and scheduled for removal in a future release (planned: 0.4.6). All four fail at least one of WashData's three appliance fit tests (user-selected discrete program, reproducible power signature, clean return to OFF), so profile matching and time-remaining estimation produce noise rather than signal: EV cycles vary by state-of-charge and load-management rather than a fixed program, heat pump cycles are weather/thermostat driven, coffee machines fragment into seconds-long user-driven bursts, and ovens hold an oscillating thermostat signature with no deterministic end. Refining defaults and phase catalogs for these classes is unlikely to recover meaningful matching quality. No behavior change for existing setups in this release: the integration continues to work for entries already configured with these types, all stored options and learned profiles are preserved, and the runtime state machine is unchanged. The new-entry device-type dropdown filters these out (so they cannot be picked for fresh installs), the options flow shows existing entries' current type with a
(deprecated)suffix so it is still selectable until the user migrates, and the Settings options-flow step displays a yellow warning at the top of the form when the entry is on a deprecated type explaining the situation and listing both the supported alternatives (Washing Machine, Dryer, Washer-Dryer Combo, Dishwasher, Air Fryer, Bread Maker, Pump) and the new Other (Advanced) bucket as a generic landing that preserves all hand-tuned settings. The deprecation warning has been internationalized: a{deprecation_warning}placeholder was added to all locale translation files so the warning (injected by the options flow) appears at the top of the Settings form description for all users regardless of language. Once a user switches to any non-deprecated type the warning disappears and the suffix is dropped. After two release cycles, the deprecated constants will be removed from the dropdown entirely and any entry still on a removed type will fall back to Other (Advanced) at runtime (so the integration does not silently pretend the orphaned entry behaves like a washing machine), with stored options preserved exactly as the user set them.
- Advanced Settings Save Blocked by "Entity None is neither a valid entity ID nor a valid UUID" (#245): After upgrading to 0.4.4.2, submitting the Advanced Settings form (with or without the Apply Suggested Values checkbox) failed with three validation errors against
external_triggers_section'sexternal_end_trigger,door_sensor_entity, andswitch_entityfields, and none of the changes were persisted. When the Advanced Settings form was reorganised into collapsible sections, these threeEntitySelectorfields were declared withdefault=Nonefor unset entities; voluptuous renders the form withNonefilled in, and on submit theEntitySelectorvalidator rejectsNonebecause it is neither a valid entity ID nor a UUID. Fix: switched all three fields to thedescription={"suggested_value": ...}pattern already used forenergy_price_entityelsewhere in the options flow, so the field renders as empty when unset and the validator only sees real entity IDs on submit. Users on 0.4.4.2 who never configured an external end trigger, door sensor, or pause switch can now save Advanced Settings (including suggested-value applications) without clearing the form first.
- Notify Entity Targets (#230): Cycle Start / Finish / Live Progress notification target dropdowns now list notify entities (e.g. those exposed by
telegram_bot) alongside legacynotify.<service>services. When the picked target is a notify entity, the dispatcher routes through the universalnotify.send_messageaction withentity_idset; legacynotify.*services continue to be called directly. No reconfiguration is required - existing setups keep working unchanged, and users who could previously only reach a Telegram chat via theCONF_NOTIFY_ACTIONSscript field or theha_washdata_cycle_endedevent can now pick the entity directly. - Bulk Dismiss in Review Learning Feedbacks: The Review Learning Feedbacks option-flow step now shows a Dismiss all pending feedback requests checkbox alongside the per-cycle review list. Checking it routes to a confirmation step that lists the total count and dismisses every pending review in one action - useful after a profile rename or large catalog cleanup leaves a long backlog of stale review requests that no longer carry useful learning signal. The single-cycle review path is unchanged. Additionally, the Review Learning Feedbacks entry in the options-flow root menu now shows the pending count as a prefix (e.g. (8) Review Learning Feedbacks) so the backlog is visible without opening the submenu; the prefix is hidden when the count is zero.
- Manual Timestamp Split in the Interactive Editor (#236): The Merge/Split Interactive Editor now offers two split methods. The existing Auto-detect idle gaps mode is unchanged. The new Manual timestamp(s) mode lets you set explicit split points by wall-clock time - handy when a machine restarts immediately after a cycle ends and there is no idle gap for auto-detection to find. Pick the cycle, choose Manual timestamp(s), then enter one or more times (
HH:MMorHH:MM:SS, one per line) inside the displayed cycle window; the form shows a preview SVG of the cycle and the exact start/end wall-clock times so you can pick a known boundary (e.g. from HA history). N timestamps produce N+1 segments; each segment must be at least 1 minute long. After confirmation, the preview SVG, per-segment profile assignment, and post-split envelope rebuild work exactly as in auto mode.
- Toggle Switches Replace Checkboxes for All Boolean Settings: Every feature toggle in the options flow now renders as an iOS-style switch instead of a plain checkbox. Until now, the only toggle that used the modern switch UI was the Dismiss all pending feedback requests field in Review Learning Feedbacks (because it was declared as
selector.BooleanSelector()while every other boolean used the bareboolvoluptuous type, which the frontend renders as a checkbox). Fifteen fields across Advanced Settings, Notifications, Settings, and the suggestion-apply confirm step were migrated toBooleanSelectorso the visual language is consistent and the active/inactive state is obvious at a glance. No stored values change - switches and checkboxes both serialize to Pythonbool. - One-Click Action Menus Replace Dropdown + Submit Pickers: Seven options-flow steps that used to require open the dropdown, pick an option, click Submit now render each action as a tappable button via
async_show_menu. Converted: Manage Cycles, Manage Profiles, Manage Phase Catalog, Merge/Split Interactive Editor, Assign Phase Ranges, Record Cycle (Manual), and Diagnostics & Maintenance. Each button is a single click that immediately advances to the next step. Conditional menus (e.g. Record Cycle, which surfaces Start / Stop / Process / Discard depending on recorder state) continue to show only the buttons that apply right now. For steps with stateful sub-actions (e.g. Assign Phase Ranges' Clear All Ranges and Save and Return) thin wrapper steps were added so the menu can dispatch them cleanly. - Unified ← Back Button Across All Menus: Every submenu in the options flow now ends with a ← Back button that returns to the previous menu in the navigation chain. A small navigation stack (
_menu_stack) tracks the path frominitdownward; clicking ← Back pops the stack and re-shows the parent. Previously the only way to leave a submenu without performing an action was the X close button in the dialog's title bar, which exited the entire options flow back to the Devices & Services page - too aggressive for users just exploring. The form-based Notifications step retains an explicit Go back without saving checkbox (because forms can't render menu buttons and the user wanted Submit to keep its save semantics); every other navigation surface now uses the same one-click Back affordance. - Recent Cycles List Becomes a Full-Width Table with Match Confidence: The Manage Cycles step now shows the eight most recent cycles as a markdown/HTML table with five aligned columns - status icon, program (widest), date+time, length, and match confidence as a percentage. Replaces the previous single-line ✓ 2026-05-19 14:30 - 85m - Cotton 60°C format that wasted most of the dialog's horizontal space and did not expose match confidence at all. To make confidence available, a new
match_confidencefield is persisted on the cycle dict at every site that assigns a profile (runtime match commit, post-cycle auto-label, and bulkauto_label_cycles). Cycles labeled before this release would otherwise show-; on integration startup, a backgroundasync_backfill_match_confidence()task re-runs the matcher once for every labeled cycle without the field and writes the result, so the table is fully populated on the next visit. The backfill is idempotent - already-backfilled cycles are skipped - and runs as a non-blockingasync_create_taskso startup is unaffected. - Profile Summary as a Full-Width Table: Manage Profiles now shows the same table layout as Recent Cycles, with five columns - profile name (widest), cycle count, average length, last-run date, and average energy per cycle (Wh or kWh, formatted by magnitude). Replaces the bullet list
- Cotton 60°C: 24 • ~135mthat scaled poorly once a household had a dozen profiles. The data already existed on thelist_profiles()result -last_run,avg_energy,cycle_count,avg_duration- and is now surfaced consistently. - Advanced Settings Reorganised into Seven Collapsible Sections: The single 50-field Advanced Settings form is now grouped into named, collapsible sections via
homeassistant.data_entry_flow.section: Suggested Settings, Detection & Power Thresholds, Profile Matching & Learning, Timing, Maintenance & Debug, Anti-Wrinkle Shield (Dryers), Delayed Start Detection, External Triggers, Door & Pause, and a conditional Pump Monitor for the pump device type. Suggested Settings (containing theApply Suggested Valuescheckbox and its full description of pending suggestions) and Detection & Power Thresholds are open by default; the rest collapse so the form opens compact. Submission still saves every field in one click - sections are a visual grouping only, and the handler flattens the nested user input back to a flat dict before merging into the options entry. Field labels and descriptions are now nested undersections.<name>.{data,data_description}instrings.jsonandtranslations/en.jsonso each field's help text shows up under its proper section header. - Review Learning Feedbacks Landing Menu: The feedback step is now a menu landing page with three buttons - Review a pending feedback (opens the per-cycle picker on a dedicated sub-step
learning_feedbacks_pick), Dismiss all pending feedback (jumps to the existing confirmation step), and ← Back. The pending items are previewed in a full-width table above the buttons (detected program, confidence %, reported-at). Previously the picker, the dismiss-all toggle, and the go-back checkbox were all crammed into one form; splitting them puts each action one click away. When there are no pending feedbacks, the empty state is also a menu with just ← Back instead of a form with an empty schema.
- Live Progress Notifications Now Dismiss on Cycle End and HA Restart: Two bugs combined to leave the live/chronometer notification stranded on mobile devices, so the cycle-finish notification arrived alongside (rather than replacing) a stale countdown that kept ticking - sometimes into negative numbers. (1) Cycle end did call
clear_notification, but the dispatch was gated on_live_notification_sent_count > 0, so after a Home Assistant restart - which resets the in-memory counter to 0 while the notification continues to live on the phone - the clear was silently skipped and the stale notification never went away. (2) The HA shutdown path (async_unload_entry→async_shutdown) didn't issue a clear at all, so restarting the integration or HA left the chronometer ticking against a frozenwhentimestamp until the next cycle-end clear would have run. Fix: the clear path now fires whenever live services or actions are configured (a no-op clear for a non-existent tag is harmless on the mobile_app side), andasync_shutdownissues a best-effort clear before persisting active-cycle state. Cycle end (completed / force-stopped / interrupted) now reliably dismisses the live notification before the finish notification is dispatched, so the finish notification is the only one visible; HA restart and integration reload also dismiss it. - Delayed Start Detection Redesigned, Replacing the Drain-Spike Model (#238): The original 0.4.4 implementation watched for a brief low-power drain spike followed by sustained standby - a pattern fitted to one specific washing-machine behaviour and broken for everyone else. Dryers (no pump, no drain) could never satisfy it, and the drain-window ceiling was silently clamped to
start_threshold_w, whose UI cap of 100 W locked out anyone whose appliance drew more than that while waiting (e.g. tumble-dryer anti-damp rotations at ~240 W rejected the configuration with "Value 240 is too large"). The detector now uses a band-based model: any power held between Stop Threshold (W) and Start Threshold (W) for a configurable confirmation window is treated as the machine waiting to start its cycle. Transitioning out ofdelay_waitintoSTARTINGadditionally requires the high-power streak to span at leaststart_duration_thresholdreal seconds across two consecutive readings, so an isolated anti-damp pulse or menu-navigation peak no longer false-triggers the cycle. The three obsolete drain knobs (delay_drain_min_power,delay_drain_max_power,delay_drain_max_duration) are removed and replaced with a single Standby Confirm Time (s) field (default 60 s); the Max Wait Time (h) safety timeout is unchanged. Migration to schema 3.4 strips the deprecated keys from existing entries. The cap on Start Threshold (W) is raised from 100 W to 500 W so high-standby appliances can be configured without hitting the validator. Disabled by default - users who never use delayed-start are unaffected. - Dishwasher Pump-Out Ghost Cycle, Follow-Up to 0.4.4.1 (#43): The 0.4.4.1 fix correctly ignored the mid-cycle drain spike (user logs confirmed
spike_seen=False) but Smart Termination was still firing ~4 minutes before the real end-of-cycle pump-out, closing the cycle just early enough for the pump-out to register as a brief but still user-visiblestarting → offsequence. The dishwasher Smart Termination wait gate had a 5-minute escape hatch (past_wait_period = duration >= expected + 300 s) that - combined with state that could not be fully reproduced in synthetic tests - was letting the cycle close before the pump-out arrived. The escape hatch is now widened to 30 minutes (DISHWASHER_END_SPIKE_WAIT_SECONDS = 1800.0); within that window the cycle stays in ENDING until the real pump-out fires and arms_end_spike_seen=Truevia the existing 85% progress gate, after which Smart Termination fires normally and folds the pump-out into the original cycle. The matching deferral in_should_defer_finishuses the same widened window so the two paths cannot drift. Verified against a synthetic replay of the May 6 user-reported timeline: cycle closes at 235.1 min with the pump-out included, no ghost cycle. Trade-off: dishwashers genuinely without an end-of-cycle pump-out now sit in ENDING up to 30 min past expected (vs 5 min before) before the safety bound releases them - bounded, but slightly later than before.
- Issue Validator: Clearer "Debug Logs N/A" Guidance: When the auto-validator flags a bug report for missing debug logs, the bot's reminder comment now explicitly shows reporters how to mark logs as not applicable - write
N/Afollowed by a one-line reason (example:N/A - typo in README, no runtime behavior involved) - and lists the kinds of reports where this is acceptable (documentation typos, translation string fixes, pure UI/config-flow layout issues with no runtime code path). The validator already acceptedN/Aas a valid response, but the previous reminder text only said "writeN/Afollowed by a clear explanation of why" without showing the format or giving examples, which led reporters to either leave the field blank or write a bareN/Aand stay flagged. Behaviour unchanged for reports that include real logs. - Event Bus Reference (#231): The README now has a dedicated Events section documenting the three events fired by the integration (
ha_washdata_cycle_started,ha_washdata_cycle_ended, andha_washdata_pump_stuck) with the full payload schema for each, including thecycle_datasubdict shape:id,duration,max_power,energy_wh,status,termination_reason,profile_name,sampling_interval,device_type, and thesignaturefeature vector. Also documents the Fire Events toggle that gates them globally and the fact thatpower_data/debug_data/power_traceare stripped fromcycle_databefore firing to stay under Home Assistant's 32 KB event payload limit (use the diagnostics download if you need the raw samples). No code change; purely fills the gap reported by users who could observe the event in Developer Tools → Events but had nothing in the README to point aplatform: eventautomation at.
- Release-Reference Bot Auto-Notifies and Closes Resolved Issues: A new workflow (
.github/workflows/release_references.yml) runs on every published GitHub release and scans the release notes for issue references (#<number>or full issue URLs to this repo). For each referenced open issue created before the release was published, the bot posts a comment linking to the release page, adds thedonelabel, and inserts a hidden marker (<!-- release-reference-bot -->) into the comment body.close_done_issues.ymlwas extended to recognise that marker as an "owner-equivalent" last comment, so the existing 5-day inactivity rule auto-closes the issue without the maintainer having to comment manually. Reporter replies after the bot's comment still block the auto-close (the last-comment check fails), giving anyone whose problem is not actually fixed by the release a clean way to push back. The workflow also exposes aworkflow_dispatchinput to re-scan a specific tag, and de-duplicates against its own previous comments (matching thetag:<release.tag_name>line) so re-runs are idempotent. - Test Suite Split into Fast / Slow / Benchmark Categories: The dev test loop previously took ~12 minutes because two stress-simulation tests in
tests/repro/test_comprehensive_stress_suite.pyalone consumed ~11 minutes, and a further dozen real-data replay tests added another ~40 seconds. For small changes (UI tweaks, config-flow edits, translation updates) this discouraged running the suite at all. Tests are now tagged with pytest markers:slow(real-data replays fromcycle_data/, stress simulations, full HA integration flow - 8 files / 144 tests) andbenchmark(pure performance characterization - 2 tests).pytest.iniskips both categories by default, so./run_tests.sh(and rawpytest tests/) now run only the 271-test fast suite in about 30 seconds (a 23× speedup). The runner also accepts--slow,--bench, and--allto opt in. No tests were deleted - the full suite still runs via./run_tests.sh --allfor releases and CI. SeeTESTING.md→ Test Categories for the marker conventions when adding new tests.
- New Device Type: Oven (#137): Added support for electric ovens with defaults tuned for the heating profile of a thermostat-regulated cavity (4-hour active timeout to cover slow roasts and pyrolytic self-clean, 10-minute off-delay and 15-minute min-off-gap to bridge thermostat-driven silences without splitting one bake into multiple cycles, 0.5 Wh start-energy gate to filter incidental fan/light draws, 5.0 % progress-smoothing tolerance for the bistable on/off swings during temperature regulation, 10-minute completion threshold). Includes five built-in phases: Pre-Heat, Heating, Maintaining Temp, Cool Down, and Pyrolytic Clean. Selectable in the device-type dropdown with a
mdi:stoveicon. Issue and feature-request templates updated to list the new option.
- Issue Automation: Comment Responses No Longer Trigger Auto-Close: The issue-automation pipeline closed reports as "no response in 5 days" even when the reporter had answered. The validator only re-checked on issue-body edits, so reporters who replied via a comment (often with a debug log attached) went undetected; the daily auto-closer then closed the issue purely on the age of the bot's first flag. The validator now also fires on comments from the issue reporter and swaps the
more info requiredlabel forawaiting maintainer review, surfacing the response to the maintainer immediately. The auto-closer additionally checks for reporter comments since the bot flag as a backstop, so a stale flag never trumps an actual reply. - Dishwasher Pump-Out Registers as a Ghost Cycle (#43): On dishwashers with passive drying and an end-of-cycle drain pump, the integration was closing the cycle ~12 minutes before the real pump-out and then registering the pump-out itself as a brand-new "starting → running → ending" sequence visible in the UI. Root cause: a brief mid-cycle drain wind-down at ~50% of the expected duration set
_end_spike_seen=True, which pre-armed Smart Termination so it fired at 99% of expected duration before the real pump-out arrived. After Smart Termination closed the cycle, the pump-out (a 16–20 W spike for ~1 min) was then 12 min outside the 600 s suspicious-window for ghost suppression on devices whose previous cycle had finished slightly earlier (e.g. a longer-trained profile), so it slipped through unsuppressed and started a fresh detection. Fix: the end-spike marker is now only armed when the spike occurs at ≥ 85% of the expected duration (mid-cycle drain spikes are ignored for end-spike tracking but still keep the cycle in ENDING via the existinglong_ending_tailpath). Additionally, between 85% expected and the smart-termination wait window (expected + 300 s), the dishwasher passive drying protection now defers the cycle finish whenever no end spike has been seen yet - so the pump-out gets folded into the original cycle instead of being treated as a new cycle. Verified against a real diag-export power trace from issue #43: pre-fix → 1 cycle (233 min) + 1 ghost cycle; post-fix → 1 cycle (234.3 min) + 0 ghost cycles. No behavioural change for non-dishwasher devices, for unmatched cycles (no profile), or for dishwashers whose end spike fires within the last 15% of expected duration. - Translation Placeholder Mismatch in
notify_finish_message(#219): Home Assistant logged a startup validation error for all non-English locales becauseen.jsondescribed the finish notification format with only three placeholders ({device},{duration},{program}), while the German, Czech, and Italian translations already listed all five ({energy_kwh}and{cost}were added in v0.4.4 but the English source string was only partially updated). HA validates every localization against the English reference, so any language whose description matched the updated set was flagged as inconsistent. The English source string is now corrected to include all five placeholders, and the description in all 57 remaining language files has been updated accordingly so placeholder sets are consistent across all locales.
- Delayed Start Detection (#193): When enabled under Advanced Settings, WashData now detects machines programmed with a delayed start. A brief low-power drain spike (e.g. the initial drain-check a washing machine performs before sleeping) followed by sustained standby power is recognised as the machine waiting to begin its cycle. The state sensor transitions to "Waiting to Start" (
delay_wait) during the delay period - no program time, progress, or ETA is tracked until the real cycle begins. Configurable thresholds: Drain Spike Min/Max Power (defaults 10 W / 80 W), Max Drain Spike Duration (60 s), and a Max Wait Time safety timeout (8 h, after which the state resets to Off). The feature is disabled by default and requiresstart_threshold_wto be set above the drain spike power level to function correctly. - Wall-Clock Time in Trim Cycle Editor (#212): The Trim Cycle graph now shows real wall-clock times (
HH:MM) on the x-axis as the primary label, with the relative offset (+N min) shown below each tick as a secondary reference. The S and E trim-point markers display the exact wall-clock time (HH:MM:SS) instead of a relative offset. The Set Trim Start and Set Trim End forms have been redesigned: both now show the full cycle window (start → end), the current trim position in wall-clock time and minutes, and a single clock-picker field pre-populated with the current position - making it straightforward to set a trim point by entering a known time from another source (e.g. HA history graphs or a smart plug log) without having to calculate offsets manually. - Energy & Cost Placeholders in Finish Notifications (#196): The finish message template now supports two new placeholders:
{energy_kwh}(energy consumed during the cycle, formatted to 3 decimal places) and{cost}(energy cost, formatted to 2 decimal places). To enable{cost}, configure an electricity price under Notifications → Energy Price - either a static value (e.g.0.22) or a Home Assistant entity that holds the current tariff (anysensor.*,input_number.*, ornumber.*with a numeric state). The entity takes precedence if both are set;{cost}resolves to an empty string when no price is configured. Currency is not included - add it directly in your template (e.g.{cost} €). Example:{device} finished. Duration: {duration}m | Energy: {energy_kwh} kWh | Cost: {cost} €. Energy per cycle is now also stored in the cycle history record (energy_wh) computed from the stored power trace at save time, covering all storage paths including the manual recorder. - Multi-Target Notifications (#203): The notification system now supports multiple recipients per event type. The single Notification Service dropdown and Events to Notify checkbox have been replaced with three independent multi-select fields - Cycle Start, Cycle Finish, and Live Progress - each accepting any number of
notify.*services. A family member who only needs to know when laundry is done can be added to Cycle Finish only, while another person who also wants live progress can be added to all three. Existing configurations are migrated automatically: the previous single service is mapped into each list that matches the previously selected events, so no manual reconfiguration is required after upgrade. Notification actions and presence gating continue to work across all targets. - New Device Type: Bread Maker (#190): Added full support for bread makers with optimised defaults (2-hour no-update timeout for long proving periods, 5-minute off-delay for keep-warm, 30-minute completion threshold) and five built-in phases: Kneading, Resting, Proving, Baking, and Keep Warm.
- New Device Type: Pump / Sump Pump (#195): Added a dedicated pump device type (
mdi:water-pump) with defaults tuned for short, high-frequency cycles: 10 s sampling interval (captures sub-30 s runs that the 30 s default misses), 20 s off-delay (pumps stop sharply with no warm-down), 5 s minimum cycle threshold (valid with 1–2 readings), 60 s minimum off-gap (handles pumps cycling every few minutes), and a 0.003 Wh start-energy gate. Includes a Pump Stuck Alert - a configurable threshold (pump_stuck_duration, default 30 min) that fires a singleha_washdata_pump_stuckHA event if a pump cycle runs longer than expected, enabling automations to alert on a jammed motor or continuous-run fault. - Pump Runs (Last 24 h) Sensor (#195): When using the Pump device type, a new
pump_runs_todaysensor (mdi:counter) counts completed pump cycles in a rolling 24-hour window - useful for monitoring sump pump activity during heavy rain or drought periods. - Cycle Counter Sensor (#210): A new
cycle_countsensor (mdi:counter, unit: cycles, state class:total_increasing) tracks the total number of completed cycles stored for the device. Use this in automations to schedule maintenance tasks - such as cleaning filters, refilling consumables, or checking bearing wear - based on a fixed cycle interval rather than a calendar schedule. - Chronometer Countdown Notifications (#194): Added an optional Chronometer Countdown Timer toggle to the Notifications settings. When enabled, each live progress notification push includes
chronometer,when, andcountdownfields pointing to the estimated cycle finish time. The Android companion app renders a live countdown timer on the notification that ticks down automatically between pushes - eliminating the "frozen at X minutes" problem without requiring more frequent server-side sends. Disabled by default; Android companion app only. - Live Diagnostics in HA Diagnostics Export:
async_get_config_entry_diagnosticsnow includes alive_diagnosticssnapshot from a new in-memoryDiagBuffer- a rolling 24-hour ring buffer capturing every raw power reading, every detector state transition, and the last 5000 log lines. Zero disk I/O; vanishes on HA restart. - User-Triggered Pause (#146): Cycles can now be paused and resumed explicitly via a Pause Cycle button entity, a Resume Cycle button entity, or the new
ha_washdata.pause_cycle/ha_washdata.resume_cycleHA services (both acceptdevice_id). While paused, the cycle is protected from watchdog termination (verified_pause = True) - the appliance can sit at zero or low power indefinitely without WashData closing the session. All time metrics (elapsed time, time remaining, total duration, and progress %) exclude the time spent paused, so a cycle paused for 30 minutes does not inflate the estimated total. The primary use case is energy price management: trigger a pause when tariff is high, resume when it drops (e.g. with Cheapest Energy Hours). An optional Cut Power When Pausing toggle (pause_cuts_power, off by default) instructs WashData to also callswitch.turn_off/switch.turn_onon the configured switch entity when pause or resume is triggered - useful for appliances that remember their position after a power cut. The Pause button is only available while a cycle is Running or Starting; the Resume button is only available while the cycle is user-paused. - Door Sensor & Clean State (#153): An optional Door Sensor field (any
binary_sensor,on = open) can now be configured under Advanced Settings. When set, WashData uses door state in two ways. During an active cycle, opening the door automatically setsverified_pause = True- the same protection used by user-triggered pauses - confirming the interruption is intentional and guarding the cycle against watchdog termination (useful for add-clothes machines such as Samsung AddWash). Closing the door does not auto-resume the cycle - this is intentional and is stated explicitly in the field description; the user must press Resume or call the service. After a cycle ends, if the door is still closed the device enters a new Clean state (STATE_CLEAN) indicating laundry is waiting to be unloaded. The state clears as soon as the door is opened. A configurable Laundry Waiting Notification Delay (notify_unload_delay_minutes, default 60 min, set to 0 to disable) fires a single reminder via the existing finish notification channel once the door has remained closed for that many minutes after cycle end - the "your wash has been sitting there for an hour" nag. The reminder message is fully customizable via a Laundry Waiting Message template (notify_unload_message); the default reads"{device} finished {duration}m ago - laundry is still inside."and supports{device}and{duration}placeholders.
delay_waitState Now Has a Human-Readable Label: Thedelay_waitstate was missing from the sensor state translation map instrings.json, causing the UI to display the raw key. It now renders as "Delay Start".- Bug Report Template: Pump Label Aligned: The device type dropdown in
bug_report.ymlnow reads "Pump / Sump Pump", matching thefeature_request.ymltemplate. - Issue Template: Outdated Version Check: The automated issue validator now fetches the latest stable and pre-release versions from the GitHub Releases API and flags any bug report submitted against an older version. The reporter is asked to confirm the bug still exists on the latest release before the issue is investigated. The version field description now links directly to the Releases page.
- Issue Template: Device Type Sync: Added Bread Maker and Pump / Sump Pump to the Feature Request template's device type dropdown - these device types were supported since v0.4.4 but were missing from that template.
- Issue Automation: Auto-Assign: All newly opened and reopened issues are now automatically assigned to the maintainer, so every report lands in the triage queue without a manual step.
- Issue Automation: Auto-Close Incomplete Reports: A daily workflow scans open issues still carrying the
more info requiredlabel. Issues that remain unfixed for 2 days receive a single reminder comment; issues that remain unfixed for 5 days are closed asnot_plannedwith instructions to reopen a properly filled report. - Import Accepts HA Diagnostic Export Format: The import feature (Configure → Diagnostic Settings → Import) now accepts three JSON formats without any manual preprocessing: (1) the regular WashData export; (2) the raw diagnostics dict produced by WashData (with
store_exportat the top level, e.g. copy-pasted from a log or API response); and (3) the full HA diagnostics download file (the.jsonfile downloaded via Settings → Devices & Services → (device) → Download Diagnostics), which wraps our data in ahome_assistant/dataenvelope. When importing from a diagnostic source, any fields replaced by the redactor (**REDACTED**) are silently stripped, so sensitive fields such aspower_sensorandnotify_serviceare never overwritten with the placeholder string. - Offline Diagnostic Analyser (
devtools/analyze_diag.py): New developer tool that processes any WashData diagnostics export and prints a side-by-side table of current vs. suggested settings with a per-parameter rationale. Covers power thresholds, energy gates, timing/operational parameters, and matching tolerances - all derived from the device's own recorded cycle data without requiring a running Home Assistant instance. Surfaces suggestions already computed by live HA operation for cross-checking, and includes a cycle-history summary per programme (average duration, standard deviation, variance). Runpython3 devtools/analyze_diag.py <export.json>from the repo root with the venv activated; seedevtools/README.mdfor full usage. - Batch Multi-Cycle Simulation (suggestion engine): The suggestion engine now derives power thresholds, dead-zone, and end-energy gate from the aggregate of all labeled cycles (
run_batch_simulation) rather than only the most recent one. The 5th-percentile minimum-active-power across cycles is used for the stop threshold (more robust against outlier cycles), and the 75th-percentile of early-dip timestamps is used for the running dead zone. A newprocess_cycle_endhook re-runs the batch every 5 newly labeled cycles so suggestions improve progressively as more data accumulates. - Min-Off-Gap Suggestion: The suggestion engine now derives a
min_off_gaprecommendation from observed inter-cycle gaps. The p05 gap × 0.8 is used as the proposed value, floored by the device-type default, so the suggestion is always conservative enough to avoid splitting real cycles. - DataLoader New Export Format Support (benchmark tooling):
tests/benchmarks/parameter_optimizer.DataLoadernow handles both the newdata → store_export → data → past_cyclesexport path and the legacydata → store_data → past_cyclespath, so all user-contributed diagnostic exports incycle_data/are usable by the benchmark suite. - Dishwasher Post-Cycle Ghost Suppression (#43): The ghost suppressor now uses a 10-minute suspicious window for dishwashers (vs. 3 minutes for other devices) to catch drain pump-outs that fire 3–8 minutes after the main cycle ends. Within that window, a pump-out is suppressed faster: 3 minutes elapsed + 1 minute of silence, instead of the standard 10-minute/5-minute thresholds. A secondary fallback guard in
_on_cycle_endindependently suppresses any cycle starting within 10 minutes of the previous cycle end if it lasted under 5 minutes and consumed less than 1 Wh. - Confidence Improves With Confirmed Cycles (#199): When a profile has ≥2 confirmed labeled cycles, the envelope's averaged power curve is now used as the matching reference instead of the original single sample cycle. Because the envelope represents the average of all user-confirmed runs, repeated correct detections now produce progressively higher (and more stable) confidence scores rather than staying frozen at the first sample's similarity level.
- Suppress Feedback Notifications (#199): Added a new Suppress Feedback Notifications toggle under Advanced Settings. When enabled, WashData still tracks pending feedback and records confirmations internally, but stops creating persistent "Verify Cycle" notifications in the HA sidebar - useful for devices like dishwashers that are always detected correctly and need no ongoing prompting.
- Feedback Threshold Settings Now Exposed in UI (#199): Auto-Label Confidence and Feedback Request Confidence are now available as editable fields under Advanced Settings. Previously these keys existed internally but could only be changed by directly editing config entries. Lowering Auto-Label Confidence below the observed match score allows consistent detections to be auto-labeled without any user prompt.
- Profile Granularity Guide (#201): The Create New Profile form now shows inline guidance explaining what the matcher uses (power shape, duration, total energy) and what to expect from different levels of profile detail - including which program combinations are reliably auto-detected (e.g. wash-only vs wash+dry), which may need manual confirmation at first (temperature-only variants of the same programme), and how to approach a complex machine like a washer-dryer combo. The README also has a new "Profile Granularity" section (§ 3 of the Getting Started guide) with the same information in a more detailed, tabular form.
- Watchdog Boundary Condition: Standby Power Equal to Stop Threshold Not Recognised as Low Power (#197 regression): When an appliance's standby power draw exactly equals the configured Stop Threshold (e.g. both at 2 W), the Watchdog's power classification used a strict less-than comparison (
< stop_threshold) for the low-power silence path. A 2 W reading against a 2 W threshold evaluated toFalse, so the Watchdog fell through to the high-power stale path and injected a refresh keepalive instead of a 0 W keepalive - leaving the cycle stuck inRunningstate for the entire duration of sensor silence. On one reported case this produced a 136-minute close delay instead of the expected ~6 minutes. The low-power branch condition is now<=(and the high-power branch is>) so a standby value exactly equal to the stop threshold is correctly routed to the 0 W keepalive path that advances the cycle-closure accumulator. - Chronometer Countdown Frozen at 0:00 (#194 follow-up): When the Chronometer Countdown Timer is enabled and the live progress notification cap is reached before the cycle ends, the Android companion app was left displaying a frozen
0:00countdown with no further updates. The live notification sender now detects this condition - remaining time at or below zero, chronometer active, cap exhausted - and sends one additional plain-text update that replaces the stale countdown timer. This overrun update does not increment the sent-count, so a cap of N notifications remains N notifications for the purpose of normal cycle progress; the overrun is a single one-shot correction applied only once per cycle. - Pause/Resume Buttons Always Shown as Unavailable: The Pause Cycle and Resume Cycle button entities never registered state-change callbacks, so their
availableproperty was evaluated only once at HA startup (when no cycle is active →False) and never refreshed. Both buttons now subscribe toSIGNAL_WASHER_UPDATEviaasync_added_to_hassand callasync_write_ha_state()on each manager update, making them correctly enable when a cycle is running and disable when it ends. - Start Notification Fired Up to 36 Minutes Late (#206): The cycle-start push notification was gated on profile detection completing, meaning it did not fire until 3 consecutive profile matches had been confirmed at 5-minute intervals (≥15 minutes minimum, often 30–36 minutes for dishwashers due to their 10% minimum-duration ratio before the first match attempt). The HA bus event (
ha_washdata_cycle_started) already fired immediately - only the push notification vianotify.*services was delayed. The notification is now dispatched at the moment the appliance transitions toRUNNING, regardless of whether a program has been identified yet. Users who included{program}in their start message template will now see "detecting..." until a match is confirmed; the default message has been updated to"{device} started."(without{program}) to avoid the awkward placeholder. A restart-recovery fallback remains in the profile-matching loop in case the notification was missed due to an HA restart before the first cycle snapshot was saved. - Dishwasher ECO Cycle Killed During Passive Drying (#43): Four separate bugs caused dishwasher cycles to be terminated hours before the physical run finished, leaving a 60–90 minute gap of missing data and triggering spurious pump-out ghost cycles.
- Bug A - Watchdog force-ended unmatched cycles too early: The 4-hour (
14400 s) device-specific silence floor - intended to cover dishwasher passive open-door drying - was only applied when a profile had already been matched. An unmatched cycle (program == "detecting...") fell back to the 1-hour (3600 s) base timeout. Because dishwasher ECO programs use very little power and can go completely silent for 1–2 hours while drying, the watchdog killed the cycle roughly 1 hour after the last sensor update - while the appliance was still running. The floor is now applied unconditionally regardless of match state. - Bug B - Cycle detector held ENDING state open for hours after pump-out: After the watchdog was fixed, the
CycleDetectoritself held the cycle inENDINGstate untilmin_off_gap(up to 9000 s = 2.5 h) had elapsed after the pump-out spike. For an unmatched cycle that has already seen a terminal end spike, the full gap is unnecessary. Theeffective_off_delayis now capped at 1800 s (30 min) in this case, so the cycle closes cleanly ~30 minutes after the pump-out rather than sitting open for hours. - Bug C - Passive drying phase ended prematurely when profile confidence was low: For a newly added dishwasher with only 1–2 recorded cycles, profile match confidence at ~120 min into an ECO cycle (the point when the first match attempt fires, after the 50% duration fast-reject clears) is typically 0.30–0.45 - below the
_should_defer_finishconfidence gate of 0.55. A terminal drain-pump spike in earlyENDINGthen reset_time_below_threshold, and the subsequent 60-minute silence caused_should_defer_finishto returnFalseand the cycle to be terminated at ~180 min, storing only 120 min of duration in the profile (the_last_active_timeof the drain spike, not the actual finish). The bug only affected devices with fewer confirmed cycles; the old instance with 7 cycles was unaffected because its confidence was high enough to trigger smart termination at 99% of expected duration instead._should_defer_finishnow includes a dishwasher-specific drying guard: if the device is a dishwasher, a profile has been matched, and the current duration is below 85% of the expected duration, deferral is forced regardless of confidence score. This bridges the gap between the first profile match at ~50% duration and the smart-termination window at ~85–99%, preventing the 60-minute silence timeout from firing mid-drying-phase. - Bug D - Stored cycle duration snapped to terminal drain spike timestamp: When the timeout eventually fired and
_finish_cyclewas called withkeep_tail=False(the default for all devices),end_timewas set to_last_active_time- the timestamp of the terminal drain-pump spike at ~120 min rather than the actual timeout time at ~180 min. This corrupted the profile'savg_duration(average dropped from 233 min to 177 min). The fallback-timeout path in theENDINGstate now useskeep_tail=Truefor dishwashers, storing the timeout timestamp asend_timeso the recorded duration reflects the true elapsed time even when deferral ultimately expires.
- Bug A - Watchdog force-ended unmatched cycles too early: The 4-hour (
- Wipe ALL Data Now Clears Suggested Settings (#185): "Wipe ALL data for this device" previously left the
suggested_settingssensor populated with stale values. The wipe operation now also clears suggestions, envelopes, feedback history, pending feedback, and auto-adjustment logs - all data derived from the cycle history that was just deleted. - Applied Suggestions No Longer Reappear (#185): After a user applied suggested settings, the sensor count stayed at the old value and the suggestions reappeared immediately on the next cycle because the engine re-computes them from the same data. Suggestions are now cleared from the store when the user saves the options form after confirming them. Additionally, the suggestion engine now skips generating a suggestion for any key whose recommended value already matches the current config option, so accepted suggestions stay silent until the underlying data actually shifts.
- Corrected Cycle Duration Not Saved (#155): When a user selected the "Correct" action in the learning feedback dialog and changed the cycle duration, the correction was applied but the profile's
avg_durationstatistic was not always updated. If the cycle had no power data (e.g. imported or stripped by retention),async_rebuild_envelopeskipped the stats update entirely and returned early, leaving the profile showing the old wrong duration. The stats update (min/max/avg) is now computed directly from the stored cycle durations even when the envelope shape cannot be built. Additionally, if no target profile can be determined (both the form field and the pending feedback's detected profile are absent), the duration correction is now applied directly to the cycle using its existing profile, and a warning is logged - previously it was silently dropped. - Current Phase Stays On Post-Cycle (#192): The "Current Phase" sensor kept showing the last detected phase (e.g. "Spin") after a cycle ended instead of resetting to "Off".
_last_match_result- which carries the matched phase label - was passed to the learning manager at cycle end but never cleared in the state teardown block. The sensor'sphase_descriptionproperty reads_last_match_result.matched_phasefirst, so it remained stuck on the last phase until the next periodic_update_estimatescall (up to 5 minutes later)._last_match_resultis now cleared in the same teardown block as the other cycle state, immediately after the learning call that needs it. - Publish-on-Change Sensor Cycle Close Delay (#197): Three related bugs caused cycles to remain open for up to 58 minutes after an appliance physically finished when the power sensor only publishes on value change (e.g. LightwaveRF). Once the appliance stabilised at a standby value below the stop threshold, the sensor went completely silent and the cycle could not close promptly.
- Bug A - No-Update Timeout ignored for low-power silence: The user-configurable No-Update Timeout (
no_update_active_timeout) only applied to high-power states. For low-power silence the watchdog used a separate 3 600 s (low_power_no_update_timeout) ceiling, so a user who set "No-Update Timeout" to 140 s had no effect at all on how quickly the cycle closed after the appliance reached standby. The low-power watchdog now also honoursno_update_active_timeout: once real-update silence exceeds this threshold (and no verified pause is active), a 0 W keepalive is injected on every watchdog tick to advance the off-delay accumulator. With the default 116 s watchdog interval, cycles now close within ~6 minutes of going to standby. Verified pauses (e.g. dishwasher drying confirmed by envelope alignment) continue to be exempted so legitimate long silent phases are unaffected. - Bug B - Profile match always returned
expected_duration = 0.0: Profiles created beforeavg_durationtracking was introduced (or imported without duration data) storedavg_duration = 0. The snapshot-building loop passed this zero to the analysis worker, which propagated it as the match result'sexpected_duration. Every match attempt logged a"update_match: invalid raw_expected_duration"warning and the time-remaining estimate stayed at zero throughout the cycle. The snapshot builder now falls back throughprofile.avg_duration → cycle.duration → len(segment) × dtso a zero value is always replaced with a computed estimate. - Bug C -
update_matchlog always said(> 6h)for zero durations: The validation branch that rejectedexpected_duration <= 0shared a single log message with the> 6hbranch, making it impossible to tell from logs whether the rejection was a zero/negative value or an implausibly large one. The two conditions are now logged separately as(<= 0)and(> 6h).
- Bug A - No-Update Timeout ignored for low-power silence: The user-configurable No-Update Timeout (
- Duplicate Notifications Suppressed: When multiple notification targets were configured, WashData could dispatch the same push notification more than once for a single event if a service appeared in more than one list.
_dispatch_notificationnow deduplicates the resolved service set before sending, so eachnotify.*service receives at most one call per event regardless of how many event-type lists it appears in. - Stale Notification Services Cleared on Save: Removing a
notify.*service from one of the multi-target lists and saving options could leave the old entry in the in-memory runtime set, continuing to fire notifications to the removed service until the next HA restart. The options-flow reload path now fully rebuilds_notify_start_services,_notify_finish_services, and_notify_live_servicesfrom the saved config on every options save, ensuring removed services take effect immediately. - Migration Overwrites User-Cleared Notification Lists: The 0.3.x → multi-target migration used truthiness checks (
if not options.get(...)) so an explicit empty list[](user deliberately cleared a service list) was treated the same as a missing key and overwritten with the migrated service. Checks are now key-presence tests so an explicit[]is preserved. - Notification Collapse Drops Finish Events While Away: When "notify only when home" deferral was active, the pending queue deduped on
event_typefor all event types - a secondcycle_finishwhile the user was out silently dropped the first. OnlyNOTIFY_EVENT_LIVEentries are now replaced; all other events accumulate so multiple completions are all delivered on return. - Auto-Tune Notification Uses Wrong Channel: The ghost-cycle auto-tune notification was always dispatched with
NOTIFY_EVENT_FINISHeven when only start services were configured, resulting in no notification being sent. The event type is now chosen based on which services are actually configured: finish services take priority, then start services. - Pause/Resume State Lost on Crash:
async_pause_cycleandasync_resume_cycledid not persist the updated pause state immediately - it was only written by the periodic 60-second save or on clean shutdown. Both methods now fire-and-forgetasync_save_active_cycleafter committing state, matching the pattern used by the periodic save. - Basic Settings Save Retains Stale
pump_stuck_duration: Saving from the basic settings page without opening advanced settings did not run the cleanup that dropsCONF_PUMP_STUCK_DURATIONwhen the device type is not pump. The cleanup now also runs in the basic settings save path. - Trim Cycle: Wall-Clock Time Not Validated Against Cycle End:
_wallclock_to_offsetonly rejected times earlier than the cycle start (rolling them to the next day), but did not check the upper bound. A time past the actual cycle end produced a valid-looking offset beyond the stored power data. The function now acceptscycle_end_dtand returnsNonefor any offset outside[0, cycle_duration]. - Correction Feedback Applies Learning to Detected Profile Incorrectly: When a user submitted a duration-only correction (no
corrected_profile), the correction path fell back to the detected profile and called_apply_correction_learningagainst it as if the user had confirmed it, potentially shifting that profile's weights incorrectly. The fallback is removed; duration-only corrections are now handled exclusively by the direct cycle-update branch. - Batch Simulation Scheduled Multiple Times Before Completing:
_last_batch_simulation_countwas only updated inside_async_run_batch_simulationon completion. Rapid cycle-ends before the first simulation finished saw the unchanged count and scheduled duplicate jobs. The count is now set immediately before the task is created. - Import Does Not Clear Resample Cache:
async_import_datareplacedself._databut did not clear_cached_sample_segments, leaving stale cache entries from the old dataset that could produce incorrect alignment results until the next HA restart. The cache is now cleared immediately after assigning the new data. - Pump Watchdog Fires Before Stuck-Pump Alarm:
DEFAULT_NO_UPDATE_ACTIVE_TIMEOUT_BY_DEVICEforDEVICE_TYPE_PUMPwas600 s(10 min), shorter thanDEFAULT_PUMP_STUCK_DURATION(1800 s / 30 min). The watchdog terminated running pump cycles before theha_washdata_pump_stuckevent could fire. The timeout is nowDEFAULT_PUMP_STUCK_DURATION + 60 sso the stuck alarm always fires first. - Cycle Count Sensor Uses Wrong State Class:
WasherCycleCountSensordeclaredstate_class=SensorStateClass.TOTAL_INCREASING, butlen(past_cycles)can decrease when cycles are deleted. HA interpreted drops as meter resets. Thestate_classhas been removed. - Workflow: Null Comment Body Causes TypeError: The
close_incomplete_issuesworkflow calledc.body.includes(...)without guarding against null bodies (GitHub API can returnnullfor deleted or empty comments), causing workflow runs to fail. Both predicates now checkc.body != nullfirst.
- Trim Cycle Service: Added a new
trim_cycleHA service call that trims a cycle's stored power trace to a user-specified[trim_start_s, trim_end_s]window. Offsets are renormalized to start at zero, and all cycle metadata -start_time,end_time,duration,signature,sampling_interval- is recomputed and persisted atomically. Useful for removing noisy preamble or lingering standby readings from a recorded cycle. - Trim Cycle UI: Added a "Trim Cycle Data" action under Manage Cycles in the options flow. The UI presents an SVG preview of the full power curve with the kept region highlighted, and separate steps for setting the trim start and end points in whole minutes. Changes can be previewed, reset, and applied without leaving the interface.
- Power Data Preserved for Pending Feedback: Cycles that have an open feedback request are now exempted from the periodic power-data strip pass in
ProfileStore. Previously, the background cleanup could remove the stored power trace before the user had reviewed the feedback dialog, causing the SVG comparison chart to show no data. Pending cycles are identified by their presence inpending_feedbackand are skipped unconditionally until feedback is resolved. - Streamlined Power Data Decompression: Replaced a duplicate inline decompression loop in
ProfileStorewith a call to the shareddecompress_power_data()utility. The utility correctly handles both the legacy ISO-timestamp format and the newer relative-offset format, removing a subtle inconsistency where the old inline code only handled one variant. - Translation Overhaul: Comprehensively reviewed and rewrote all 60 non-English translation files. Machine-translated strings that were semantically incorrect, awkward, or technically wrong have been replaced with idiomatic translations. Common fixes applied across languages include: missing
{suggestions_count}placeholders in settings descriptions; translated JSON parameter key names inapply_suggestions(keys must stay in English); translated MDI icon names; nonsensical state labels (e.g. "jogging" for "running", "country" for "device state", "fire/combustion" for notification trigger); and leftover untranslated English strings.sr-Latn.jsonwas fully rewritten from scratch - the previous file contained Serbian Cyrillic script instead of Latin script. - Full Diagnostics Export: The HA diagnostics download now returns the complete store export - all profiles (including power samples), all past cycles, envelopes, feedback history, auto-adjustments, suggestions, custom phases, and full
entry_data/entry_optionsconfig. Previously it returned only counts and a single-cycle summary, making it nearly useless for debugging. Personally identifiable fields (power_sensor,notify_service,notify_people,name,unique_id, etc.) are still redacted. - Unix Timestamp Format Recognition:
detect_power_data_formatnow identifies absolute Unix epoch timestamps (> 1e8) as theunix_timestampvariant rather than silently discarding them.migrate_power_data_to_offsetsaccepts this format and converts epoch-based traces to relative offsets by subtracting the cycle start time, so imported or externally generated power traces are handled correctly without data loss. - Translated Service Errors: Service validation errors in
trim_cycle(device not found, integration not loaded, cycle missing, invalid trim range, empty trim window) now raiseServiceValidationErrorwithtranslation_domainandtranslation_keyinstead of plainValueErrorstrings, giving users localized error messages in automations and the developer tools panel. - SVG Safety and Robustness: HTML special characters in SVG titles and no-data labels are now escaped, preventing rendering artefacts when cycle names contain
&,<, or>. The "no power data" placeholder in merge-preview and trim-preview SVGs is now a configurable parameter rather than a hardcoded English string, enabling full localization. - Per-Device Logging Extended:
DeviceLoggerAdaptercoverage extended toProfileStoreandCycleRecorder. All log messages from these components now carry the device name prefix, consistent with the rest of the engine. - Removed Stub UI Option: Removed the "Label Multiple Cycles" entry from the Manage Cycles menu. The option was listed in the UI but had no backing implementation, leading to a silent no-op when selected.
- Trim Offsets: Negative Input Clamping:
handle_trim_cyclein__init__.pynow clamps bothtrim_start_sandtrim_end_stomax(0.0, ...)before the range-validity check. Previously a negative input bypassed thetrim_invalid_rangeguard and was forwarded to the store unchanged. - Envelope Persistence After Startup Repair:
async_loadnow callsawait self.async_save()a second time afterasync_rebuild_all_envelopes()completes. Previously, rebuilt envelopes were applied toself._datain memory but never written to durable storage; a subsequent restart would redo the repair and lose any downstream use of the rebuilt data. start_timeString Validation inadd_cycle: Stringstart_timevalues are now validated withdt_util.parse_datetime()(falling back to a float conversion attempt) before being stored as canonical ISO timestamps. Previously any non-empty string passed through unconditionally, so a malformed or placeholder value could reachpower_data_to_offsetsand corrupt the stored offset array.- Test Alignment with
_cleanintest_trailing_zero_impact:duration_beforeand then_trailing == len(…) - len(trimmed)assertion now operate on_clean(pd)rather than the rawpdlist. Malformed rows filtered by_cleaninside_trim_trailing/_trailing_zero_countcaused length mismatches and potential index errors when real-world cycle files contained non-numeric entries. lt.jsonSmart-Quote Corruption: The Lithuanian translation file had itstrim_cycleservices block committed with Unicode curly quotes ("/") used as JSON structural delimiters instead of ASCII". This produced anInvalid JSONerror at startup. The block was rewritten with correct ASCII quoting while preserving the Lithuanian„…"content quotes inside string values.
- Profile Statistics Graph Flat Line: Fixed a bug where newly detected cycles showed a flat line at zero and
0.00 kWhin the Profile Statistics graph. The cycle storage routine (_add_cycle_data) incorrectly treated already-converted offset values ([seconds, power]) as unix timestamps, subtracting the cycle start unix timestamp from them a second time. This produced large negative offsets (≈ −1.7 billion seconds) that causednp.interpin the envelope builder to extrapolate to the boundary power value (typically ~0 W) across the entire time grid, rendering a flat zero curve regardless of actual power draw. - Automatic Recovery of Corrupted Cycles: On startup, the integration now automatically detects and repairs any cycles stored with the corrupted negative offsets (first offset < −10⁸ s is physically impossible for an appliance cycle). The original offsets are recovered by adding the cycle's
start_timeunix timestamp back. Affected cycles are repaired in-place and saved transparently - no manual action required.
- New Device Types: Added full support for Air Fryer (#133) and Heat Pump (#134), with optimized defaults and custom icons (
mdi:pot-steam,mdi:heat-pump). - Anti-Wrinkle Mode: Added a dedicated anti-wrinkle state for dryers and washer-dryer combos, including state transitions and shielding (#68).
- Card Customization: Added new dashboard card settings including specialized toggles for
Spinning Icon,Show State,Show Program, andShow Details. - Automated Translation Sync: Enhanced
translate.pyto automatically update the frontend card'sTRANSLATIONSobject from language files, providing out-of-the-box localization for all 27+ supported languages. - Inverted External Trigger: Added a new setting to invert the logic of the external cycle end trigger. Users can now choose to complete a cycle when an external binary sensor turns OFF instead of ON.
- Randomized Cache Buster: The dashboard card now uses a timestamp-based cache buster that refreshes every time the integration is loaded, ensuring immediate updates without browser cache clearance.
- Action-Based Notifications: Added notification actions with priority dispatch and fallback routing (actions → notify service → persistent notification).
- Presence-Gated Notifications: Optional home/away gating to defer notifications until a tracked person is home.
- Live Progress Mobile Notifications: Added in-place companion-app live updates (
cycle_live) with per-cycle throttling, overrun protection caps, mobile-only payload routing, and automatic clear on cycle completion. - Feedback Review Power Visualization: Added an inline SVG chart in "Review Learned Feedbacks" that overlays the current cycle trace with learned profile data for faster manual verification.
- Multi-Profile Comparison Graph: Feedback review now renders all candidate profiles in a single combined chart, highlighting the detected profile and showing the actual cycle trace for direct visual comparison.
- Top Match Candidates Summary: Added ranked candidate details (confidence, MAE, correlation, duration ratio) to feedback review to improve correction decisions.
- Per-Device Log Context: All log messages emitted by the core engine (
WashDataManager,ProfileStore,CycleDetector,LearningManager,CycleRecorder) now include the device name as a[Device Name]prefix. When running two or more devices simultaneously, every log line is immediately attributable to its source without cross-referencing entry IDs. - UI Menu Clarity: All
SelectSelectordropdowns in the configuration flow now useSelectOptionDictwith explicit human-readable labels (e.g. "Split a Cycle (Find gaps)", "Export All Data", "Confirm - Correct Detection"). Previously, raw internal values such as"split"or"auto_label_cycles"were displayed directly in the UI. - Translation Cleanup: Removed stale action option keys from
strings.jsonanden.jsonthat were no longer backed by selectors in the config flow (assign_mode, export/import mode, cycle history editor actions, and several management menu entries). Reduces translator noise and prevents spurious untranslated keys in other languages. - Phase Catalog Translations: Extended
manage_phase_catalogaction labels and descriptions to Swedish, Tamil, Telugu, and Simplified Chinese. - Unified Time Handling: Refactored the core engine to use a single canonical offset-based time format for storage. Includes automatic migration of legacy data to prevent corruption and fixes "offset-naive/offset-aware" comparison bugs (#144).
- Profile Rename Cascade: Renaming a profile now automatically updates all historically recorded cycles and pending feedback requests, maintaining end-to-end data integrity (#154).
- Detection Persistence: Implemented temporal persistence for profile matching. Start notifications now only fire after a match remains stable over several intervals, drastically reducing false or jittery alerts.
- Enhanced Feedback Resolution: Overhauled the feedback resolution flow with new "Delete" and "Ignore" actions, giving users more granular control over learned cycles.
- Sub-State Extraction: The dashboard card now intelligently extracts and displays the specific phase from the state (e.g., showing "Rinsing" instead of "Running (Rinsing)") for a cleaner UI experience.
- History Timeline Restoration: Restored categorical history diagrams for State and Program sensors by implementing the
enumdevice class (#157). - Localized Menus: Updated configuration flow to use
SelectSelector, enabling natively localized menu options across all supported languages. - Notification Event Toggle: Added
notify_fire_eventsoption to control emission of cycle start/end events. - Migration Normalization: Added migration helper defaults for new notification options to ensure deterministic upgrades.
- Notification Options UX: Moved notification settings to a dedicated "Notifications" options step and removed duplicate live-enable controls, using event selection as the single source of truth.
- Live Progress Match-Aware Flow: Live notifications now send a one-time "no profile matched yet" message before detection converges, then switch to periodic progress updates only after a profile duration is available.
- Ultra-Long Cycle Support: Significantly improved handling for modern high-efficiency dishwashers with cycles exceeding 230 minutes.
- Increased
DEFAULT_MAX_DEFERRAL_SECONDSto 4 hours to prevent long silent Eco drying phases from being cut off. - Extended dishwasher-specific
NO_UPDATE_ACTIVE_TIMEOUTto 4 hours. - Increased the default dishwasher
MIN_OFF_GAPto 1 hour to prevent fragmentation when no profile is matched.
- Increased
- Robust Zombie Killer: Refined the "Zombie Killer" hard-limit to be more lenient, now triggering at 300% of expected duration (previously 200%) and requiring at least 4 hours of runtime. This prevents premature termination of long-running appliances while still protecting against runaway ghost cycles.
- Device-Aware Suggestions: The
SuggestionEngineis now aware of the configured device type and uses device-specific safety floors foroff_delayrecommendations, preventing it from suggesting dangerously short timeouts for dishwashers. - Translation Tool Docs: Added documentation for the Home Assistant integration translation helper script.
- Learning Pipeline Context Propagation: Propagated runtime match ranking through manager/learning flow so feedback requests retain candidate context.
- Feedback Chart Readability: Increased chart and legend typography and spacing to improve readability on Home Assistant dialogs.
- Phase Assignment Visualization: Replaced ASCII timeline with interactive SVG power curve chart showing average cycle profile, colored phase spans, and gating line boundaries for better profile phase visualization.
- Phase Catalog Management: Implemented full create/edit/delete capabilities for custom phases in the phase catalog, allowing users to build device-specific phase vocabularies. Default phases can also be edited, with overrides automatically stored in the custom phases list.
- Device-Type Phase Filtering: Phase options in the profile assignment flow are now automatically filtered by the currently selected device type, ensuring only relevant phases appear in dropdowns.
- Cross-Device Catalog View: "Manage Phase Catalog" now displays and groups phases for all supported device types in one place, instead of only the current integration device type.
- Phase Action Wording Cleanup: Updated phase management action labels to clearer wording ("Create New Phase", "Edit Phase", "Delete Phase").
- Current Phase Sensor Exposure: Added a standard device sensor for current phase (
sensor.<device>_current_phase) so active phase is visible in normal entity views without enabling diagnostics. - Phase-Only Offset Input: Simplified phase assignment to use offset-based time entry (minutes from cycle start) instead of timestamp selection, reducing complexity and user error.
- Suggested Settings Discoverability: Improved the Suggested Settings UX with sensor-first guidance, a one-time "suggestions ready" notification when recommendations become available, and an explicit review step before suggested values are staged in Advanced Settings.
- Phase Unique ID Management: Built-in and custom phases are now assigned stable unique IDs. Phase catalog operations (edit, delete) resolve by ID rather than by name, eliminating ambiguity when similarly-named phases exist across different scopes. Includes improved error handling for rename/delete conflicts.
- Duration Consistency Metric: Profile sensor attributes now expose a
consistency_minfield (standard deviation of recorded cycle durations, in minutes), allowing users to diagnose variability in learned profiles directly from the entity state. - Signal Processing Edge-Case Guards: Added guards against non-positive step and gap values in the resampling pipeline, preventing division-by-zero and NaN propagation in high-noise or sparse-sensor environments.
- Cycle Detector Numeric Guards:
update_matchnow validates confidence and expected-duration values withmath.isfinite(), falling back to0.0with a debug log entry instead of propagating NaN or infinity into downstream sensors. - Suggestion Engine Resilience: Added
TypeError/ValueErrorguards when parsing profile data in theSuggestionEngine, preventing crashes when stored profile fields contain unexpected types. - Card Registration Resilience: Dashboard card asset registration now catches all setup exceptions and logs a warning, allowing the rest of the integration to load normally; setup will retry on the next Home Assistant restart.
- Live Notification Interval Constraint: Added minimum-value constraint documentation for
notify_live_interval_secondsacross all supported languages, guiding users away from excessively short polling intervals. - Diagnostics Sensitive Data Redaction: Config entry diagnostics now automatically redact personally identifiable fields (
notify_service,notify_people,notify_actions,power_sensor,external_end_trigger) before the report is generated. - Services Description Consistency: Updated
services.yamldescriptions to consistently refer to "WashData" instead of "washing machine", accurately reflecting multi-device support. - Enhanced Issue Templates: Overhauled bug report and feature request templates to cover all 8 supported device types (Washing Machine, Dryer, Washer-Dryer Combo, Dishwasher, Coffee Machine, Electric Vehicle, Air Fryer, Heat Pump). Improved debug log guidance to clearly distinguish copy-paste (short logs) from file attachment (long logs), and added a separate note explaining that HA diagnostics exports are distinct from debug logs and must be attached as files. Added a pre-submission checklist requiring reporters to confirm they searched for duplicates and filled in all applicable fields. Translation contributions are now handled via a dedicated Pull Request template instead of an issue template.
- Issue Triage Automation: Added a GitHub Actions workflow that automatically validates new and edited issues, comments with targeted guidance on any missing required sections, and applies a
needs-more-infolabel - keeping the issue queue actionable without manual triage. Supports adry_runinput for safe testing without posting comments or modifying labels. - Auto-Close Done Issues: Added a GitHub Actions workflow that automatically closes open issues labelled
doneafter 5 days of inactivity, provided the last comment is from the maintainer. Posts a closing comment with reopen instructions before closing.
- Manual Recording Revert (#151): Fixed an issue where manual recordings could unexpectedly revert configuration changes.
- Data Import Fix (#152): Resolved a bug that prevented successful data imports into the profile store.
- Profile Store Reliability (#155): Fixed synchronization issues when updating profile statuses and statistics.
- Translation Consistency: Synchronized
en.jsonwithstrings.jsonto ensure a canonical source of truth for translations. - Energy Threshold Defaults: Fixed a bug where
start_energy_thresholdandend_energy_thresholdwere incorrectly defaulting to 0.0W in the detector configuration, which could lead to premature cycle ends in noisy environments. They now correctly respect device-specific constant defaults. - Config Reload Consistency: Added missing energy threshold updates to the configuration reload logic, ensuring settings take effect immediately when changed in the UI.
- Config Flow Null Option Guard: Fixed a crash in the options flow where a
SelectSelectorentry with aNonevalue would cause aKeyErrorduring form processing. Such entries are now silently skipped. - Profile Stats After Deletion: Fixed
async_rebuild_envelopeincorrectly computingmin_durationandmax_durationfrom the outlier-filtered duration set.min/maxnow reflect the true observed range of all cycles; onlyavg_durationuses the IQR-filtered set for robustness. This means deleting an outlier cycle now correctly recalculates the profile's duration range. - Feedback Translation Placeholder Mismatch: Fixed options-flow description placeholders (
{comparison_data}) to prevent missing-value translation errors during feedback review. - Feedback SVG Legend Clipping: Fixed a viewBox height mismatch that could render legend content outside the visible area.
- Global Phase Duplication in Selectors: Fixed edit/delete phase dialogs duplicating "All Devices" phases once per device type by introducing scoped deduplication and explicit scope keys.
- Phase Scope Resolution in Edit/Delete: Fixed phase edit/delete operations to resolve against the selected phase scope (
device_type) so similarly named phases remain deterministic. - Notification Action Script Context: Fixed Home Assistant script action execution by passing a valid
Contexttoscript.async_run, resolving runtime errors like "Running script requires passing in a context". - Notification Dispatch Ordering: Fixed a routing regression where configured actions could suppress notify-service delivery; actions and mobile notifications now run together as expected.
- Live State Coverage: Fixed live-progress gating to continue updates during
STATE_ENDING(not onlyRUNNING/PAUSED) until cycle completion. - Legacy Phase Diagnostic Confusion: Removed stale diagnostic phase entity behavior and added cleanup of the old
wash_phaseregistry entry to prevent misleading duplicate/legacy phase sensors. - Orphaned Diagnostic Entity Cleanup: Added automatic registry reconciliation for diagnostic entities on startup and profile updates, removing stale unavailable duplicates (including old
profile_count_*entries left behind by profile renames) without manual per-device cleanup. - End-Spike Revert to Running: Fixed a bug where a power burst during the
ENDINGstate (e.g. the final pump-out on a dishwasher) could incorrectly transition the device back torunning, delaying the reported cycle end by several minutes. Thelong_ending_tailguard (ignore any spike after 120 s inENDING) now applies to all device types, not only dishwashers. _time_in_stateReset After Config Reload: Fixed a bug where reloading the integration configuration reset the in-state timer to zero. On the next power spike the guard checked0 s >= 120 sand failed, causing a falseending → runningtransition. The timer is now recomputed from the persistedstate_enter_timeon restore so the guard works correctly across reloads.- Cycle Split "Unknown Error" (#167): Fixed a
TypeError: argument must be strcrash inanalyze_split_synccaused by a dead-codedt_util.parse_datetime(cycle["start_time"])call that failed whenstart_timeis adatetimeobject rather than an ISO string. The parsed value was never used; the line has been removed. - Unknown Cycle Statistics Empty (#168): Fixed two root causes for statistics (graph and energy) being blank after assigning an unknown cycle to a new profile.
_rebuild_envelope_syncnow uses the shared_decompress_power_data()helper instead of raw iteration withfloat()conversion. The raw conversion silently discarded every data point for cycles stored in the legacy ISO-timestamp format, producing an empty envelope and a blank graph.create_profile_standalonenow labels the reference cycle with the new profile name (when it is currently unlabeled) and immediately rebuilds the envelope, so statistics are populated as soon as the profile is created - without requiring a separate feedback-correction step.
- Merge Preview Graph Empty: Fixed the cycle merge preview showing a blank graph. When cycles have no recorded power data, a proper "No power data available for preview" placeholder is rendered instead of a broken empty image. The config flow also falls back to italic text if the SVG cannot be built.
- Broken Duration After Merge: Fixed merged cycles showing wildly incorrect durations (e.g.
-29555478m). Two root causes were addressed:- Sort order: cycles were sorted lexicographically by ISO timestamp string, which gives wrong chronological order when start times use different UTC offset representations (e.g.
+01:00vs+02:00). Sorting now uses the parsed UTC timestamp. - Corrupt
last_t_abs: the end timestamp was taken from the last element of the power-data list, which could be a corrupted or out-of-order entry near Unix epoch. The merge now takes the maximum absolute timestamp across all collected data points, and falls back to the cycle's storedend_timefield when no power data is available at all.
- Sort order: cycles were sorted lexicographically by ISO timestamp string, which gives wrong chronological order when start times use different UTC offset representations (e.g.
manual_durationPersisting After Merge: The merged cycle no longer inherits the source cycle'smanual_durationoverride, ensuring the freshly computed duration is always displayed.- Merge Preview SVG Encoding: Fixed the merge-preview placeholder SVG (shown when cycles contain no recorded power data) to properly XML-escape the title and label strings. Profile names or translated strings containing
&,<, or>no longer produce malformed SVG output. - Translation Corrections (Bosnian, Traditional Chinese): Removed stray zero-width space characters (U+200B) from the Bosnian
no_power_previewstring, and corrected the Traditional Chineseno_power_previewvalue which was incorrectly using Simplified Chinese characters (没有/预览/数据→沒有/預覽/數據).
- HA Test Harness Adoption: Replaced
MagicMock-based hass objects with realHomeAssistantinstances frompytest_homeassistant_custom_componentacross all new test modules. OnlyProfileStoreandCycleDetectorare patched as true external I/O boundaries. - Event Payload & Ghost Cycle Tests (
test_manager_event_payload_and_ghosts.py): CoversEVENT_CYCLE_ENDEDpayload field exclusion (power_data,debug_data,power_trace) and ghost-cycle energy threshold detection using real HA event bus listeners. - Migration Harness Tests (
test_migration_harness.py): Validatesasync_migrate_entryfield movement (data → options), idempotency on re-run, and no-op behaviour when already at the latest schema version. - Pre-Completion Notification Tests (
test_manager_precompletion_harness.py): Pins the ambiguity gate - notifications are suppressed when_last_match_ambiguous=True, sent exactly once when unambiguous, and not re-sent on subsequent calls. - Match Persistence / Transition Tests (
test_manager_matching_harness.py): Covers the full persistence-counter state machine inside_async_do_perform_matching: single-call accumulation, below-threshold staying atdetecting..., threshold commit, profile-change counter reset, high-confidence override bypassing persistence, and ambiguous-result gating. - Live Notification Harness (
test_manager_live_notifications.py): Added focused coverage for mobile-only routing, payload keys, overrun cap enforcement, away-mode deferred live coalescing, clear-on-end behavior,STATE_ENDINGsupport, one-time pre-match waiting message, and post-match periodic update activation. - Phase Catalog Atomic Operations (
test_issue_166_phase_catalog.py): Validates that phase renames and deletions behave atomically with unique-ID resolution, preventing unintended collisions or cascading updates across scopes. - Profile Sensor Attributes (
test_profile_sensor_attributes.py): Covers the exposure of new sensor attributes includingconsistency_minon profile sensors. - Diagnostic Entity Cleanup (
test_diagnostic_entity_cleanup.py): Validates automatic removal of orphaned diagnostic entities on startup, covering stale profile count sensors, legacywash_phaseentries, and debug entity visibility toggling based onexpose_debug_entities.
- Manual Recording Trimming: Fixed a bug where manual recordings (e.g., Dishwashers in Eco mode) were internally shortened by incorrectly snapping the cycle duration to the last recorded power reading, losing trailing silence like drying phases.
- Profile Statistics Accuracy: Corrected profile duration calculations to use the authoritative cycle duration instead of data-offset bounds. This fixes incorrect remaining-time predictions and profile "shrinkage" over time.
- Aggressive Tail Trimming: Modified recorder suggestions to be less aggressive. Suggested tail trims are now
0.0for silence periods under 10 minutes, protecting legitimate silent phases in appliances. - Data Optimization Logic: Fixed maintenance logic that was incorrectly snapping durations to the last power reading during start-time shift corrections.
- Envelope Reconstruction: Updated the statistical engine to correctly respect explicit cycle durations even when power sensor updates are sparse or missing at the end of a run.
- Advanced Parameter Auto-Suggestion:
- New
SuggestionEnginethat analyzes your appliance's actual power traces to recommend optimal settings. - Automatically suggests values for
start_threshold_w,off_delay,watchdog_interval, and more based on observed behavior. - Periodic background optimization to keep settings tuned as your appliance ages or use patterns change.
- New
- Electric Vehicle (EV) Support:
- Added new "Electric Vehicle" device type with optimized defaults.
- New icons and phase heuristics ("Charging", "Maintenance").
- Divergence Detection:
- Improved matching logic to detect when a cycle starts diverging from its matched profile.
- Automatically reverts to "Detecting..." if confidence drops significantly below the cycle's peak (default 40% drop).
- Card Animation:
- Added native spinning animation to the dashboard card icon when the appliance is running.
- Configurable Stability Thresholds:
- Added
DEFAULT_MATCH_REVERT_RATIOandDEFAULT_DEFER_FINISH_CONFIDENCEtoconst.pyfor easier fine-tuning.
- Added
- Profile-Aware Watchdog:
- The watchdog now uses "look-ahead" logic from the matched profile to prevent premature cycle termination during long legitimate pauses (e.g., dishwasher drying).
- Automatically extends silence timeouts if the cycle is within its expected profile duration.
- Zombie Protection:
- Implemented a hard "Zombie Killer" limit that force-ends cycles exceeding 200% of their expected profile duration (min 2 hours).
- Stuck Power Prevention:
- Automatically resets the power sensor to 0W when a cycle is forced to end by the watchdog or manual stop, fixing issues where the entity remained at a high value.
- Zero-Latency Low-Power Processing:
- Power updates below
min_powernow bypass all debouncing, smoothing, and sampling interval filters, ensuring immediate cycle-end detection.
- Power updates below
- Program Detection Stability:
- Implemented temporal persistence for profile matching: requires 3 consecutive consistent matches before switching from "detecting..." to a profile, or before unmatching a profile.
- Added a minimum confidence gap for mid-cycle profile switching to prevent "flapping" between similar programs.
- Total Duration Sensor:
- New
total_durationsensor providing the predicted total cycle time (Elapsed + Remaining). - Designed specifically to support full progress bars in
timer-bar-card. - Dynamically updates as estimates are refined.
- New
- Manual Recording Robustness:
- Increased gap threshold to 6 hours to support very long Eco cycles with multi-hour silent phases.
- Unified automatic trimming threshold to 1.0W across the integration.
- Clean Card UI:
- Removed redundant "off" label from completion details when the appliance is inactive.
- Zigbee2MQTT Guidance:
- Added optimized configuration tips for Z2M smart plug users in README.
- Profile Alignment Error (#112):
- Fixed a critical
TypeError: 'float' object is not subscriptablein the profile matching pipeline.
- Fixed a critical
- Terminal State Persistence:
- Fixed a bug where
finishedandinterruptedstates were not resetting tooffafter the intended 30-minute timeout.
- Fixed a bug where
- Profile Shrinking:
- Fixed an issue where maintenance tasks would aggressively trim trailing silence from completed cycles, causing profiles to shorten over time.
- Termination Hangs:
- Restricted "Deferred Finish" logic to require high confidence (> 0.55) or a verified pause, preventing cycles from hanging on mismatched long profiles.
- Long Drying Phase Support:
- Fixed an issue where dishwashers with multi-hour silent drying phases were being split into multiple cycles by the watchdog.
- Recognizes "Verified Pause" from profile envelope to extend silence timeouts.
- Test Stability:
- Resolved several
TypeErrorissues in the test suite and improved mocking reliability for sensors and configs.
- Resolved several
- Persistent Terminal States:
- Implemented proper
finished,interrupted, andforce_stoppedstates that persist for 30 minutes after cycle completion. - Improves visibility of cycle outcomes in the UI (users can now see "Finished" instead of just "Off").
- Auto-resets to
offafter 30 minutes, or immediately if a new cycle starts.
- Implemented proper
- Coffee Machine Defaults: Added dedicated defaults for coffee machines (faster sampling, shorter timeouts) to improve detection out-of-the-box.
- French Translation: Added full French localization (thanks to @MaximeNagel).
- Profile Sorting: Improved sorting for profile lists (natural sort), ensuring correct numeric order (e.g.
1, 2, 10instead of1, 10, 2). - Refactored Device Defaults: Consolidated and cleaned up device-specific default settings logic.
- Test Suite: Enhanced test coverage for cycle state transitions and manager notifications.
- Stuck Power Value: Fixed issue where the power entity would get stuck at the last non-zero value after a cycle ended.
- Timezone Display: Fixed issue where timestamps in specific UI menus were shown in GMT instead of local time.
- Advanced Settings Error: Fixed a crash that prevented advanced settings from being saved in the configuration flow.
- State Logic: Fixed assertions and logic validation for terminal states.
- Notification Tests: Fixed test environment formatting for notification services.
Major Architectural Rewrite ("vNext")
This release marks a complete re-engineering of the WashData core, transitioning from simple heuristics to a rigorous signal processing pipeline and robust state machine. While the version number is minor, this is effectively a new engine under the hood.
🎉 Milestones Reached!
- WashData is now available in the HACS Default Repository!
- Passed 1,000 active installations across the community.
- Reached 500+ stars on GitHub.
Thank you to everyone who has been patient during development and to all contributors who provided invaluable feedback, bug reports, and feature suggestions. This release wouldn't be possible without you!
Important
Fresh Start Recommended
This release includes significant changes to how cycles are detected and profiles are matched. The new engine depends on clean, accurate data to work properly.
If you're unsure whether your previously recorded cycles were captured correctly (e.g., cycles that ended prematurely, incorrectly merged fragments, or noisy data from before tuning your thresholds), we recommend:
- Delete your existing cycle history via Configure → Manage Cycles → Delete All
- Use the new "Record Cycle" feature to capture fresh, clean training data for each program you use
This ensures the best possible matching accuracy with the new architecture.
-
New Signal Processing Engine (
signal_processing.py):- Dt-Aware Integration: Replaced simple averaging with trapezoidal Riemann sum integration (
integrate_wh) that respects variable sampling intervals. - Robust Smoothing: Implemented
robust_smooth, a hybrid algorithm combining a Median Filter (spike rejection) with a Time-Aware Exponential Moving Average (EMA) for clean trend detection. - Adaptive Resampling: New primitives (
resample_adaptive,resample_uniform) handle irregular sensor updates and enforce strict gap handling (no interpolation across large gaps). - Idle Baseline Learning: Automatically learns the device's "true zero" using Median Absolute Deviation (MAD), removing the need for manual calibration.
- Dt-Aware Integration: Replaced simple averaging with trapezoidal Riemann sum integration (
-
Finite State Machine (FSM):
- Replaced binary ON/OFF logic with a formal FSM:
OFF→STARTING→RUNNING↔PAUSED→ENDING→OFF. - Dt-Aware Gating: Start/End detection now uses accumulated time/energy gates (e.g., "energy since idle > X Wh") rather than sample counts, making it immune to sensor update frequency.
- Smart Pausing: Distinguishes between "End of Cycle" and "Mid-Cycle Pause" using dynamic thresholds derived from the sensor's sampling cadence (
_p95_dt).
- Replaced binary ON/OFF logic with a formal FSM:
- Profile Store v2 (
profile_store.py):- New Schema: Introduced a versioned storage schema (v2) optimized for performance.
- Trace Compression: Historical power traces are now compressed using relative time deltas, significantly reducing disk usage.
- Robust Migration: Included a designated
WashDataStoreengine that automatically upgrades v1 data to v2 without data loss, preserving user labels and corrections.
- Configurable Sampling Interval: New "Sampling Interval" setting allows users to throttle high-frequency sensors (e.g., 1s updates) to reduce CPU load.
- Precision Configuration: Configuration flow now uses Text Box inputs for all numeric thresholds, offering precise control over parameters like
start_energy_threshold(Wh) anddrop_ratio. - Smart Resume: "Resurrection" logic restores the exact cycle state (including sub-state) after a Home Assistant restart.
- Auto-Labeling: Increased default confidence threshold to 0.75 (from 0.70) to leverage the improved accuracy of the new engine.
- Diagnostic Sensors: Added dynamic diagnostic sensors for each profile (e.g.,
sensor.washdata_..._profile_cotton_count) showing the total cycle count properly. - Statistics: Added "Total Energy" column to the Profile Statistics table, showing the cumulative energy consumed for each profile.
- Low-Rate Polling Support: Optimized default settings for devices with 30-60s update intervals (e.g., Shelly Cloud, Tuya), including a 30s watchdog and 180s off-delay.
- User Experience: moved "Review Learned Feedbacks" to the main menu (bottom) for easier access, and removed confusing options.
- Logging: Added more granular
termination_reasonlogging (e.g.,smart,timeout,force_stopped) tocycle_detectorandprofile_store. - Timezone Robustness: Complete refactor to use timezone-aware datetimes (
dt_util.now()) exclusively, permanently fixing "offset-naive/offset-aware" comparison errors. - Strict Typing: Codebase now strictly adheres to type hinting, with extensive use of
TypeAliasanddataclassfor internal structures. - Performance: Optimized
last_match_detailssensor attribute to exclude large raw data arrays, preventing Home Assistant state update bloat. - Serialization: Fixed
MatchResultJSON serialization issues that were blocking sensor updates.
- Premature Termination & Dishwasher Logic: Major robustness improvements for dishwashers.
- Implemented "Verified Pause" logic to prevent early termination during long drying phases.
- Added "End Spike Wait Period": Dishwashers now wait up to 5 extra minutes after expected duration to capture final pump-out spikes.
- Increased Smart Termination duration ratio to 0.99 (from 96%) to ensure strictly conservative termination for dishwashers.
- Ghost Cycles: Enhanced filtering and elimination of false detection.
- Persistent Suppression: The "Suspicious Window" (20 min) now persists across restarts (restoring
last_cycle_end), preventing end-spikes from triggering ghosts after reboots. - Tail Preservation: Disabled "zero trimming" for confirmed completed cycles, preventing the "profile shrinking" feedback loop where tails were lost.
- Implemented
completion_min_secondslogic to ignore brief spikes.
- Persistent Suppression: The "Suspicious Window" (20 min) now persists across restarts (restoring
- Start/End Flutter: Start debounce and End repeat counts are now configurable and backed by robust accumulators, eliminating false starts/ends.
- Cycle Detector: Adjusted duration validation logic to strict 90%-125% window for completion.
- Translations: Fixed "intl string context variable not provided" errors in logs by properly passing placeholders to translation engine.
- Debug Sensors: Fixed "Top Candidates" sensor showing "None" due to missing data propagation.
- Code Quality: Addressed various linting issues (indentation, whitespace, unused arguments).
- Crash Fixes: Resolved
UnboundLocalErrorand specific edge-case crashes inprofile_store.pyduring migration. - Critical Fix (Runtime Matching): Fixed an issue where runtime profile matching was blocking the event loop and skipping DTW; now uses the full async pipeline.
- Legacy Data Repair: Added automatic reconstruction of missing
time_gridin old profile envelopes to prevent errors. - Validation: Fixed missing
dtw_bandwidthkey instrings.jsoncausing config flow validation errors. - Maintenance Safety: Fixed aggressive cleanup logic that was deleting empty/new profiles (pending training); these are now safely preserved.
- Test Suite: Fixed verification tests for Smart Termination and Profile Store matching.
- Visual Settings Guide: Expanded
SETTINGS_VISUALIZED.mdwith comprehensive documentation for 20+ parameters, organized into logical sections (Signal Conditioning, Detection, Matching, Integrity, Interruption, Learning, Notifications). - Complete Parameter Coverage: All advanced settings now documented with explanations, including
sampling_interval,watchdog_interval,profile_match_threshold,duration_tolerance,learning_confidence,auto_label_confidence, andabrupt_drop_ratio.
- Removed
auto_merge_gap_seconds: This setting was never used in the actual merge logic; removed from code, config flow, and translations. - Removed
auto_merge_lookback_hours: Similar unused legacy setting removed from codebase and UI. - Fixed Unused Imports: Cleaned up unused
DEFAULT_PROFILE_MATCH_MAX_DURATION_RATIOand duplicate import warnings. - Fixed F-String Warning: Removed empty f-string in config_flow post-process step.
- Legacy Logic: Removed "consecutive samples" based detection in favor of time-aware accumulators.
- Sliders: Removed slider inputs in config flow in favor of precise text inputs.
- Manual Control: "Force End Cycle" button to manually terminate stuck cycles (treats as "Completed" and saves data).
- Reliability: "Cycle Resurrection" logic to restore active cycle state after Home Assistant restarts.
- Reliability: "Smart Resume" estimation to provide progress updates during "detecting..." phase based on historical averages.
- Smart Cycle Extension: New feature to prevent premature cycle termination during long low-power phases (e.g., dishwasher drying).
- Statistics: Energy (kWh) estimates added to Profile Statistics table.
- UI: Added legends to profile graphs and scaled them up by 50% for better readability.
- Config Flow: Split "Manage Data" into "Manage Cycles" and "Manage Profiles" menus for better usability.
- Config Flow: Added ability to manually edit profile "Average Duration" for tuning Smart Extension.
- Icons: Fixed missing icon for "Cycle Program" select entity (now dynamically adapts to device type).
- Frontend: Added missing "min" unit to "Time Remaining" display on card.
- Logic: Improved filtering of "Ghost Cycles" (extremely short noise events).
- Bug: Fixed JSON serialization error preventing status updates in some cases.
- Bug: Ensure stats are immediately rebuilt after merging cycles.
- Translation Keys: Corrected missing labels for "Smart Extension Threshold" and other advanced settings.
- Manual Duration for Profiles: Users can now specify a manual "Baseline Duration" when creating profiles, useful for setting up profiles without historical data (e.g., "Eco Mode - 180 mins").
- Onboarding First Profile Step: New users are now prompted to optionally create their first profile immediately after setting up the device, streamlining the initial experience.
- Automatic Recalculation: Deleting a cycle now automatically triggers a recalculation of the associated profile's statistical envelope, ensuring accurate estimates even after cleaning up bad data.
- Translations: Fixed missing text labels in the "Create Profile" modal and Onboarding flow.
- Configuration Flow: Resolved
AttributeError: _get_schemain the initial setup step. - Mocking Issues: Improved test verification process for Config Flow.
- Manual Override: Fixed issue where unselecting a manual profile while idle would not clear the program sensor.
This release marks a significant milestone for WashData, introducing intelligent profile-based cycle detection, a dedicated dashboard card, a completely rewritten configuration experience, and major improvements to cycle detection and time estimation.
Brand new in v0.3.0! A native-feeling Lovelace card designed specifically for washing machines, dryers, and dishwashers.
- Compact Tile Design: A sleek 1x6 row layout that fits perfectly with standard Home Assistant tile cards
- Dynamic Styling: Configure a custom Active Icon Color (e.g., Green or Blue) that lights up only when the appliance is running
- Program Display: Directly shows the detected or selected program (e.g., "Cotton 60°C") via the new Program Entity selector
- Smart Details: Toggle between "Time Remaining" (estimated) and "Progress %" as the primary status indicator
- Shadow DOM Implementation: Isolated styling to prevent conflicts with other Home Assistant components
New cycle detection capabilities powered by NumPy:
- Profile-Based Detection: Learns appliance cycle patterns and uses shape correlation matching (MAE, correlation, peak similarity) to identify cycles in real-time
- Predictive Cycle Ending: Short-circuits the
off_delaywait when a cycle matches with high confidence (>90%) and is >98% complete, reducing unnecessary wait times by up to 30 seconds - Confidence Boosting: Adds a 20% score boost to profile matches with exceptionally high shape correlation (>0.85)
- Smart Time Prediction: Detects high-variance phases (e.g., heating) and "locks" the time estimate to prevent erratic jumps during unstable phases
- Cycle Extension Logic: Automatically extends cycles when profile matching indicates the appliance is still running, preventing premature cycle end detection
- Sub-State Reporting: Displays detailed cycle phases (e.g., "Running (Heating)", "Running (Cotton 60°C - 75%)") for better visibility
- Profile Persistence: Detected cycle profile names are now persisted and restored across Home Assistant restarts
- New
select.<name>_program_selectentity for manual program overrides and system teaching - Allows users to manually select the active program, helping the system learn and improve detection accuracy
The configuration flow has been rebuilt from the ground up to be friendlier and more organized:
- Two-Step Wizard:
- Step 1 (Basic): Essential settings (Device Type, Power Sensor) and Notifications are now properly grouped here
- Step 2 (Advanced): Accessible via the "Edit Advanced Settings" checkbox, containing fine-tuning options for power thresholds and timeouts
- Smart Suggestions: The "Apply Suggested Values" feature is now integrated into the Advanced step, helping you easily adopt values learned by the engine
- Reactive Synchronization: All profile/cycle modifications (create/delete/rename/label) trigger updates to keep the
selectentity in sync - Precision UI: Replaced sliders with precise text-based box inputs for all configuration parameters
- Start Duration Threshold: Now configurable in advanced settings (previously hard-coded)
New logic to handle tricky appliances and prevent false detections:
- Start Debounce Filtering: Configurable debounce period to ignore brief power spikes before confirming cycle start
- Running Dead Zone: A new setting to ignore power dips during the first few seconds of a cycle (useful for machines that pause shortly after starting)
- End Repeat Count: Requires the "Off" condition to be met multiple times consecutively before finishing a cycle, preventing false cycle ends during long pauses/soaking
- Ghost Cycle Prevention: Added
completion_min_secondsto filter out short "noise" cycles from being recorded as completed - Device Type Configuration: Support for multiple appliance types (washing machine, dryer, dishwasher, coffee machine) with device-type-aware progress smoothing thresholds
- EMA Smoothing: Implemented Exponential Moving Average smoothing for progress and time-remaining sensors, eliminating the "jumping" behavior seen in previous versions
- Monotonic Progress: The progress percentage is now (almost) strictly enforced to never go backwards, ensuring a consistent countdown experience
- Smoothed Progress Initialization:
_smoothed_progressis now properly initialized in__init__to avoid runtime errors - Smart Phase Detection: High-variance phases are detected and handled separately to prevent estimate instability
- Configurable alerts (
notify_before_end_minutes) before estimated cycle end - Helps users prepare for cycle completion without constant monitoring
- Data-Driven Tests: New test suite
tests/test_real_data.pyreplays real-world CSV/JSON cycle data - Manager Tests: New
tests/test_manager.pyfor comprehensive manager functionality testing - Profile Store Tests: New
tests/test_profile_store.pyfor storage and matching validation - Restart Persistence Tests: New
tests/repro/test_restart_persistence.pyto verify state recovery - Cycle Detector Improvements: Enhanced
tests/test_cycle_detector.pywith new test cases - Conftest Utilities: Added
tests/conftest.pywith shared test fixtures
- GitHub Actions Workflows: Added
hassfest.ymlandvalidate.ymlfor automated validation - Enhanced Mock Tooling:
devtools/mqtt_mock_socket.pynow supports:--speedup X: Compresses time for faster testing--variability Y: Adds realistic duration variance (default 0.15) for shape matching validation--fault [DROPOUT|GLITCH|STUCK|INCOMPLETE]: Injects anomalies for resilience testing
- Secrets Template: Added
devtools/secrets.py.templatefor easier development setup
- Enhanced README: Completely rewritten with detailed configuration options, examples, and troubleshooting
- Updated IMPLEMENTATION.md: Reflects new architecture with NumPy-powered matching and profile persistence
- Improved TESTING.md: Enhanced verification guide with new test scenarios
- Screenshot Assets: Added screenshots in
img/directory:integration-controls.pngintegration-diagnostics.pngintegration-profiles.pngintegration-sensors.pngintegration-settings.png
- GitHub Funding: Added
.github/FUNDING.ymlfor sponsor support
- Manifest Dependencies: Added
lovelaceandhttptoafter_dependenciesto ensure reliable card loading - NumPy Requirement: Added
numpyto requirements for advanced shape correlation matching - Service Definitions: Enhanced
services.yamlwith new export and configuration options - Profile Store Refactoring: Complete rewrite for improved type safety, compression, and NumPy-powered matching
- Manager Enhancements:
- Better state machine handling with reactive synchronization
- Improved notification system
- Enhanced progress tracking with device-type-aware smoothing
- Power sensor change protection (blocked when cycle is active)
- Cycle Detector Evolution:
- More robust state transitions
- Better handling of edge cases
- Enhanced logging for debugging
- Translation Updates: Full translation support for new wizard steps and advanced settings in both
strings.jsonandtranslations/en.json - Configuration Validation: Improved validation and error handling in config flow
- Settings Migration: Automatic migration of existing settings to new format
- Fixed Indentation Issues: Corrected inconsistent indentation throughout codebase
- Removed Trailing Whitespace: Cleaned up formatting issues
- Removed Unused Variables: Eliminated unused
managervariable fromasync_step_settings - Removed Unused Imports: Cleaned up
MagicMockandSTATE_OFFimports from test files - Improved Error Handling: Added descriptive debug logging for exception handling
- Fixed Redundant Code: Removed duplicate
device_typeassignment in cycle data - Memory Leak Prevention: Fixed event listener accumulation in dashboard card
- Performance Optimization: Implemented result caching for profile matcher to avoid redundant calls
- Deprecated Auto-Maintenance Switch: Removed standalone
auto_maintenanceswitch entity (now a backend setting)
- README Path Inconsistency: Corrected card path documentation from
/ha_washdata/card.jsto/ha_washdata/ha-washdata-card.js - Card Editor Domain Support: Added "select" entity domain to program_entity selector in dashboard card editor
- End Condition Counter: Fixed potential infinite increment issue when power stays low for extended periods
- Start Duration Threshold: Removed unconditional override in initial setup to allow user customization
- Cycle Interruption Handling: Better detection and classification of interrupted, force-stopped, and resumed cycles
- Profile Match Extension: Added confidence check (≥70%) to prevent spurious cycle extensions
- Config Flow Import: Removed duplicate import of
CONF_AUTO_MERGE_GAP_SECONDS - Power Sensor Change: Now properly blocked when a cycle is active to prevent data inconsistency
- All code changes have been validated through CodeQL security scanning
- No new vulnerabilities introduced
- Automatic Migration: Your existing settings will be migrated automatically to the new format
- Card Setup: After updating, look for the "WashData Card" in the dashboard card picker
- Select Entity: A new
select.<name>_program_selectentity will be created automatically - Deprecated Switch: The
auto_maintenanceswitch entity will be removed; this is now a backend setting
None. This release is fully backward compatible with v0.2.x configurations.
See git history for details on previous releases.