v0.4.4 #218
3dg1luk43
announced in
Announcements
v0.4.4
#218
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
0.4.4 - 2026-04-27
✨ Features
delay_wait) during the delay period — no program time, progress, or ETA is tracked until the real cycle begins. Configurable thresholds: Drain Spike Min/Max Power (defaults 10 W / 80 W), Max Drain Spike Duration (60 s), and a Max Wait Time safety timeout (8 h, after which the state resets to Off). The feature is disabled by default and requiresstart_threshold_wto be set above the drain spike power level to function correctly.HH:MM) on the x-axis as the primary label, with the relative offset (+N min) shown below each tick as a secondary reference. The S and E trim-point markers display the exact wall-clock time (HH:MM:SS) instead of a relative offset. The Set Trim Start and Set Trim End forms have been redesigned: both now show the full cycle window (start → end), the current trim position in wall-clock time and minutes, and a single clock-picker field pre-populated with the current position — making it straightforward to set a trim point by entering a known time from another source (e.g. HA history graphs or a smart plug log) without having to calculate offsets manually.{energy_kwh}(energy consumed during the cycle, formatted to 3 decimal places) and{cost}(energy cost, formatted to 2 decimal places). To enable{cost}, configure an electricity price under Notifications → Energy Price - either a static value (e.g.0.22) or a Home Assistant entity that holds the current tariff (anysensor.*,input_number.*, ornumber.*with a numeric state). The entity takes precedence if both are set;{cost}resolves to an empty string when no price is configured. Currency is not included - add it directly in your template (e.g.{cost} €). Example:{device} finished. Duration: {duration}m | Energy: {energy_kwh} kWh | Cost: {cost} €. Energy per cycle is now also stored in the cycle history record (energy_wh) computed from the stored power trace at save time, covering all storage paths including the manual recorder.notify.*services. A family member who only needs to know when laundry is done can be added to Cycle Finish only, while another person who also wants live progress can be added to all three. Existing configurations are migrated automatically: the previous single service is mapped into each list that matches the previously selected events, so no manual reconfiguration is required after upgrade. Notification actions and presence gating continue to work across all targets.mdi:water-pump) with defaults tuned for short, high-frequency cycles: 10 s sampling interval (captures sub-30 s runs that the 30 s default misses), 20 s off-delay (pumps stop sharply with no warm-down), 5 s minimum cycle threshold (valid with 1–2 readings), 60 s minimum off-gap (handles pumps cycling every few minutes), and a 0.003 Wh start-energy gate. Includes a Pump Stuck Alert - a configurable threshold (pump_stuck_duration, default 30 min) that fires a singleha_washdata_pump_stuckHA event if a pump cycle runs longer than expected, enabling automations to alert on a jammed motor or continuous-run fault.pump_runs_todaysensor (mdi:counter) counts completed pump cycles in a rolling 24-hour window - useful for monitoring sump pump activity during heavy rain or drought periods.cycle_countsensor (mdi:counter, unit: cycles, state class:total_increasing) tracks the total number of completed cycles stored for the device. Use this in automations to schedule maintenance tasks - such as cleaning filters, refilling consumables, or checking bearing wear - based on a fixed cycle interval rather than a calendar schedule.chronometer,when, andcountdownfields pointing to the estimated cycle finish time. The Android companion app renders a live countdown timer on the notification that ticks down automatically between pushes - eliminating the "frozen at X minutes" problem without requiring more frequent server-side sends. Disabled by default; Android companion app only.async_get_config_entry_diagnosticsnow includes alive_diagnosticssnapshot from a new in-memoryDiagBuffer- a rolling 24-hour ring buffer capturing every raw power reading, every detector state transition, and the last 5000 log lines. Zero disk I/O; vanishes on HA restart.ha_washdata.pause_cycle/ha_washdata.resume_cycleHA services (both acceptdevice_id). While paused, the cycle is protected from watchdog termination (verified_pause = True) - the appliance can sit at zero or low power indefinitely without WashData closing the session. All time metrics (elapsed time, time remaining, total duration, and progress %) exclude the time spent paused, so a cycle paused for 30 minutes does not inflate the estimated total. The primary use case is energy price management: trigger a pause when tariff is high, resume when it drops (e.g. with Cheapest Energy Hours). An optional Cut Power When Pausing toggle (pause_cuts_power, off by default) instructs WashData to also callswitch.turn_off/switch.turn_onon the configured switch entity when pause or resume is triggered - useful for appliances that remember their position after a power cut. The Pause button is only available while a cycle is Running or Starting; the Resume button is only available while the cycle is user-paused.binary_sensor,on = open) can now be configured under Advanced Settings. When set, WashData uses door state in two ways. During an active cycle, opening the door automatically setsverified_pause = True- the same protection used by user-triggered pauses - confirming the interruption is intentional and guarding the cycle against watchdog termination (useful for add-clothes machines such as Samsung AddWash). Closing the door does not auto-resume the cycle - this is intentional and is stated explicitly in the field description; the user must press Resume or call the service. After a cycle ends, if the door is still closed the device enters a new Clean state (STATE_CLEAN) indicating laundry is waiting to be unloaded. The state clears as soon as the door is opened. A configurable Laundry Waiting Notification Delay (notify_unload_delay_minutes, default 60 min, set to 0 to disable) fires a single reminder via the existing finish notification channel once the door has remained closed for that many minutes after cycle end - the "your wash has been sitting there for an hour" nag. The reminder message is fully customizable via a Laundry Waiting Message template (notify_unload_message); the default reads"{device} finished {duration}m ago - laundry is still inside."and supports{device}and{duration}placeholders.🛠️ Improvements
delay_waitState Now Has a Human-Readable Label: Thedelay_waitstate was missing from the sensor state translation map instrings.json, causing the UI to display the raw key. It now renders as "Delay Start".bug_report.ymlnow reads "Pump / Sump Pump", matching thefeature_request.ymltemplate.more info requiredlabel. Issues that remain unfixed for 2 days receive a single reminder comment; issues that remain unfixed for 5 days are closed asnot_plannedwith instructions to reopen a properly filled report.store_exportat the top level, e.g. copy-pasted from a log or API response); and (3) the full HA diagnostics download file (the.jsonfile downloaded via Settings → Devices & Services → (device) → Download Diagnostics), which wraps our data in ahome_assistant/dataenvelope. When importing from a diagnostic source, any fields replaced by the redactor (**REDACTED**) are silently stripped, so sensitive fields such aspower_sensorandnotify_serviceare never overwritten with the placeholder string.devtools/analyze_diag.py): New developer tool that processes any WashData diagnostics export and prints a side-by-side table of current vs. suggested settings with a per-parameter rationale. Covers power thresholds, energy gates, timing/operational parameters, and matching tolerances - all derived from the device's own recorded cycle data without requiring a running Home Assistant instance. Surfaces suggestions already computed by live HA operation for cross-checking, and includes a cycle-history summary per programme (average duration, standard deviation, variance). Runpython3 devtools/analyze_diag.py <export.json>from the repo root with the venv activated; seedevtools/README.mdfor full usage.run_batch_simulation) rather than only the most recent one. The 5th-percentile minimum-active-power across cycles is used for the stop threshold (more robust against outlier cycles), and the 75th-percentile of early-dip timestamps is used for the running dead zone. A newprocess_cycle_endhook re-runs the batch every 5 newly labeled cycles so suggestions improve progressively as more data accumulates.min_off_gaprecommendation from observed inter-cycle gaps. The p05 gap × 0.8 is used as the proposed value, floored by the device-type default, so the suggestion is always conservative enough to avoid splitting real cycles.tests/benchmarks/parameter_optimizer.DataLoadernow handles both the newdata → store_export → data → past_cyclesexport path and the legacydata → store_data → past_cyclespath, so all user-contributed diagnostic exports incycle_data/are usable by the benchmark suite._on_cycle_endindependently suppresses any cycle starting within 10 minutes of the previous cycle end if it lasted under 5 minutes and consumed less than 1 Wh.🐛 Bug Fixes
< stop_threshold) for the low-power silence path. A 2 W reading against a 2 W threshold evaluated toFalse, so the Watchdog fell through to the high-power stale path and injected a refresh keepalive instead of a 0 W keepalive — leaving the cycle stuck inRunningstate for the entire duration of sensor silence. On one reported case this produced a 136-minute close delay instead of the expected ~6 minutes. The low-power branch condition is now<=(and the high-power branch is>) so a standby value exactly equal to the stop threshold is correctly routed to the 0 W keepalive path that advances the cycle-closure accumulator.0:00countdown with no further updates. The live notification sender now detects this condition — remaining time at or below zero, chronometer active, cap exhausted — and sends one additional plain-text update that replaces the stale countdown timer. This overrun update does not increment the sent-count, so a cap of N notifications remains N notifications for the purpose of normal cycle progress; the overrun is a single one-shot correction applied only once per cycle.availableproperty was evaluated only once at HA startup (when no cycle is active →False) and never refreshed. Both buttons now subscribe toSIGNAL_WASHER_UPDATEviaasync_added_to_hassand callasync_write_ha_state()on each manager update, making them correctly enable when a cycle is running and disable when it ends.ha_washdata_cycle_started) already fired immediately - only the push notification vianotify.*services was delayed. The notification is now dispatched at the moment the appliance transitions toRUNNING, regardless of whether a program has been identified yet. Users who included{program}in their start message template will now see "detecting..." until a match is confirmed; the default message has been updated to"{device} started."(without{program}) to avoid the awkward placeholder. A restart-recovery fallback remains in the profile-matching loop in case the notification was missed due to an HA restart before the first cycle snapshot was saved.14400 s) device-specific silence floor - intended to cover dishwasher passive open-door drying - was only applied when a profile had already been matched. An unmatched cycle (program == "detecting...") fell back to the 1-hour (3600 s) base timeout. Because dishwasher ECO programs use very little power and can go completely silent for 1–2 hours while drying, the watchdog killed the cycle roughly 1 hour after the last sensor update - while the appliance was still running. The floor is now applied unconditionally regardless of match state.CycleDetectoritself held the cycle inENDINGstate untilmin_off_gap(up to 9000 s = 2.5 h) had elapsed after the pump-out spike. For an unmatched cycle that has already seen a terminal end spike, the full gap is unnecessary. Theeffective_off_delayis now capped at 1800 s (30 min) in this case, so the cycle closes cleanly ~30 minutes after the pump-out rather than sitting open for hours._should_defer_finishconfidence gate of 0.55. A terminal drain-pump spike in earlyENDINGthen reset_time_below_threshold, and the subsequent 60-minute silence caused_should_defer_finishto returnFalseand the cycle to be terminated at ~180 min, storing only 120 min of duration in the profile (the_last_active_timeof the drain spike, not the actual finish). The bug only affected devices with fewer confirmed cycles; the old instance with 7 cycles was unaffected because its confidence was high enough to trigger smart termination at 99% of expected duration instead._should_defer_finishnow includes a dishwasher-specific drying guard: if the device is a dishwasher, a profile has been matched, and the current duration is below 85% of the expected duration, deferral is forced regardless of confidence score. This bridges the gap between the first profile match at ~50% duration and the smart-termination window at ~85–99%, preventing the 60-minute silence timeout from firing mid-drying-phase._finish_cyclewas called withkeep_tail=False(the default for all devices),end_timewas set to_last_active_time— the timestamp of the terminal drain-pump spike at ~120 min rather than the actual timeout time at ~180 min. This corrupted the profile'savg_duration(average dropped from 233 min to 177 min). The fallback-timeout path in theENDINGstate now useskeep_tail=Truefor dishwashers, storing the timeout timestamp asend_timeso the recorded duration reflects the true elapsed time even when deferral ultimately expires.suggested_settingssensor populated with stale values. The wipe operation now also clears suggestions, envelopes, feedback history, pending feedback, and auto-adjustment logs - all data derived from the cycle history that was just deleted.avg_durationstatistic was not always updated. If the cycle had no power data (e.g. imported or stripped by retention),async_rebuild_envelopeskipped the stats update entirely and returned early, leaving the profile showing the old wrong duration. The stats update (min/max/avg) is now computed directly from the stored cycle durations even when the envelope shape cannot be built. Additionally, if no target profile can be determined (both the form field and the pending feedback's detected profile are absent), the duration correction is now applied directly to the cycle using its existing profile, and a warning is logged - previously it was silently dropped._last_match_result- which carries the matched phase label - was passed to the learning manager at cycle end but never cleared in the state teardown block. The sensor'sphase_descriptionproperty reads_last_match_result.matched_phasefirst, so it remained stuck on the last phase until the next periodic_update_estimatescall (up to 5 minutes later)._last_match_resultis now cleared in the same teardown block as the other cycle state, immediately after the learning call that needs it.no_update_active_timeout) only applied to high-power states. For low-power silence the watchdog used a separate 3 600 s (low_power_no_update_timeout) ceiling, so a user who set "No-Update Timeout" to 140 s had no effect at all on how quickly the cycle closed after the appliance reached standby. The low-power watchdog now also honoursno_update_active_timeout: once real-update silence exceeds this threshold (and no verified pause is active), a 0 W keepalive is injected on every watchdog tick to advance the off-delay accumulator. With the default 116 s watchdog interval, cycles now close within ~6 minutes of going to standby. Verified pauses (e.g. dishwasher drying confirmed by envelope alignment) continue to be exempted so legitimate long silent phases are unaffected.expected_duration = 0.0: Profiles created beforeavg_durationtracking was introduced (or imported without duration data) storedavg_duration = 0. The snapshot-building loop passed this zero to the analysis worker, which propagated it as the match result'sexpected_duration. Every match attempt logged a"update_match: invalid raw_expected_duration"warning and the time-remaining estimate stayed at zero throughout the cycle. The snapshot builder now falls back throughprofile.avg_duration → cycle.duration → len(segment) × dtso a zero value is always replaced with a computed estimate.update_matchlog always said(> 6h)for zero durations: The validation branch that rejectedexpected_duration <= 0shared a single log message with the> 6hbranch, making it impossible to tell from logs whether the rejection was a zero/negative value or an implausibly large one. The two conditions are now logged separately as(<= 0)and(> 6h)._dispatch_notificationnow deduplicates the resolved service set before sending, so eachnotify.*service receives at most one call per event regardless of how many event-type lists it appears in.notify.*service from one of the multi-target lists and saving options could leave the old entry in the in-memory runtime set, continuing to fire notifications to the removed service until the next HA restart. The options-flow reload path now fully rebuilds_notify_start_services,_notify_finish_services, and_notify_live_servicesfrom the saved config on every options save, ensuring removed services take effect immediately.if not options.get(...)) so an explicit empty list[](user deliberately cleared a service list) was treated the same as a missing key and overwritten with the migrated service. Checks are now key-presence tests so an explicit[]is preserved.event_typefor all event types — a secondcycle_finishwhile the user was out silently dropped the first. OnlyNOTIFY_EVENT_LIVEentries are now replaced; all other events accumulate so multiple completions are all delivered on return.NOTIFY_EVENT_FINISHeven when only start services were configured, resulting in no notification being sent. The event type is now chosen based on which services are actually configured: finish services take priority, then start services.async_pause_cycleandasync_resume_cycledid not persist the updated pause state immediately — it was only written by the periodic 60-second save or on clean shutdown. Both methods now fire-and-forgetasync_save_active_cycleafter committing state, matching the pattern used by the periodic save.pump_stuck_duration: Saving from the basic settings page without opening advanced settings did not run the cleanup that dropsCONF_PUMP_STUCK_DURATIONwhen the device type is not pump. The cleanup now also runs in the basic settings save path._wallclock_to_offsetonly rejected times earlier than the cycle start (rolling them to the next day), but did not check the upper bound. A time past the actual cycle end produced a valid-looking offset beyond the stored power data. The function now acceptscycle_end_dtand returnsNonefor any offset outside[0, cycle_duration].corrected_profile), the correction path fell back to the detected profile and called_apply_correction_learningagainst it as if the user had confirmed it, potentially shifting that profile's weights incorrectly. The fallback is removed; duration-only corrections are now handled exclusively by the direct cycle-update branch._last_batch_simulation_countwas only updated inside_async_run_batch_simulationon completion. Rapid cycle-ends before the first simulation finished saw the unchanged count and scheduled duplicate jobs. The count is now set immediately before the task is created.async_import_datareplacedself._databut did not clear_cached_sample_segments, leaving stale cache entries from the old dataset that could produce incorrect alignment results until the next HA restart. The cache is now cleared immediately after assigning the new data.DEFAULT_NO_UPDATE_ACTIVE_TIMEOUT_BY_DEVICEforDEVICE_TYPE_PUMPwas600 s(10 min), shorter thanDEFAULT_PUMP_STUCK_DURATION(1800 s / 30 min). The watchdog terminated running pump cycles before theha_washdata_pump_stuckevent could fire. The timeout is nowDEFAULT_PUMP_STUCK_DURATION + 60 sso the stuck alarm always fires first.WasherCycleCountSensordeclaredstate_class=SensorStateClass.TOTAL_INCREASING, butlen(past_cycles)can decrease when cycles are deleted. HA interpreted drops as meter resets. Thestate_classhas been removed.close_incomplete_issuesworkflow calledc.body.includes(...)without guarding against null bodies (GitHub API can returnnullfor deleted or empty comments), causing workflow runs to fail. Both predicates now checkc.body != nullfirst.New Contributors
This discussion was created from the release v0.4.4.
Beta Was this translation helpful? Give feedback.
All reactions