Skip to content

[FIX] Inject watchdog keepalive during anti-wrinkle silence - #340

Open
sharkyy wants to merge 2 commits into
3dg1luk43:0.5.4from
sharkyy:fix/anti-wrinkle-watchdog-keepalive
Open

[FIX] Inject watchdog keepalive during anti-wrinkle silence#340
sharkyy wants to merge 2 commits into
3dg1luk43:0.5.4from
sharkyy:fix/anti-wrinkle-watchdog-keepalive

Conversation

@sharkyy

@sharkyy sharkyy commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Linked Accepted Issue (required for non-translation PRs)

Closes #339

Description

The detector enters anti_wrinkle at the end of a dryer cycle and never leaves it when the power sensor is publish-on-change (Shelly/Zigbee/etc.). On a recorded run the state entered anti_wrinkle at 16:29 and was still anti_wrinkle at 18:18 — long after the dryer physically finished.

The anti-wrinkle idle-timeout (anti_wrinkle_idle_timeout) and the 2-hour safety cap both live inside the detector and only advance from within process_reading, so they need incoming readings. A publish-on-change sensor goes silent once power flatlines at standby / 0 W after the last tumble pulse, so the idle timer freezes mid-count.

Nothing was advancing the timer during that silence. As pointed out in review, the watchdog cannot help: it is stopped for the whole anti-wrinkle tail — anti-wrinkle is entered via _finish_cycle, whose on_cycle_end callback calls _stop_watchdog(), and it is only restarted on the next cycle start. The state-expiry timer, however, keeps ticking through the tail (it is armed by the cycle-end tail via _start_state_expiry_timer, and _cycle_completed_time is set), but it explicitly returned early for anti_wrinkle.

This PR drives the keepalive from the state-expiry timer instead. Once the sensor has been silent longer than off_delay it injects a 0 W keepalive, letting the detector's own idle-timeout (or the 2-hour safety cap) close the tail into off.

Two important details:

  • Real-silence gating. The silence is measured against _last_real_reading_time (only genuine sensor readings bump it), not _last_reading_time which the keepalive itself bumps. Because off_delay equals the 60 s state-expiry interval, gating on the self-bumped clock would make the condition true only every other tick, so the idle timer would advance at half real-time and take ~2× as long to close. Real-silence gating fires a keepalive every tick during genuine silence, so the tail closes right at anti_wrinkle_idle_timeout after the last tumble pulse.
  • Throttle bypass. The keepalive bumps _last_reading_time, which is also the sampling-throttle clock in _async_power_changed. A real tumble pulse (>= min_power, therefore not is_low_power) arriving within one sampling interval of a keepalive would otherwise be discarded by that throttle before reaching the detector and could not reset the idle timer, so high-power readings are exempted from the throttle while in anti_wrinkle. Sub-min_power baseline readings stay throttled as before.

Validated live on a real dryer with a publish-on-change plug: the tail now closes into off on schedule instead of hanging.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔄 Refactor (code reorganization with no behavior change)
  • 📚 Documentation update
  • 🧪 Test additions/improvements
  • ⚡ Performance improvement
  • 🎨 UI/UX improvement
  • 🌍 Translation/Localization

Changes Made

  • manager.py _handle_state_expiry: inject a 0 W keepalive during anti_wrinkle on real sensor silence (> off_delay), gated on _last_real_reading_time, so the detector's idle-timeout / 2 h safety closes the tail into off
  • manager.py _async_power_changed: real anti-wrinkle tumble pulses (>= min_power) bypass the sampling throttle so a recent keepalive cannot suppress them
  • manager.py _watchdog_check_stuck_cycle: reverted to its original form (the previous, non-working watchdog branch removed)
  • tests/test_anti_wrinkle_silent_close.py: new regression tests
  • CHANGELOG.md: entry under the existing 0.5.4 section

Testing

  • Unit tests added/updated
  • Ran: ./run_tests.sh (via pytest tests/)

New tests: _handle_state_expiry injects a keepalive during anti-wrinkle silence; it gates on real-silence (fires even when _last_reading_time was just self-bumped); no injection before off_delay; no-op before the first reading; the real detector closes anti-wrinkle into off under injected keepalives once the idle-timeout elapses; a real tumble pulse resets the idle timer; the throttle bypass admits real pulses while RUNNING high readings and anti-wrinkle baselines stay throttled.

Full fast suite green: 1365 passed (only test_mock_socket_synthesis.py skipped — missing optional nicegui devtools dependency, unrelated to this change).

