You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
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_entry → async_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 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 of delay_wait into STARTING additionally requires the high-power streak to span at least start_duration_threshold real 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-visible starting → off sequence. 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=True via 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_finish uses 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.
📖 Documentation
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/A followed 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 accepted N/A as a valid response, but the previous reminder text only said "write N/A followed 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 bare N/A and 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, and ha_washdata_pump_stuck) with the full payload schema for each, including the cycle_data subdict shape: id, duration, max_power, energy_wh, status, termination_reason, profile_name, sampling_interval, device_type, and the signature feature vector. Also documents the Fire Events toggle that gates them globally and the fact that power_data / debug_data / power_trace are stripped from cycle_data before 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 a platform: event automation at.
🛠 Internal / Developer Experience
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 the done label, and inserts a hidden marker (<!-- release-reference-bot -->) into the comment body. close_done_issues.yml was 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 a workflow_dispatch input to re-scan a specific tag, and de-duplicates against its own previous comments (matching the tag:<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.py alone 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 from cycle_data/, stress simulations, full HA integration flow — 8 files / 144 tests) and benchmark (pure performance characterization — 2 tests). pytest.ini skips both categories by default, so ./run_tests.sh (and raw pytest tests/) now run only the 271-test fast suite in about 30 seconds (a 23× speedup). The runner also accepts --slow, --bench, and --all to opt in. No tests were deleted — the full suite still runs via ./run_tests.sh --all for releases and CI. See TESTING.md → Test Categories for the marker conventions when adding new tests.