Skip to content

Releases: 3dg1luk43/ha_washdata

v0.5.4

Choose a tag to compare

@3dg1luk43 3dg1luk43 released this 17 Aug 21:26
32fd596

0.5.4 - 2026-08-17

Features

  • 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 adaptive ha-washdata-card with a layout option: 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); and glance - 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 single STATE_COLORS source, 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. Existing type: custom:ha-washdata-card dashboards are unchanged (they default to tile). 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) The min_power noise-gate suggestion is now also stripped so it cannot be dragged below the real operating draw by the a...

Read more

v0.5.3

Choose a tag to compare

@3dg1luk43 3dg1luk43 released this 26 Jul 21:25
50c0218

0.5.3 - 2026-07-26

Features

  • 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, and import_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_wrinkle was 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 five anti_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.

Performance

  • 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/abs calls 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 _runs function 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 uses np.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, when enable_phase_matching is 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 the config/site read (used to resolve the community co...

Read more

v0.5.2

Choose a tag to compare

@3dg1luk43 3dg1luk43 released this 23 Jul 09:44
08aa94e

0.5.2 - 2026-07-23

Bug Fixes

  • 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 finally block, 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 same integrate_wh path used at detection time. Trim also updates max_power from 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_wh is now set to 0 and max_power reflects 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.CancelledError was a BaseException and bypassed the except Exception handler, so task_registry.finish() was never called. The task stayed in the running state indefinitely, making its header pill spin forever even after reload. All nine task runners now explicitly catch CancelledError, mark the task cancelled, and re-raise so asyncio behaves correctly.

  • Tasks and write locks no longer leak across reload_config_entry (#323): async_unload_entry now expli...

Read more

v0.5.1

Choose a tag to compare

@3dg1luk43 3dg1luk43 released this 20 Jul 12:52
07af5e1

0.5.1 - 2026-07-20

✨ Features

  • 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_profile key 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 a power_profile attribute: 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 companion power_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_block to 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_profile attribute (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.

🐛 Bug Fixes

  • 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 invalid rgb(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(), and rgba() 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 no...

Read more

0.5.0 - Goodbye OptionsFlow, Hello Panel (Oh, and hi WashData Community Store)

Choose a tag to compare

@3dg1luk43 3dg1luk43 released this 17 Jul 08:23
e3b1cd2

0.5.0 - "Goodbye OptionsFlow, Hello Panel (Oh, and hi WashData Community Store)" - 2026-07-16

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.

✨ Features

  • 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_total reports 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 structured content_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_due list 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 ...

Read more

v0.4.5.2

Choose a tag to compare

@3dg1luk43 3dg1luk43 released this 13 Jun 10:58
aae5fb1

0.4.5.2 - 2026-06-13

🐛 Bug Fixes

  • 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.

New Contributors

v0.4.5.1

Choose a tag to compare

@3dg1luk43 3dg1luk43 released this 11 Jun 10:29
8460b86

0.4.5.1 - 2026-06-11

🐛 Bug Fixes

  • 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.

🛠 Internal / Developer Experience

  • 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 Request workflow re-runs on every pull_request_target edit/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 a concurrency group (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 accepted issue 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 labelled needs 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 the accepted label is parked under an awaiting maintainer label 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 every translations/*.json file, so the exemption now recognises strings.json as a localization asset alongside the translations/ 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 PRs workflow was renamed Close Stale PRs and now closes both needs description (5-day grace) and needs 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 the issues: write permission it needs to comment, alongside its existing pull-requests: write. New repository labels needs accepted issue and awaiting maintainer back this flow.

v0.4.5

Choose a tag to compare

@3dg1luk43 3dg1luk43 released this 03 Jun 17:13
770bf5c

0.4.5 - 2026-06-02

✨ Features

  • 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, or none. The card switched from a single click listener 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 is more-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 stable notification_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 timeout to the companion app so WashData notifications vanish automatically after the configured time (e.g. 3600 = one hour). Default 0 keeps 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.

🐛 Bug Fixes

  • 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 live alert_once flag 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 min unit through a state-dependent property that returned None while 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_notification for 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.

🎨 UI & UX

  • 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.

📖 Documentation & Templates

  • 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 re...

Read more

v0.4.4.3

Choose a tag to compare

@3dg1luk43 3dg1luk43 released this 21 May 07:46
378f02f

0.4.4.3 - 2026-05-21

✨ Features

  • 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).

⚠️ Deprecations

  • 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.

🐛 Bug Fixes

  • 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's external_end_trigger, door_sensor_entity, and switch_entity fields, and none of the changes were persisted. When the Advanced Settings form was reorganised into collapsible sections, these three EntitySelector fields were declared with default=None for unset entities; voluptuous renders the form with None filled in, and on submit the EntitySelector validator rejects None because it is neither a valid entity ID nor a UUID. Fix: switched all three fields to the description={"suggested_value": ...} pattern already used for energy_price_entity elsewhere 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.

v0.4.4.2

Choose a tag to compare

@3dg1luk43 3dg1luk43 released this 20 May 18:01
0d259aa

0.4.4.2 - 2026-05-11

✨ Features

  • Notify Entity Targets (#230): Cycle Start / Finish / Live Progress notification target dropdowns now list notify entities (e.g. those exposed by telegram_bot) alongside legacy notify.<service> services. When the picked target is a notify entity, the dispatcher routes through the universal notify.send_message action with entity_id set; legacy notify.* 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 the CONF_NOTIFY_ACTIONS script field or the ha_washdata_cycle_ended event 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:MM or HH: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.

🎨 UI & UX

  • 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 bare bool voluptuous type, which the frontend renders as a checkbox). Fifteen fields across Advanced Settings, Notifications, Settings, and the suggestion-apply confirm step were migrated to BooleanSelector so 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 Python bool.
  • 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 from init downward; 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_confidence field is persisted on the cycle dict at every site that assigns a profile (runtime match commit, post-cycle auto-label, and bulk auto_label_cycles). Cycles labeled before this release would otherwise show ; on integration startup, a background async_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-blocking async_create_task so 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 • ~135m that scaled poorly once a household had a dozen profiles. The data already existed on the list_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 the Apply Suggested Values checkbox 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 under sections.<name>.{data,data_description} in strings.json and translations/en.json so 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.

🐛 Bug Fixes

  • 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_entryasync_shutdown) didn't issue a clear at all, so restarting the integration or HA left the chronometer ticking against a frozen when timestamp 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), and async_shutdown issues 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 i...
Read more