Tested on:

  • Home Assistant version: 2026.7.4
  • WashData version: 0.5.3 (fix targets 0.5.4)
  • Device type(s): Dryer (Bosch Series 8 heat-pump, publish-on-change Zigbee power sensor)

Breaking Changes?

  • This PR includes breaking changes

Checklist

  • My code follows the project's code standards (PEP 8, type hints)
  • I've added/updated docstrings for new functions/classes
  • I've added corresponding tests (if applicable)
  • I've updated documentation (README, CHANGELOG, etc. if applicable)
  • I've checked that python3 -m compileall custom_components tests passes
  • I've run ./run_tests.sh and all tests pass
  • No hardcoded UI strings - all user-facing text is in strings.json and translations/
  • I've reviewed my own code for quality

@github-actions

Copy link
Copy Markdown

Hi @sharkyy! This PR references #339, which has not yet received the accepted label.

@3dg1luk43 has been notified and will review the linked issue. Once the accepted label is added there, this PR will pass the check automatically.

Nothing more is needed from you for now — thanks for your patience!

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 60e22747-d2ed-4dc9-a73a-7e9fd233d7fe

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The watchdog now injects synthetic 0W readings when anti-wrinkle receives no sensor updates beyond off_delay, allowing the detector to reach OFF. Real anti-wrinkle pulses bypass throttling, and release metadata is updated to 0.5.4.

Changes

Anti-wrinkle watchdog fix

Layer / File(s) Summary
Watchdog keepalive and release update
custom_components/ha_washdata/manager.py, custom_components/ha_washdata/manifest.json, CHANGELOG.md
Real anti-wrinkle pulses are delivered despite throttling; the watchdog injects 0W readings after prolonged silence, and release metadata documents version 0.5.4.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PowerSensor
  participant WashDataManager
  participant Detector
  participant Entities
  PowerSensor->>WashDataManager: Sends anti-wrinkle pulse
  WashDataManager->>Detector: Processes real power reading
  WashDataManager->>WashDataManager: Detects silence beyond off_delay
  WashDataManager->>Detector: Injects synthetic 0W reading
  Detector-->>WashDataManager: Transitions to OFF
  WashDataManager->>Entities: Notifies state update
Loading

Possibly related PRs

Suggested reviewers: 3dg1luk43

Poem

