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 - 2026-04-27
✨ Features
Delayed Start Detection (#193): When enabled under Advanced Settings, WashData now detects machines programmed with a delayed start. A brief low-power drain spike (e.g. the initial drain-check a washing machine performs before sleeping) followed by sustained standby power is recognised as the machine waiting to begin its cycle. The state sensor transitions to "Waiting to Start" (delay_wait) during the delay period — no program time, progress, or ETA is tracked until the real cycle begins. Configurable thresholds: Drain Spike Min/Max Power (defaults 10 W / 80 W), Max Drain Spike Duration (60 s), and a Max Wait Time safety timeout (8 h, after which the state resets to Off). The feature is disabled by default and requires start_threshold_w to be set above the drain spike power level to function correctly.
Wall-Clock Time in Trim Cycle Editor (#212): The Trim Cycle graph now shows real wall-clock times (HH:MM) on the x-axis as the primary label, with the relative offset (+N min) shown below each tick as a secondary reference. The S and E trim-point markers display the exact wall-clock time (HH:MM:SS) instead of a relative offset. The Set Trim Start and Set Trim End forms have been redesigned: both now show the full cycle window (start → end), the current trim position in wall-clock time and minutes, and a single clock-picker field pre-populated with the current position — making it straightforward to set a trim point by entering a known time from another source (e.g. HA history graphs or a smart plug log) without having to calculate offsets manually.
Energy & Cost Placeholders in Finish Notifications (#196): The finish message template now supports two new placeholders: {energy_kwh} (energy consumed during the cycle, formatted to 3 decimal places) and {cost} (energy cost, formatted to 2 decimal places). To enable {cost}, configure an electricity price under Notifications → Energy Price - either a static value (e.g. 0.22) or a Home Assistant entity that holds the current tariff (any sensor.*, input_number.*, or number.* with a numeric state). The entity takes precedence if both are set; {cost} resolves to an empty string when no price is configured. Currency is not included - add it directly in your template (e.g. {cost} €). Example: {device} finished. Duration: {duration}m | Energy: {energy_kwh} kWh | Cost: {cost} €. Energy per cycle is now also stored in the cycle history record (energy_wh) computed from the stored power trace at save time, covering all storage paths including the manual recorder.
Multi-Target Notifications (#203): The notification system now supports multiple recipients per event type. The single Notification Service dropdown and Events to Notify checkbox have been replaced with three independent multi-select fields - Cycle Start, Cycle Finish, and Live Progress - each accepting any number of notify.* services. A family member who only needs to know when laundry is done can be added to Cycle Finish only, while another person who also wants live progress can be added to all three. Existing configurations are migrated automatically: the previous single service is mapped into each list that matches the previously selected events, so no manual reconfiguration is required after upgrade. Notification actions and presence gating continue to work across all targets.
New Device Type: Bread Maker (#190): Added full support for bread makers with optimised defaults (2-hour no-update timeout for long proving periods, 5-minute off-delay for keep-warm, 30-minute completion threshold) and five built-in phases: Kneading, Resting, Proving, Baking, and Keep Warm.
New Device Type: Pump / Sump Pump (#195): Added a dedicated pump device type (mdi:water-pump) with defaults tuned for short, high-frequency cycles: 10 s sampling interval (captures sub-30 s runs that the 30 s default misses), 20 s off-delay (pumps stop sharply with no warm-down), 5 s minimum cycle threshold (valid with 1–2 readings), 60 s minimum off-gap (handles pumps cycling every few minutes), and a 0.003 Wh start-energy gate. Includes a Pump Stuck Alert - a configurable threshold (pump_stuck_duration, default 30 min) that fires a single ha_washdata_pump_stuck HA event if a pump cycle runs longer than expected, enabling automations to alert on a jammed motor or continuous-run fault.
Pump Runs (Last 24 h) Sensor (#195): When using the Pump device type, a new pump_runs_today sensor (mdi:counter) counts completed pump cycles in a rolling 24-hour window - useful for monitoring sump pump activity during heavy rain or drought periods.
Cycle Counter Sensor (#210): A new cycle_count sensor (mdi:counter, unit: cycles, state class: total_increasing) tracks the total number of completed cycles stored for the device. Use this in automations to schedule maintenance tasks - such as cleaning filters, refilling consumables, or checking bearing wear - based on a fixed cycle interval rather than a calendar schedule.
Chronometer Countdown Notifications (#194): Added an optional Chronometer Countdown Timer toggle to the Notifications settings. When enabled, each live progress notification push includes chronometer, when, and countdown fields pointing to the estimated cycle finish time. The Android companion app renders a live countdown timer on the notification that ticks down automatically between pushes - eliminating the "frozen at X minutes" problem without requiring more frequent server-side sends. Disabled by default; Android companion app only.
Live Diagnostics in HA Diagnostics Export: async_get_config_entry_diagnostics now includes a live_diagnostics snapshot from a new in-memory DiagBuffer - a rolling 24-hour ring buffer capturing every raw power reading, every detector state transition, and the last 5000 log lines. Zero disk I/O; vanishes on HA restart.
User-Triggered Pause (#146): Cycles can now be paused and resumed explicitly via a Pause Cycle button entity, a Resume Cycle button entity, or the new ha_washdata.pause_cycle / ha_washdata.resume_cycle HA services (both accept device_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 call switch.turn_off / switch.turn_on on the configured switch entity when pause or resume is triggered - useful for appliances that remember their position after a power cut. The Pause button is only available while a cycle is Running or Starting; the Resume button is only available while the cycle is user-paused.
Door Sensor & Clean State (#153): An optional Door Sensor field (any binary_sensor, on = open) can now be configured under Advanced Settings. When set, WashData uses door state in two ways. During an active cycle, opening the door automatically sets verified_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_wait State Now Has a Human-Readable Label: The delay_wait state was missing from the sensor state translation map in strings.json, causing the UI to display the raw key. It now renders as "Delay Start".
Bug Report Template: Pump Label Aligned: The device type dropdown in bug_report.yml now reads "Pump / Sump Pump", matching the feature_request.yml template.
Issue Template: Outdated Version Check: The automated issue validator now fetches the latest stable and pre-release versions from the GitHub Releases API and flags any bug report submitted against an older version. The reporter is asked to confirm the bug still exists on the latest release before the issue is investigated. The version field description now links directly to the Releases page.
Issue Template: Device Type Sync: Added Bread Maker and Pump / Sump Pump to the Feature Request template's device type dropdown — these device types were supported since v0.4.4 but were missing from that template.
Issue Automation: Auto-Assign: All newly opened and reopened issues are now automatically assigned to the maintainer, so every report lands in the triage queue without a manual step.
Issue Automation: Auto-Close Incomplete Reports: A daily workflow scans open issues still carrying the more info required label. Issues that remain unfixed for 2 days receive a single reminder comment; issues that remain unfixed for 5 days are closed as not_planned with instructions to reopen a properly filled report.
Import Accepts HA Diagnostic Export Format: The import feature (Configure → Diagnostic Settings → Import) now accepts three JSON formats without any manual preprocessing: (1) the regular WashData export; (2) the raw diagnostics dict produced by WashData (with store_export at the top level, e.g. copy-pasted from a log or API response); and (3) the full HA diagnostics download file (the .json file downloaded via Settings → Devices & Services → (device) → Download Diagnostics), which wraps our data in a home_assistant / data envelope. When importing from a diagnostic source, any fields replaced by the redactor (**REDACTED**) are silently stripped, so sensitive fields such as power_sensor and notify_service are never overwritten with the placeholder string.
Offline Diagnostic Analyser (devtools/analyze_diag.py): New developer tool that processes any WashData diagnostics export and prints a side-by-side table of current vs. suggested settings with a per-parameter rationale. Covers power thresholds, energy gates, timing/operational parameters, and matching tolerances - all derived from the device's own recorded cycle data without requiring a running Home Assistant instance. Surfaces suggestions already computed by live HA operation for cross-checking, and includes a cycle-history summary per programme (average duration, standard deviation, variance). Run python3 devtools/analyze_diag.py <export.json> from the repo root with the venv activated; see devtools/README.md for full usage.
Batch Multi-Cycle Simulation (suggestion engine): The suggestion engine now derives power thresholds, dead-zone, and end-energy gate from the aggregate of all labeled cycles (run_batch_simulation) rather than only the most recent one. The 5th-percentile minimum-active-power across cycles is used for the stop threshold (more robust against outlier cycles), and the 75th-percentile of early-dip timestamps is used for the running dead zone. A new process_cycle_end hook re-runs the batch every 5 newly labeled cycles so suggestions improve progressively as more data accumulates.
Min-Off-Gap Suggestion: The suggestion engine now derives a min_off_gap recommendation from observed inter-cycle gaps. The p05 gap × 0.8 is used as the proposed value, floored by the device-type default, so the suggestion is always conservative enough to avoid splitting real cycles.
DataLoader New Export Format Support (benchmark tooling): tests/benchmarks/parameter_optimizer.DataLoader now handles both the new data → store_export → data → past_cycles export path and the legacy data → store_data → past_cycles path, so all user-contributed diagnostic exports in cycle_data/ are usable by the benchmark suite.
Dishwasher Post-Cycle Ghost Suppression (#43): The ghost suppressor now uses a 10-minute suspicious window for dishwashers (vs. 3 minutes for other devices) to catch drain pump-outs that fire 3–8 minutes after the main cycle ends. Within that window, a pump-out is suppressed faster: 3 minutes elapsed + 1 minute of silence, instead of the standard 10-minute/5-minute thresholds. A secondary fallback guard in _on_cycle_end independently suppresses any cycle starting within 10 minutes of the previous cycle end if it lasted under 5 minutes and consumed less than 1 Wh.
Confidence Improves With Confirmed Cycles (#199): When a profile has ≥2 confirmed labeled cycles, the envelope's averaged power curve is now used as the matching reference instead of the original single sample cycle. Because the envelope represents the average of all user-confirmed runs, repeated correct detections now produce progressively higher (and more stable) confidence scores rather than staying frozen at the first sample's similarity level.
Suppress Feedback Notifications (#199): Added a new Suppress Feedback Notifications toggle under Advanced Settings. When enabled, WashData still tracks pending feedback and records confirmations internally, but stops creating persistent "Verify Cycle" notifications in the HA sidebar - useful for devices like dishwashers that are always detected correctly and need no ongoing prompting.
Feedback Threshold Settings Now Exposed in UI (#199): Auto-Label Confidence and Feedback Request Confidence are now available as editable fields under Advanced Settings. Previously these keys existed internally but could only be changed by directly editing config entries. Lowering Auto-Label Confidence below the observed match score allows consistent detections to be auto-labeled without any user prompt.
Profile Granularity Guide (#201): The Create New Profile form now shows inline guidance explaining what the matcher uses (power shape, duration, total energy) and what to expect from different levels of profile detail - including which program combinations are reliably auto-detected (e.g. wash-only vs wash+dry), which may need manual confirmation at first (temperature-only variants of the same programme), and how to approach a complex machine like a washer-dryer combo. The README also has a new "Profile Granularity" section (§ 3 of the Getting Started guide) with the same information in a more detailed, tabular form.
🐛 Bug Fixes
Watchdog Boundary Condition: Standby Power Equal to Stop Threshold Not Recognised as Low Power (#197 regression): When an appliance's standby power draw exactly equals the configured Stop Threshold (e.g. both at 2 W), the Watchdog's power classification used a strict less-than comparison (< stop_threshold) for the low-power silence path. A 2 W reading against a 2 W threshold evaluated to False, 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 in Running state for the entire duration of sensor silence. On one reported case this produced a 136-minute close delay instead of the expected ~6 minutes. The low-power branch condition is now <= (and the high-power branch is >) so a standby value exactly equal to the stop threshold is correctly routed to the 0 W keepalive path that advances the cycle-closure accumulator.
Chronometer Countdown Frozen at 0:00 (#194 follow-up): When the Chronometer Countdown Timer is enabled and the live progress notification cap is reached before the cycle ends, the Android companion app was left displaying a frozen 0:00 countdown with no further updates. The live notification sender now detects this condition — remaining time at or below zero, chronometer active, cap exhausted — and sends one additional plain-text update that replaces the stale countdown timer. This overrun update does not increment the sent-count, so a cap of N notifications remains N notifications for the purpose of normal cycle progress; the overrun is a single one-shot correction applied only once per cycle.
Pause/Resume Buttons Always Shown as Unavailable: The Pause Cycle and Resume Cycle button entities never registered state-change callbacks, so their available property was evaluated only once at HA startup (when no cycle is active → False) and never refreshed. Both buttons now subscribe to SIGNAL_WASHER_UPDATE via async_added_to_hass and call async_write_ha_state() on each manager update, making them correctly enable when a cycle is running and disable when it ends.
Start Notification Fired Up to 36 Minutes Late (#206): The cycle-start push notification was gated on profile detection completing, meaning it did not fire until 3 consecutive profile matches had been confirmed at 5-minute intervals (≥15 minutes minimum, often 30–36 minutes for dishwashers due to their 10% minimum-duration ratio before the first match attempt). The HA bus event (ha_washdata_cycle_started) already fired immediately - only the push notification via notify.* services was delayed. The notification is now dispatched at the moment the appliance transitions to RUNNING, regardless of whether a program has been identified yet. Users who included {program} in their start message template will now see "detecting..." until a match is confirmed; the default message has been updated to "{device} started." (without {program}) to avoid the awkward placeholder. A restart-recovery fallback remains in the profile-matching loop in case the notification was missed due to an HA restart before the first cycle snapshot was saved.
Dishwasher ECO Cycle Killed During Passive Drying (#43): Four separate bugs caused dishwasher cycles to be terminated hours before the physical run finished, leaving a 60–90 minute gap of missing data and triggering spurious pump-out ghost cycles.
Bug A - Watchdog force-ended unmatched cycles too early: The 4-hour (14400 s) device-specific silence floor - intended to cover dishwasher passive open-door drying - was only applied when a profile had already been matched. An unmatched cycle (program == "detecting...") fell back to the 1-hour (3600 s) base timeout. Because dishwasher ECO programs use very little power and can go completely silent for 1–2 hours while drying, the watchdog killed the cycle roughly 1 hour after the last sensor update - while the appliance was still running. The floor is now applied unconditionally regardless of match state.
Bug B - Cycle detector held ENDING state open for hours after pump-out: After the watchdog was fixed, the CycleDetector itself held the cycle in ENDING state until min_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. The effective_off_delay is now capped at 1800 s (30 min) in this case, so the cycle closes cleanly ~30 minutes after the pump-out rather than sitting open for hours.
Bug C - Passive drying phase ended prematurely when profile confidence was low: For a newly added dishwasher with only 1–2 recorded cycles, profile match confidence at ~120 min into an ECO cycle (the point when the first match attempt fires, after the 50% duration fast-reject clears) is typically 0.30–0.45 — below the _should_defer_finish confidence gate of 0.55. A terminal drain-pump spike in early ENDING then reset _time_below_threshold, and the subsequent 60-minute silence caused _should_defer_finish to return False and the cycle to be terminated at ~180 min, storing only 120 min of duration in the profile (the _last_active_time of 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_finish now includes a dishwasher-specific drying guard: if the device is a dishwasher, a profile has been matched, and the current duration is below 85% of the expected duration, deferral is forced regardless of confidence score. This bridges the gap between the first profile match at ~50% duration and the smart-termination window at ~85–99%, preventing the 60-minute silence timeout from firing mid-drying-phase.
Bug D - Stored cycle duration snapped to terminal drain spike timestamp: When the timeout eventually fired and _finish_cycle was called with keep_tail=False (the default for all devices), end_time was 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's avg_duration (average dropped from 233 min to 177 min). The fallback-timeout path in the ENDING state now uses keep_tail=True for dishwashers, storing the timeout timestamp as end_time so the recorded duration reflects the true elapsed time even when deferral ultimately expires.
Wipe ALL Data Now Clears Suggested Settings (#185): "Wipe ALL data for this device" previously left the suggested_settings sensor populated with stale values. The wipe operation now also clears suggestions, envelopes, feedback history, pending feedback, and auto-adjustment logs - all data derived from the cycle history that was just deleted.
Applied Suggestions No Longer Reappear (#185): After a user applied suggested settings, the sensor count stayed at the old value and the suggestions reappeared immediately on the next cycle because the engine re-computes them from the same data. Suggestions are now cleared from the store when the user saves the options form after confirming them. Additionally, the suggestion engine now skips generating a suggestion for any key whose recommended value already matches the current config option, so accepted suggestions stay silent until the underlying data actually shifts.
Corrected Cycle Duration Not Saved (#155): When a user selected the "Correct" action in the learning feedback dialog and changed the cycle duration, the correction was applied but the profile's avg_duration statistic was not always updated. If the cycle had no power data (e.g. imported or stripped by retention), async_rebuild_envelope skipped the stats update entirely and returned early, leaving the profile showing the old wrong duration. The stats update (min/max/avg) is now computed directly from the stored cycle durations even when the envelope shape cannot be built. Additionally, if no target profile can be determined (both the form field and the pending feedback's detected profile are absent), the duration correction is now applied directly to the cycle using its existing profile, and a warning is logged - previously it was silently dropped.
Current Phase Stays On Post-Cycle (#192): The "Current Phase" sensor kept showing the last detected phase (e.g. "Spin") after a cycle ended instead of resetting to "Off". _last_match_result - which carries the matched phase label - was passed to the learning manager at cycle end but never cleared in the state teardown block. The sensor's phase_description property reads _last_match_result.matched_phase first, so it remained stuck on the last phase until the next periodic _update_estimates call (up to 5 minutes later). _last_match_result is now cleared in the same teardown block as the other cycle state, immediately after the learning call that needs it.
Publish-on-Change Sensor Cycle Close Delay (#197): Three related bugs caused cycles to remain open for up to 58 minutes after an appliance physically finished when the power sensor only publishes on value change (e.g. LightwaveRF). Once the appliance stabilised at a standby value below the stop threshold, the sensor went completely silent and the cycle could not close promptly.
Bug A - No-Update Timeout ignored for low-power silence: The user-configurable No-Update Timeout (no_update_active_timeout) only applied to high-power states. For low-power silence the watchdog used a separate 3 600 s (low_power_no_update_timeout) ceiling, so a user who set "No-Update Timeout" to 140 s had no effect at all on how quickly the cycle closed after the appliance reached standby. The low-power watchdog now also honours no_update_active_timeout: once real-update silence exceeds this threshold (and no verified pause is active), a 0 W keepalive is injected on every watchdog tick to advance the off-delay accumulator. With the default 116 s watchdog interval, cycles now close within ~6 minutes of going to standby. Verified pauses (e.g. dishwasher drying confirmed by envelope alignment) continue to be exempted so legitimate long silent phases are unaffected.
Bug B - Profile match always returned expected_duration = 0.0: Profiles created before avg_duration tracking was introduced (or imported without duration data) stored avg_duration = 0. The snapshot-building loop passed this zero to the analysis worker, which propagated it as the match result's expected_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 through profile.avg_duration → cycle.duration → len(segment) × dt so a zero value is always replaced with a computed estimate.
Bug C - update_match log always said (> 6h) for zero durations: The validation branch that rejected expected_duration <= 0 shared a single log message with the > 6h branch, 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).
Duplicate Notifications Suppressed: When multiple notification targets were configured, WashData could dispatch the same push notification more than once for a single event if a service appeared in more than one list. _dispatch_notification now deduplicates the resolved service set before sending, so each notify.* service receives at most one call per event regardless of how many event-type lists it appears in.
Stale Notification Services Cleared on Save: Removing a notify.* service from one of the multi-target lists and saving options could leave the old entry in the in-memory runtime set, continuing to fire notifications to the removed service until the next HA restart. The options-flow reload path now fully rebuilds _notify_start_services, _notify_finish_services, and _notify_live_services from the saved config on every options save, ensuring removed services take effect immediately.
Migration Overwrites User-Cleared Notification Lists: The 0.3.x → multi-target migration used truthiness checks (if not options.get(...)) so an explicit empty list [] (user deliberately cleared a service list) was treated the same as a missing key and overwritten with the migrated service. Checks are now key-presence tests so an explicit [] is preserved.
Notification Collapse Drops Finish Events While Away: When "notify only when home" deferral was active, the pending queue deduped on event_type for all event types — a second cycle_finish while the user was out silently dropped the first. Only NOTIFY_EVENT_LIVE entries are now replaced; all other events accumulate so multiple completions are all delivered on return.
Auto-Tune Notification Uses Wrong Channel: The ghost-cycle auto-tune notification was always dispatched with NOTIFY_EVENT_FINISH even when only start services were configured, resulting in no notification being sent. The event type is now chosen based on which services are actually configured: finish services take priority, then start services.
Pause/Resume State Lost on Crash: async_pause_cycle and async_resume_cycle did 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-forget async_save_active_cycle after committing state, matching the pattern used by the periodic save.
Basic Settings Save Retains Stale pump_stuck_duration: Saving from the basic settings page without opening advanced settings did not run the cleanup that drops CONF_PUMP_STUCK_DURATION when the device type is not pump. The cleanup now also runs in the basic settings save path.
Trim Cycle: Wall-Clock Time Not Validated Against Cycle End: _wallclock_to_offset only 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 accepts cycle_end_dt and returns None for any offset outside [0, cycle_duration].
Correction Feedback Applies Learning to Detected Profile Incorrectly: When a user submitted a duration-only correction (no corrected_profile), the correction path fell back to the detected profile and called _apply_correction_learning against it as if the user had confirmed it, potentially shifting that profile's weights incorrectly. The fallback is removed; duration-only corrections are now handled exclusively by the direct cycle-update branch.
Batch Simulation Scheduled Multiple Times Before Completing: _last_batch_simulation_count was only updated inside _async_run_batch_simulation on completion. Rapid cycle-ends before the first simulation finished saw the unchanged count and scheduled duplicate jobs. The count is now set immediately before the task is created.
Import Does Not Clear Resample Cache: async_import_data replaced self._data but did not clear _cached_sample_segments, leaving stale cache entries from the old dataset that could produce incorrect alignment results until the next HA restart. The cache is now cleared immediately after assigning the new data.
Pump Watchdog Fires Before Stuck-Pump Alarm: DEFAULT_NO_UPDATE_ACTIVE_TIMEOUT_BY_DEVICE for DEVICE_TYPE_PUMP was 600 s (10 min), shorter than DEFAULT_PUMP_STUCK_DURATION (1800 s / 30 min). The watchdog terminated running pump cycles before the ha_washdata_pump_stuck event could fire. The timeout is now DEFAULT_PUMP_STUCK_DURATION + 60 s so the stuck alarm always fires first.
Cycle Count Sensor Uses Wrong State Class: WasherCycleCountSensor declared state_class=SensorStateClass.TOTAL_INCREASING, but len(past_cycles) can decrease when cycles are deleted. HA interpreted drops as meter resets. The state_class has been removed.
Workflow: Null Comment Body Causes TypeError: The close_incomplete_issues workflow called c.body.includes(...) without guarding against null bodies (GitHub API can return null for deleted or empty comments), causing workflow runs to fail. Both predicates now check c.body != null first.