A rabbit sends zero watts with care,
So sleepy timers tick through silent air.
Real tumble pulses hop right through,
Anti-wrinkle finds OFF anew.
Version four brings a tidy lair!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The code summary matches issue #339 by adding an anti-wrinkle watchdog keepalive branch and preserving real pulse handling.
Out of Scope Changes check ✅ Passed Only expected versioning and changelog updates are shown; no unrelated changes are evident.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly matches the main fix: injecting a watchdog keepalive during anti-wrinkle silence.
Description check ✅ Passed The description follows the template closely and includes the linked issue, change summary, type, changes, testing, and checklist.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@custom_components/ha_washdata/manager.py`:
- Around line 3252-3255: The anti-wrinkle keepalive path must not update the
sampling-throttle clock used by the reading-processing logic. Replace the
`_last_reading_time` assignment after `self.detector.process_reading(0.0, now)`
with a separate keepalive timestamp, and update the keepalive scheduling logic
to use it while preserving `_last_reading_time` for real readings so high-power
tumble pulses still reach the detector.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c6d460e7-8292-4f9c-838a-a98e0d17f7db

📥 Commits

Reviewing files that changed from the base of the PR and between 50c0218 and 5f9a470.

⛔ Files ignored due to path filters (1)
  • tests/test_anti_wrinkle_watchdog_keepalive.py is excluded by !tests/**
📒 Files selected for processing (3)
  • CHANGELOG.md
  • custom_components/ha_washdata/manager.py
  • custom_components/ha_washdata/manifest.json
📜 Review details
⚠️ CI failures not shown inline (2)

GitHub Actions: Validate Pull Request / 1_Check PR Description.txt: [FIX] Inject watchdog keepalive during anti-wrinkle silence

Conclusion: failure

View job details

##[group]Run actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b
 with:
   github-***REDACTED***
   script: const pr = context.payload.pull_request;
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = pr.number;
const prAuthor = pr.user.login;
const prBody = pr.body || '';
const GITLOCALIZE_BOT = 'gitlocalize-app[bot]';
if (prAuthor === owner || prAuthor === GITLOCALIZE_BOT) {
  console.log(`Author is repo owner or GitLocalize bot (${prAuthor}), skipping completeness check.`);
  return;
}
if (pr.state === 'closed') {
  console.log('PR is closed — skipping completeness check.');
  return;
}
const BOT_MARKER = '<!-- pr-validator-completeness-bot -->';
const LABEL = 'needs description';
// Check 1: description section has real content (not just HTML comments / placeholders)
const descSection = (prBody.match(/## Description\s*([\s\S]*?)(?=\n##|$)/i) || [])[1] || '';
const descClean = descSection
  .replace(/<!--[\s\S]*?-->/g, '')
  .replace(/\bCloses?\s+#\s*<!--.*?-->/gi, '')
  .trim();
const descriptionOk = descClean.length > 10;
// Check 2: at least one checkbox checked in Type of Change
const typeSection = (prBody.match(/## Type of Change\s*([\s\S]*?)(?=\n##|$)/i) || [])[1] || '';
const typeOk = /- \[x\]/i.test(typeSection);
const isComplete = descriptionOk && typeOk;
// Find existing bot comment (paginate so the marker is not missed on busy PRs)
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: prNumber, per_page: 100 });
const existing = comments.find(c => c.body && c.body.includes(BOT_MARKER));
if (isComplete) {
  // Remove warning label if present
  try {
    await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name: LABEL });
  } catch (_) {}
  // Remove warning comment if present
  if (existing) {
    await github.rest.issues.deleteComment({ owner, repo, comment_id: existing.id });
  }
  console.log('PR description is complete.');
  return;
}
c...

GitHub Actions: Validate Pull Request / Check PR Description: [FIX] Inject watchdog keepalive during anti-wrinkle silence

Conclusion: failure

View job details

##[group]Run actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b
 with:
   github-***REDACTED***
   script: const pr = context.payload.pull_request;
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = pr.number;
const prAuthor = pr.user.login;
const prBody = pr.body || '';
const GITLOCALIZE_BOT = 'gitlocalize-app[bot]';
if (prAuthor === owner || prAuthor === GITLOCALIZE_BOT) {
  console.log(`Author is repo owner or GitLocalize bot (${prAuthor}), skipping completeness check.`);
  return;
}
if (pr.state === 'closed') {
  console.log('PR is closed — skipping completeness check.');
  return;
}
const BOT_MARKER = '<!-- pr-validator-completeness-bot -->';
const LABEL = 'needs description';
// Check 1: description section has real content (not just HTML comments / placeholders)
const descSection = (prBody.match(/## Description\s*([\s\S]*?)(?=\n##|$)/i) || [])[1] || '';
const descClean = descSection
  .replace(/<!--[\s\S]*?-->/g, '')
  .replace(/\bCloses?\s+#\s*<!--.*?-->/gi, '')
  .trim();
const descriptionOk = descClean.length > 10;
// Check 2: at least one checkbox checked in Type of Change
const typeSection = (prBody.match(/## Type of Change\s*([\s\S]*?)(?=\n##|$)/i) || [])[1] || '';
const typeOk = /- \[x\]/i.test(typeSection);
const isComplete = descriptionOk && typeOk;
// Find existing bot comment (paginate so the marker is not missed on busy PRs)
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: prNumber, per_page: 100 });
const existing = comments.find(c => c.body && c.body.includes(BOT_MARKER));
if (isComplete) {
  // Remove warning label if present
  try {
    await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name: LABEL });
  } catch (_) {}
  // Remove warning comment if present
  if (existing) {
    await github.rest.issues.deleteComment({ owner, repo, comment_id: existing.id });
  }
  console.log('PR description is complete.');
  return;
}
c...
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.py: ONLY NumPy allowed for numerical operations - No SciPy, scikit-learn, or other ML libraries
No external API calls - All processing must be local
ALWAYS use dt_util.now() for timezone-aware datetimes instead of standard datetime functions
All time/energy calculations MUST be datetime-aware using timestamps, not sample counts, with explicit gap handling
NO inline strings in Python code for UI text - All labels/descriptions must be in strings.json and translations/en.json
Use async_update_entry for config entry modifications in Home Assistant
Store tunables in entry.options, identity keys in entry.data
Gate debug entities behind expose_debug_entities option - only expose debug information when explicitly enabled
Always exclude power_data, debug_data, and power_trace from fired Home Assistant events to comply with 32KB event data limit
NEVER drop user data during migrations - always preserve cycles, labels, and corrections from previous versions

Files:

  • custom_components/ha_washdata/manager.py
🔇 Additional comments (2)
custom_components/ha_washdata/manifest.json (1)

24-24: LGTM!

CHANGELOG.md (1)

8-12: LGTM!

Comment thread custom_components/ha_washdata/manager.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@custom_components/ha_washdata/manager.py`:
- Around line 2870-2872: Update the anti-wrinkle throttle-bypass condition in
the detector logic around is_anti_wrinkle_pulse to compare power against the
same effective exit threshold used by the idle-timer reset:
max(anti_wrinkle_exit_power, stop_threshold_w), rather than min_power. Add a
regression case covering an anti-wrinkle pulse below min_power that still meets
this effective threshold and must bypass throttling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2bcd822b-a05b-43a5-a840-56cb1ca1b555

📥 Commits

Reviewing files that changed from the base of the PR and between 5f9a470 and fd255ab.

⛔ Files ignored due to path filters (1)
  • tests/test_anti_wrinkle_watchdog_keepalive.py is excluded by !tests/**
📒 Files selected for processing (2)
  • CHANGELOG.md
  • custom_components/ha_washdata/manager.py
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: validate-hacs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.py: ONLY NumPy allowed for numerical operations - No SciPy, scikit-learn, or other ML libraries
No external API calls - All processing must be local
ALWAYS use dt_util.now() for timezone-aware datetimes instead of standard datetime functions
All time/energy calculations MUST be datetime-aware using timestamps, not sample counts, with explicit gap handling
NO inline strings in Python code for UI text - All labels/descriptions must be in strings.json and translations/en.json
Use async_update_entry for config entry modifications in Home Assistant
Store tunables in entry.options, identity keys in entry.data
Gate debug entities behind expose_debug_entities option - only expose debug information when explicitly enabled
Always exclude power_data, debug_data, and power_trace from fired Home Assistant events to comply with 32KB event data limit
NEVER drop user data during migrations - always preserve cycles, labels, and corrections from previous versions

Files:

  • custom_components/ha_washdata/manager.py
🔇 Additional comments (2)
custom_components/ha_washdata/manager.py (1)

3239-3272: LGTM!

CHANGELOG.md (1)

8-12: LGTM!

Comment on lines +2870 to +2872
is_anti_wrinkle_pulse = (
self.detector.state == STATE_ANTI_WRINKLE and power >= min_p
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Match the throttle bypass to the detector’s anti-wrinkle exit threshold.

The detector resets its anti-wrinkle idle timer for readings at or above max(anti_wrinkle_exit_power, stop_threshold_w), but this bypass requires power >= min_power. When that effective threshold is below min_power, a real tumble pulse in between can arrive after a synthetic keepalive, be throttled, and fail to reset the idle timer.

Use the same effective-exit threshold here and add a regression case for a pulse below min_power.

🔧 Proposed fix
+        anti_wrinkle_effective_exit = max(
+            float(self.detector.config.anti_wrinkle_exit_power),
+            float(self.detector.config.stop_threshold_w),
+        )
         is_anti_wrinkle_pulse = (
             self.detector.state == STATE_ANTI_WRINKLE
-            and power >= min_p
+            and power >= anti_wrinkle_effective_exit
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@custom_components/ha_washdata/manager.py` around lines 2870 - 2872, Update
the anti-wrinkle throttle-bypass condition in the detector logic around
is_anti_wrinkle_pulse to compare power against the same effective exit threshold
used by the idle-timer reset: max(anti_wrinkle_exit_power, stop_threshold_w),
rather than min_power. Add a regression case covering an anti-wrinkle pulse
below min_power that still meets this effective threshold and must bypass
throttling.

@sharkyy
sharkyy changed the base branch from main to 0.5.4 July 27, 2026 18:43
@sharkyy
sharkyy force-pushed the fix/anti-wrinkle-watchdog-keepalive branch from fd255ab to 94fa228 Compare July 27, 2026 18:43
The anti-wrinkle idle-timeout (anti_wrinkle_idle_timeout) and the 2 h
safety cap both live in the detector and only advance from within
process_reading, so they need incoming readings to fire. A
publish-on-change power sensor goes completely silent once power
flatlines at standby / 0 W after the last tumble pulse, which freezes
the idle timer mid-count and pins the state in anti_wrinkle until the
next real reading (typically the next cycle).

Nothing was advancing the timer during that silence. The watchdog is
stopped for the whole anti-wrinkle tail (anti_wrinkle is entered via
_finish_cycle, whose on_cycle_end callback calls _stop_watchdog, and the
watchdog is only restarted on the next cycle start), and the state-expiry
timer explicitly skipped anti_wrinkle. Drive the keepalive from the
state-expiry timer instead, which does keep ticking through the tail: on
silence longer than off_delay it injects a 0 W keepalive, letting the
detector's own idle-timeout / 2 h safety close the tail into OFF.

Gate the keepalive on _last_real_reading_time (only genuine sensor
readings bump it), not _last_reading_time which the keepalive itself
bumps: with off_delay == the 60 s state-expiry interval, gating on the
self-bumped clock made the condition true only every other tick, so the
idle timer advanced at half real-time and took ~2x as long to close.
Real-silence gating fires a keepalive every tick during genuine silence,
so the tail closes right at anti_wrinkle_idle_timeout after the last pulse.

The synthetic keepalive bumps _last_reading_time, which is also the
sampling-throttle clock in _async_power_changed. A real tumble pulse
(>= min_power, therefore not is_low_power) arriving within one sampling
interval of a keepalive would be discarded by that throttle before
reaching the detector, so it could not reset the idle timer. Exempt
high-power readings from the throttle while in anti_wrinkle; sub-min
baseline readings stay throttled as before.

Tests in tests/test_anti_wrinkle_silent_close.py cover: keepalive
injection during anti-wrinkle silence, gating on real-silence (not the
self-bumped clock), no injection before off_delay, no-op before first
reading, end-to-end close into OFF under injected keepalives, a pulse
resets the idle timer, and the throttle bypass for real pulses.

Closes 3dg1luk43#339

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sharkyy
sharkyy force-pushed the fix/anti-wrinkle-watchdog-keepalive branch from 94fa228 to 57f1be9 Compare July 30, 2026 06:56
The throttle bypass for real tumble pulses required power >= min_power, but
the detector resets its anti-wrinkle idle timer for readings at or above
effective_exit = max(anti_wrinkle_exit_power, stop_threshold_w). When that
threshold is below min_power, a real pulse in [effective_exit, min_power)
resets the detector's idle timer yet was throttled here, so a keepalive-
adjacent pulse could be dropped and the mode could time out mid-tumble.

Gate the bypass on effective_exit instead. Adds regression tests for a pulse
below min_power (must bypass) and a reading below effective_exit (must stay
throttled).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sharkyy

sharkyy commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Good catch — addressed in 626e62b.

The throttle bypass now gates on the same threshold the detector uses to reset its anti-wrinkle idle timer:

anti_wrinkle_exit_threshold = max(
    float(self.detector.config.anti_wrinkle_exit_power),
    float(self.detector.config.stop_threshold_w),
)
is_anti_wrinkle_pulse = (
    self.detector.state == STATE_ANTI_WRINKLE
    and power >= anti_wrinkle_exit_threshold
)

So when effective_exit < min_power, a real pulse in [effective_exit, min_power) — which does reset the detector's idle timer — is no longer discarded by the throttle after a keepalive.

Added two regression tests:

  • test_anti_wrinkle_pulse_below_min_power_still_bypasses_throttleeffective_exit = 2.0, min_power = 5.0; a 3.0 W pulse reaches the detector.
  • test_anti_wrinkle_reading_below_effective_exit_stays_throttled — a 1.5 W reading (below effective_exit) stays throttled, so sub-threshold baselines still can't flood the detector.

Full file green (11 passed).

kdjkdjkdj added a commit to kdjkdjkdj/ha_washdata that referenced this pull request Aug 2, 2026
Carries upstream PR 3dg1luk43#340 by Maximilian Schmidt (@sharkyy) for issue 3dg1luk43#339,
cherry-picked ahead of review so the fix can be validated on real hardware
while the maintainer is on summer break. Commits 57f1be9 and 626e62b only;
the unrelated startup/notify commit on the same branch was left out.

Verified before the pick: with the fix reverted, 4 of the 11 tests in
tests/test_anti_wrinkle_silent_close.py fail, so they do catch the defect.
With it: 11 passed, full suite 1387 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kdjkdjkdj added a commit to kdjkdjkdj/ha_washdata that referenced this pull request Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Anti-wrinkle state stays stuck for hours when a publish-on-change power sensor goes silent

1 participant