Visitar URL original
0.5.3 by 3dg1luk43 · Pull Request #327 · 3dg1luk43/ha_washdata · GitHub
Skip to content

0.5.3 - #327

Merged
3dg1luk43 merged 48 commits into
mainfrom
0.5.3
Jul 26, 2026
Merged

0.5.3#327
3dg1luk43 merged 48 commits into
mainfrom
0.5.3

Conversation

@3dg1luk43

Copy link
Copy Markdown
Owner

0.5.3

3dg1luk43 and others added 11 commits July 23, 2026 14:44
Adds a Playground-only "Test idle termination" mode to _DetailSim that
replaces the quiet (0W) tail with a synthetic standby-draw continuation,
then drives the real CycleDetector to determine if/when/how WashData stops
the cycle.

Two-phase approach for performance:
- Dense pre-fill (20 min, 30s cadence): populates _power_readings so
  _is_standby_band_stuck and _window_has_outage_gap fire correctly.
- Sparse main (1800s steps, max 15): accumulator-correct large dt, 8h
  hard cap fires normally. ~55 readings total vs 960 for naive simulation.

_sample() called on every stress reading (progress/remaining/phase tracked
through the overrun, byte-identical to the live estimator). _derive_idle_level
returns the p7 standby floor of the last 60s (not contaminated mean). Jitter
seeded from cycle id for deterministic replays. outcome["stress"] dict +
three alert codes (stress_terminated/stress_above_threshold/stress_hit_cap)
added to finalize(). run_tail() is byte-identical (unchanged).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds optional stress_tail (bool, default false) and stress_idle_w
(float|null, default null) params to start_playground_cycle_detail
WS command. _pg_detail_task routes to run_stress_tail() vs run_tail()
based on sim.stress_tail. Updates ws_schema.py, ws-types.d.ts, and
regenerates docs/WS_API.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Implement Playwright tests for the export/import wizard covering the export selection tree and import flow.
- Create unit tests for selective export functionality, ensuring proper category filtering and envelope rules.
- Add tests for import manifest analysis, including conflict resolution and device type matching.
- Introduce WebSocket API tests for export and import operations, validating data filtering and settings application.
- Ensure comprehensive coverage of merge and replace behaviors during selective import operations.
…doc (Phase 4)

Adds 10 new translation keys for the idle termination test feature under
the lbl and msg sections of translations/panel/en.json:
- pg_alert_stress_cap/ok/warn — three alert codes
- pg_stress_group/toggle/toggle_desc/idle_w/idle_w_desc/idle_auto — UI labels
- msg.pg_stress_synthetic — canvas label for the synthetic region start

Also updates docs/internal/INTEGRATION_REFERENCE.md: register item 55 for
the playground idle termination test feature, module line count for
playground.py updated to ~1960.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds 10 new keys (pg_alert_stress_cap/ok/warn, pg_stress_group/toggle/
toggle_desc/idle_w/idle_w_desc/idle_auto, msg.pg_stress_synthetic) to all
non-English panel translation files via domain-aware Claude subagents.

pg_stress_idle_auto is kept as "Auto" universally. No em dashes used.
Translations cover Germanic (de/nl/sv/da/nb/is), Romance (fr/es/pt/pt-BR/
it/ro), Slavic (cs/pl/sk/sl/hr/sr-Latn/ru/uk/bg/bs/mk), Baltic (et/fi/
lt/lv), and additional (el/sq/ja/ko/zh-Hans/hu/tr) families.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes all _pgRerunDetail() auto-trigger calls from:
- threshold drag release (canvas pointerup)
- [data-pgkey] param input changes
- stress-tail toggle and idle-level field
- "Save to settings" and "Apply best sweep value" completion

Param changes still call _render() (so the Save-to-settings button
appears when there are overrides) + _pgDrawCanvas() (so threshold lines
update live). The simulation only runs when the user presses Run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added a guided selective export/import wizard with previews, conflict handling, merge/replace modes, and cycle destination options.
    • Added a Playground idle-termination test with configurable idle levels and visualized synthetic continuation.
    • Added configurable anti-wrinkle idle timing and improved feedback resolution after cycle relabeling.
  • Bug Fixes

    • Fixed overnight cycle trimming, live power charts stopping early, startup registration races, and stale pending feedback.
  • Performance

    • Accelerated Playground simulations, profile matching, phase processing, and catalog browsing through optimized processing and caching.
  • Documentation

    • Updated release notes and anti-wrinkle guidance.

Walkthrough

Release 0.5.3 adds selective configuration transfer, Playground idle-termination simulation, optimized numerical processing, catalog caching, configurable anti-wrinkle timing, panel registration fixes, feedback resolution, and migration updates.

Changes

Selective configuration export and import

Layer / File(s) Summary
Storage and import execution
custom_components/ha_washdata/profile_store.py
Adds inventory analysis, filtered export, selective merge/replace import, conflict handling, cycle routing, deduplication, and settings filtering.
WebSocket contracts and handlers
custom_components/ha_washdata/ws_api.py, custom_components/ha_washdata/ws_schema.py, custom_components/ha_washdata/www/ws-types.d.ts
Exposes typed, access-controlled inventory, analysis, export, and import commands.
Export/import wizard
custom_components/ha_washdata/www/ha-washdata-panel.js
Adds hierarchical selection, import analysis, conflict handling, cycle destinations, downloads, and apply actions.

Playground and detector behavior

Layer / File(s) Summary
Stress-tail simulation
custom_components/ha_washdata/playground.py, custom_components/ha_washdata/const.py
Generates synthetic idle continuations, records termination outcomes, and emits stress alerts.
Stress controls and rendering
custom_components/ha_washdata/ws_api.py, custom_components/ha_washdata/www/ha-washdata-panel.js
Carries stress parameters through WebSocket requests and renders controls, alerts, overlays, and anti-wrinkle state.
Detector configuration and feedback
custom_components/ha_washdata/cycle_detector.py, custom_components/ha_washdata/manager.py, custom_components/ha_washdata/__init__.py, custom_components/ha_washdata/learning.py
Removes the obsolete running-dead-zone setting, configures anti-wrinkle timing, gates sensor swaps, and resolves pending feedback after relabeling.

Performance, caching, and release updates

Layer / File(s) Summary
Numerical optimization
custom_components/ha_washdata/analysis.py, custom_components/ha_washdata/progress.py, custom_components/ha_washdata/phase_segmenter.py
Adds optimized DTW, progress-scanning, and phase-run paths with scalar fallbacks.
Catalog caching
custom_components/ha_washdata/store_client.py
Adds bounded TTL caching, request coalescing, failure-aware reads, and write invalidation.
Panel registration and release metadata
custom_components/ha_washdata/frontend.py, custom_components/ha_washdata/manifest.json, CHANGELOG.md, test-results/.last-run.json
Coalesces panel registration, serves optional assets, updates release metadata, documents the release, and records passing test status.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Panel
  participant ws_api
  participant ProfileStore
  Panel->>ws_api: analyze_import JSON
  ws_api->>ProfileStore: build_import_manifest
  ProfileStore-->>ws_api: import manifest
  ws_api-->>Panel: manifest
  Panel->>ws_api: import_config_selective selection
  ws_api->>ProfileStore: async_import_data_selective
  ProfileStore-->>ws_api: import summary
  ws_api-->>Panel: summary
Loading
sequenceDiagram
  participant Panel
  participant ws_api
  participant DetailSim
  participant CycleDetector
  Panel->>ws_api: start stress-tail simulation
  ws_api->>DetailSim: construct replay with stress settings
  DetailSim->>CycleDetector: process synthetic readings
  CycleDetector-->>DetailSim: termination state
  DetailSim-->>ws_api: stress outcome and alerts
  ws_api-->>Panel: detail result with shaded stress region
Loading

Possibly related PRs

Poem

A bunny packs settings, neat and light,
While idle tails stretch through the night.
DTW hops faster, caches stay bright,
Tri-state carrots point left and right—
Release 0.5.3 feels just right!

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (1 warning, 2 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is too generic to describe the change and does not convey the main update. Use a concise title that names the primary change, such as selective import/export and Playground updates.
Description check ❓ Inconclusive The description is only a version number and omits the required issue link, summary, change type, testing, and checklist details. Fill out the template, especially an accepted issue reference, summary, change type, changes made, testing, and checklist items.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 0.5.3

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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
custom_components/ha_washdata/store_client.py (1)

350-369: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Avoid caching the brand search behind a fixed 60-brand limit.

list_brands caches the unfiltered brand list and applies q in memory over at most page_size=60 rows, so future growth or any non-ascending result can make later brands unsearchable for the cached TTL. Store the full brand list separately, or include a server-side prefix/range filter in the Firestore query.

🤖 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/store_client.py` around lines 350 - 369, The
list_brands method must not cache a truncated 60-brand result as the source for
in-memory searches. Update its cache/query flow to retrieve and store the
complete unfiltered brand list independently of page_size, then apply q
filtering and pagination as needed; alternatively, implement q as a server-side
prefix/range filter. Ensure later brands remain searchable throughout the cache
TTL.
🤖 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 `@CHANGELOG.md`:
- Line 12: Update the release-note entry’s attribution wording, replacing “idea
by” with “proposed by” or removing the attribution phrase while preserving the
rest of the export/import description.

In `@custom_components/ha_washdata/playground.py`:
- Around line 946-949: Move the new stress-related UI text from the inline
f-strings in the stress_tail_start emission and the stress_terminated,
stress_above_threshold, and stress_hit_cap alert details into strings.json and
translations/en.json. Update the corresponding code to retrieve and format those
localized string entries through the existing translation mechanism, preserving
the current dynamic values.
- Around line 935-938: Update the override branch in the code using
_derive_idle_level so stress_idle_override is clamped to a non-negative value
before assigning idle_w. Remove the redundant second _derive_idle_level call and
preserve the fluct_w value returned by the initial call.
- Around line 1142-1148: Update the safety-cap alert construction in the branch
appending the "stress_hit_cap" alert to replace the hardcoded “8 h” text with
the actual elapsed duration from the existing terminated_after_s value. Format
that duration clearly in the detail message while preserving the idle-draw and
threshold information.

In `@custom_components/ha_washdata/profile_store.py`:
- Around line 6075-6079: Update the device-type comparison in
async_import_data_selective to use the same matching formula as
build_import_manifest, including the empty local_device_type behavior. Ensure an
unavailable local device type yields the same device_specific category filtering
and real-history routing decisions as manifest analysis.

In `@custom_components/ha_washdata/www/ha-washdata-panel.js`:
- Around line 5404-5422: Update the checkbox in the stressToggle markup to
provide an accessible name, using an aria-label derived from the existing “Test
idle termination” label text. Keep the current layout and behavior unchanged.

---

Outside diff comments:
In `@custom_components/ha_washdata/store_client.py`:
- Around line 350-369: The list_brands method must not cache a truncated
60-brand result as the source for in-memory searches. Update its cache/query
flow to retrieve and store the complete unfiltered brand list independently of
page_size, then apply q filtering and pagination as needed; alternatively,
implement q as a server-side prefix/range filter. Ensure later brands remain
searchable throughout the cache TTL.
🪄 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: 0384307b-2150-4fec-9d01-a27f4b44beea

📥 Commits

Reviewing files that changed from the base of the PR and between 08aa94e and 5f9c83c.

⛔ Files ignored due to path filters (79)
  • custom_components/ha_washdata/translations/bg.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/bs.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/cs.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/da.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/de.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/el.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/es.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/et.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/fi.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/fr.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/hr.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/hu.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/is.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/it.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/ja.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/ko.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/lt.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/lv.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/mk.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/nb.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/nl.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/bg.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/bs.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/cs.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/da.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/de.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/el.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/en.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/es.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/et.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/fi.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/fr.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/hr.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/hu.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/is.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/it.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/ja.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/ko.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/lt.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/lv.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/mk.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/nb.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/nl.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/pl.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/pt-BR.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/pt.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/ro.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/ru.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sk.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sl.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sq.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sr-Latn.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sv.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/tr.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/uk.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/zh-Hans.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/pl.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/pt-BR.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/pt.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/ro.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/ru.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/sk.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/sl.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/sq.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/sr-Latn.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/sv.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/tr.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/uk.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/zh-Hans.json is excluded by !custom_components/ha_washdata/translations/**
  • docs/WS_API.md is excluded by !docs/**
  • docs/internal/INTEGRATION_REFERENCE.md is excluded by !docs/**
  • docs/internal/reference/11-community-store.md is excluded by !docs/**
  • playwright-tests/tests/export-import-wizard.spec.ts is excluded by !playwright-tests/**
  • tests/test_export_selective.py is excluded by !tests/**
  • tests/test_import_manifest.py is excluded by !tests/**
  • tests/test_import_selective.py is excluded by !tests/**
  • tests/test_playground_stress_tail.py is excluded by !tests/**
  • tests/test_store_client.py is excluded by !tests/**
  • tests/test_ws_export_import_selective.py is excluded by !tests/**
📒 Files selected for processing (13)
  • CHANGELOG.md
  • custom_components/ha_washdata/analysis.py
  • custom_components/ha_washdata/const.py
  • custom_components/ha_washdata/manifest.json
  • custom_components/ha_washdata/phase_segmenter.py
  • custom_components/ha_washdata/playground.py
  • custom_components/ha_washdata/profile_store.py
  • custom_components/ha_washdata/progress.py
  • custom_components/ha_washdata/store_client.py
  • custom_components/ha_washdata/ws_api.py
  • custom_components/ha_washdata/ws_schema.py
  • custom_components/ha_washdata/www/ha-washdata-panel.js
  • custom_components/ha_washdata/www/ws-types.d.ts
📜 Review details
🧰 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/phase_segmenter.py
  • custom_components/ha_washdata/const.py
  • custom_components/ha_washdata/ws_schema.py
  • custom_components/ha_washdata/progress.py
  • custom_components/ha_washdata/analysis.py
  • custom_components/ha_washdata/store_client.py
  • custom_components/ha_washdata/playground.py
  • custom_components/ha_washdata/ws_api.py
  • custom_components/ha_washdata/profile_store.py
🪛 ast-grep (0.44.1)
custom_components/ha_washdata/ws_api.py

[info] 3092-3092: use jsonify instead of json.dumps for JSON output
Context: json.dumps(payload, indent=2)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

custom_components/ha_washdata/profile_store.py

[info] 1146-1146: use jsonify instead of json.dumps for JSON output
Context: json.dumps(x, sort_keys=True, default=str)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 1158-1158: use jsonify instead of json.dumps for JSON output
Context: json.dumps(item, sort_keys=True, default=str)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🪛 Ruff (0.15.21)
custom_components/ha_washdata/phase_segmenter.py

[warning] 190-190: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

custom_components/ha_washdata/ws_schema.py

[warning] 862-862: Boolean positional value in function call

(FBT003)


[warning] 863-863: Boolean positional value in function call

(FBT003)


[warning] 864-864: Boolean positional value in function call

(FBT003)


[warning] 865-865: Boolean positional value in function call

(FBT003)


[warning] 978-978: Boolean positional value in function call

(FBT003)


[warning] 979-979: Boolean positional value in function call

(FBT003)

custom_components/ha_washdata/progress.py

[error] 374-375: try-except-continue detected, consider logging the exception

(S112)


[warning] 374-374: Do not catch blind exception: Exception

(BLE001)


[warning] 378-378: Too many statements (57 > 50)

(PLR0915)


[warning] 460-460: Do not catch blind exception: Exception

(BLE001)

custom_components/ha_washdata/analysis.py

[warning] 182-185: Use ternary operator best_prev = (m1 if m1 < m3 else m3) if m1 < m2 else m2 if m2 < m3 else m3 instead of if-else-block

Replace if-else-block with best_prev = (m1 if m1 < m3 else m3) if m1 < m2 else m2 if m2 < m3 else m3

(SIM108)


[warning] 206-206: Docstring contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF002)


[warning] 210-210: Docstring contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF002)


[warning] 211-211: Docstring contains ambiguous – (EN DASH). Did you mean - (HYPHEN-MINUS)?

(RUF002)


[warning] 211-211: Docstring contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF002)


[warning] 254-254: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)


[warning] 258-259: Replace if statement with best = min(best, pr_j1)

Replace with best = min(best, pr_j1)

(PLR1730)


[warning] 268-268: Do not catch blind exception: Exception

(BLE001)


[warning] 523-523: Do not catch blind exception: Exception

(BLE001)

custom_components/ha_washdata/store_client.py

[warning] 215-215: Dynamically typed expressions (typing.Any) are disallowed in _cache_get

(ANN401)


[warning] 227-227: Dynamically typed expressions (typing.Any) are disallowed in value

(ANN401)


[warning] 350-350: Boolean-typed positional argument in function definition

(FBT001)


[warning] 350-350: Boolean default positional argument in function definition

(FBT002)

custom_components/ha_washdata/playground.py

[warning] 384-384: Boolean-typed positional argument in function definition

(FBT001)


[warning] 384-384: Boolean default positional argument in function definition

(FBT002)


[warning] 448-448: Boolean-typed positional argument in function definition

(FBT001)


[warning] 448-448: Boolean default positional argument in function definition

(FBT002)


[warning] 487-487: Boolean-typed positional argument in function definition

(FBT001)


[warning] 487-487: Boolean default positional argument in function definition

(FBT002)


[warning] 540-540: Boolean-typed positional argument in function definition

(FBT001)


[warning] 540-540: Boolean default positional argument in function definition

(FBT002)


[warning] 866-866: Docstring contains ambiguous × (MULTIPLICATION SIGN). Did you mean x (LATIN SMALL LETTER X)?

(RUF002)


[warning] 1011-1011: Do not catch blind exception: Exception

(BLE001)

custom_components/ha_washdata/ws_api.py

[warning] 3006-3006: Do not catch blind exception: Exception

(BLE001)


[warning] 3061-3061: Do not catch blind exception: Exception

(BLE001)


[warning] 3098-3098: Do not catch blind exception: Exception

(BLE001)


[warning] 3202-3202: Do not catch blind exception: Exception

(BLE001)


[warning] 5689-5689: Boolean-typed positional argument in function definition

(FBT001)


[warning] 5689-5689: Boolean default positional argument in function definition

(FBT002)

custom_components/ha_washdata/profile_store.py

[warning] 884-884: Dynamically typed expressions (typing.Any) are disallowed in d

(ANN401)


[warning] 891-891: Dynamically typed expressions (typing.Any) are disallowed in opts

(ANN401)


[warning] 907-907: Dynamically typed expressions (typing.Any) are disallowed in selection

(ANN401)


[warning] 928-928: Dynamically typed expressions (typing.Any) are disallowed in seq

(ANN401)


[warning] 941-941: Dynamically typed expressions (typing.Any) are disallowed in data_dict

(ANN401)


[warning] 941-941: Dynamically typed expressions (typing.Any) are disallowed in entry_options

(ANN401)


[warning] 1021-1021: Dynamically typed expressions (typing.Any) are disallowed in payload

(ANN401)


[warning] 1041-1041: Prefer TypeError exception for invalid type

(TRY004)


[warning] 1041-1041: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 1054-1054: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 1080-1080: Dynamically typed expressions (typing.Any) are disallowed in payload

(ANN401)


[warning] 1080-1080: Dynamically typed expressions (typing.Any) are disallowed in local_profile_names

(ANN401)


[warning] 1146-1149: Use contextlib.suppress(TypeError, ValueError) instead of try-except-pass

Replace try-except-pass with with contextlib.suppress(TypeError, ValueError): ...

(SIM105)


[warning] 1148-1149: try-except within a loop incurs performance overhead

(PERF203)


[warning] 3419-3419: Too many branches (25 > 12)

(PLR0912)


[warning] 3419-3419: Too many statements (80 > 50)

(PLR0915)


[warning] 5953-5953: Unused method argument: entry_options

(ARG002)


[warning] 6038-6038: Too many branches (52 > 12)

(PLR0912)


[warning] 6038-6038: Too many statements (165 > 50)

(PLR0915)


[warning] 6071-6071: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 6091-6093: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 6095-6097: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 6194-6194: Dynamically typed expressions (typing.Any) are disallowed in raw_id

(ANN401)

🔇 Additional comments (32)
CHANGELOG.md (1)

8-11: LGTM!

Also applies to: 13-31

custom_components/ha_washdata/manifest.json (1)

24-24: LGTM!

custom_components/ha_washdata/playground.py (3)

39-39: LGTM!

Also applies to: 89-95, 384-404, 448-471, 487-509, 540-571


861-887: LGTM!


916-934: LGTM!

Also applies to: 950-1015

custom_components/ha_washdata/const.py (1)

997-1007: LGTM!

custom_components/ha_washdata/analysis.py (2)

165-189: LGTM!

Also applies to: 191-273


446-506: LGTM!

Also applies to: 509-524

custom_components/ha_washdata/progress.py (1)

317-462: LGTM!

custom_components/ha_washdata/phase_segmenter.py (1)

174-190: LGTM!

custom_components/ha_washdata/store_client.py (1)

170-240: LGTM!

Also applies to: 280-298, 384-402, 592-595

custom_components/ha_washdata/profile_store.py (1)

1700-1778: LGTM!

Also applies to: 3419-3449, 5893-6008

custom_components/ha_washdata/ws_schema.py (1)

203-214: LGTM!

Also applies to: 646-649, 855-866, 978-979

custom_components/ha_washdata/ws_api.py (3)

436-443: LGTM!

Also applies to: 453-472, 1081-1083, 3117-3204, 5746-5747, 5770-5774


5699-5703: 🎯 Functional Correctness

No change needed — build_cycle_detail_sim_by_id already includes stress_tail and stress_idle_w in the same positional order.


3002-3004: 🩺 Stability & Availability

No locking is needed: the reported paths do not mutate self._data. The only export-side mutations in the surrounding path occur to the payload envelope (pop("store_account")), while _scan_data iterates shallow views and json.dumps serializes dicts. A full read/write lock would not be an appropriate fix.

			> Likely an incorrect or invalid review comment.
custom_components/ha_washdata/www/ws-types.d.ts (1)

15-17: LGTM!

Also applies to: 149-151, 286-289, 687-709, 911-912, 1062-1065, 1167-1170

custom_components/ha_washdata/www/ha-washdata-panel.js (15)

1813-1814: LGTM!


2863-2863: LGTM!


5479-5481: LGTM!


5951-5951: LGTM!


6171-6184: LGTM!


6576-6581: LGTM!


7449-7450: LGTM!


7661-7902: LGTM! The device-type-mismatch banner correctly _esc-escapes the unvalidated source_device_type/local_device_type before interpolation, closing the injection vector.


8630-8630: LGTM!


8662-8691: LGTM!


9156-9159: LGTM!


10087-10087: LGTM!


10181-10187: LGTM!


10448-10587: LGTM! Selection toggles are model-driven and the async analyze/apply paths re-validate _isActiveEntry + modal type after every await, guarding against device switches mid-flight.


9999-10013: 🗄️ Data Integrity & Integration

No change needed: get_export_inventory intentionally uses a differently shaped manifest for the export selection tree.

get_export_inventory returns _scan_data() output directly, and the export UI wraps it as { categories: inventory }; the import analyze result follows the import manifest shape.

Comment thread CHANGELOG.md Outdated
Comment thread custom_components/ha_washdata/playground.py Outdated
Comment thread custom_components/ha_washdata/playground.py
Comment thread custom_components/ha_washdata/playground.py
Comment thread custom_components/ha_washdata/profile_store.py
Comment thread custom_components/ha_washdata/www/ha-washdata-panel.js
sharkyy and others added 6 commits July 24, 2026 09:16
Anti-wrinkle mode ended after a hardcoded 120 s of quiet
(_anti_wrinkle_idle_timeout), which is shorter than the pulse spacing of
some dryers. On a Bosch heat-pump dryer the tumble pulses sit 130-660 s
apart (18 pulses measured in one recorded cycle, peaks <= 87 W), so the
mode dropped out right after the first pulse and every later pulse
surfaced as an aborted false start (off -> starting -> off, 3-14 times
per cycle across three recorded cycles) instead of staying attached to
the finished cycle.

- New option anti_wrinkle_idle_timeout ("Max Pulse Gap") in the
  Anti-Wrinkle settings section, wired through const/manager/detector
  including the live options-update path
- Default stays at the previous 120 s, so behaviour is unchanged unless
  the user raises it; the exit still also honours the dynamic end
  threshold, so nothing ends sooner than before
- Playground: anti_wrinkle used to map to "Idle" in the state band, so
  the mode was invisible in the view used to tune it - it now has its own
  band segment, and all four anti_wrinkle_* options are honoured as
  simulation overrides (they were silently ignored before)
- Tests for both directions of the new tolerance

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- manager: changing the power sensor while a cycle is running returned
  early, so every other option saved in the same submission was silently
  dropped (the comment right above it already said "continue with other
  config updates"). Skip only the sensor swap and keep applying the rest.
- playground: coerce boolean overrides explicitly - bool("false") is True,
  so a string-valued toggle could enable a mode the user switched off.
  Unknown values raise and the override is ignored, as before.
- panel: expose anti_wrinkle_max_duration and anti_wrinkle_exit_power as
  Playground overrides too, so the numeric anti-wrinkle knobs are all
  tunable in the simulator.
- tests: cover the dynamic end threshold acting as a floor under the
  configured tolerance.
- panel: add anti_wrinkle_enabled to the Playground override list. It is
  the one remaining anti-wrinkle option and a toggle, so the param editor
  now renders a checkbox for fields marked 'bool' and the input handler
  stores the boolean instead of parseFloat'ing it. The sweep dropdown
  filters those fields out - a sweep walks a numeric range.
- panel: pg_desc strings for the anti-wrinkle rows that were still
  falling back to the inline English text.
- tests: pin the default 120 s tolerance behaviourally (a pulse resets
  the gap clock, 110 s of quiet keeps the mode, crossing 120 s ends it),
  so the pre-existing behaviour of installs that never set the option
  stays covered alongside the above-floor and below-floor cases.
The visible label sits in a sibling div, so a screen reader announced the
checkbox unnamed. aria-label carries the same translated string the row
already renders.
- manager: the sensor-swap guard only blocked STATE_RUNNING, so a config
  save during STARTING/PAUSED/USER_PAUSED/ENDING could re-point the
  listener mid-cycle and splice a second appliance into the same trace.
  ANTI_WRINKLE is blocked too: its tumble pulses are still attributed to
  the cycle that just finished. Blocked states are now a named frozenset
  and the warning names the state it refused in.
- playground: _coerce_bool accepted any nonzero number, so 2, -1, NaN or
  inf could switch a mode on. Only 0/1 (and the usual bool/string
  spellings) are accepted now; anything else raises and the override is
  ignored, as before.
- playground: config_summary reported only enabled + idle timeout, so the
  other three anti-wrinkle overrides could not be verified from a
  simulation result. All five are reported now.
- CHANGELOG: the entry said "all four anti_wrinkle_* options"; with
  anti_wrinkle_enabled added in the previous round it is five.
- tests: the guard across every active state, the coercer's accept/reject
  set, and that a malformed boolean override leaves the mode off.
- playground: clamp negative stress_idle_w override to >= 0; drop redundant
  _derive_idle_level call
- playground: the three stress alert details are now translatable via
  detail_key/detail_params (en.json + all 34 languages) and rendered through _t;
  stress_hit_cap reports the actual elapsed duration instead of a hardcoded "8 h"
- profile_store: align async_import_data_selective device-type gate with
  build_import_manifest so the shown analysis and the applied gating cannot diverge
- panel: add an aria-label to the idle-termination toggle checkbox
- store_client: raise list_brands to a full-catalog cap so the cached in-memory
  prefix search stays complete as the brand catalog grows
- CHANGELOG: reword "idea by" to "proposed by"

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@3dg1luk43

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@3dg1luk43

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
custom_components/ha_washdata/playground.py (2)

869-887: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Make the trailing idle estimate gap-aware.

The window is selected by timestamps, but the percentile and variance are still weighted by sample count. Missing or irregular readings can let a short cluster dominate the idle estimate and produce an incorrect stress verdict. Detect gaps and use a contiguous-tail fallback or duration-weighted statistics.

As per coding guidelines, “All time/energy calculations MUST be datetime-aware using timestamps, not sample counts, with explicit gap handling.”

🤖 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/playground.py` around lines 869 - 887, Update
the trailing-window idle estimate around the readings processing logic to
account for timestamp gaps rather than weighting samples equally. Detect gaps
using datetime timestamps and either restrict calculations to the contiguous
tail after the latest gap or apply duration-weighted percentile and variance
calculations, while preserving the existing fallback behavior for empty or
insufficient data and returning nonnegative idle and fluctuation values.

Source: Coding guidelines


978-983: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Record the forced-stop timestamp in series.

When the safety cap is reached, flush_ts advances the cursor and force-stops the detector, but no sample is appended at that timestamp. The reported elapsed duration can therefore extend two sparse steps beyond the last plotted point.

🐛 Proposed fix
                 self.cursor["t"] = (flush_ts - self.base).total_seconds()
                 self.detector.force_end(flush_ts)
+                self._sample(flush_ts)
🤖 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/playground.py` around lines 978 - 983, Update
the safety-cap branch around detector.force_end in the playground flow to append
a series sample at flush_ts after advancing the cursor and forcing the detector
to stop. Ensure the recorded timestamp matches flush_ts so the reported duration
does not extend beyond the final plotted point.
custom_components/ha_washdata/store_client.py (2)

323-348: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Apply catalog prefix filters before limiting and caching.

Both paths cache only the first page, then filter that partial result in memory, so valid entries outside the limit are invisible.

  • custom_components/ha_washdata/store_client.py#L323-L348: apply model_query before limit, or cache a complete catalog and limit after filtering.
  • custom_components/ha_washdata/store_client.py#L350-L374: apply q before limit, preserve pagination, or enforce a hard catalog-size bound.
🤖 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/store_client.py` around lines 323 - 348, Update
the catalog query methods in
custom_components/ha_washdata/store_client.py:323-348 and :350-374 so
model_query and q filtering occur before applying page_size/limit and caching,
or fetch and cache a complete bounded catalog before filtering and paginating.
Preserve pagination behavior in the q path and ensure valid entries beyond the
initial query limit remain discoverable.

597-600: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Invalidate catalog caches on status changes and protect against in-flight repopulation.

The invalidation here only handles creates. confirm_device promotes pending devices at Lines 865-872 without clearing catalog caches, so the default search can omit an approved device for up to 15 minutes. Also, a read started before invalidation can finish afterward and cache its pre-write snapshot. Invalidate after catalog-affecting updates and use a generation check or post-fetch freshness check before caching.

🤖 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/store_client.py` around lines 597 - 600, Extend
catalog cache invalidation beyond the create path: update confirm_device and any
catalog-affecting status/update flows to invalidate after the write completes,
including pending-device promotion. In the catalog read/population logic,
protect against in-flight pre-invalidation fetches by checking a cache
generation or equivalent freshness token before storing results, so stale
snapshots cannot repopulate the cache.
custom_components/ha_washdata/profile_store.py (1)

6175-6183: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Replace-mode wipe misses reference_cycles when only "real_cycles" is selected with the default (reference) cycle destination.

The wipe block only clears reference_cycles when that category is itself ticked, and only clears past_cycles when cycle_destination == "real_history". But when a user selects only real_cycles with the default cycle_destination = "reference" (the common path per the JS wizard default), imported real cycles are routed into reference_cycles (Step 3's cycle_destination == "reference" branch) without that list ever being wiped in mode == "replace". The docstring promises "each ticked category is wiped and replaced from the file," but here reference_cycles silently behaves like merge (old entries persist alongside new ones, deduplicated only by source id) even though the user explicitly chose Replace.

🐛 Proposed fix
         if mode == "replace":
-            if "reference_cycles" in cats:
+            wipe_reference_cycles = (
+                "reference_cycles" in cats
+                or ("real_cycles" in cats and cycle_destination == "reference")
+            )
+            if wipe_reference_cycles:
                 self._data["reference_cycles"] = []
             if "real_cycles" in cats and cycle_destination == "real_history":
                 self._data["past_cycles"] = []
🤖 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/profile_store.py` around lines 6175 - 6183,
Update the replace-mode wipe logic so selecting only “real_cycles” also clears
the destination list that the import will populate: clear reference_cycles when
cycle_destination is "reference", and past_cycles when it is "real_history".
Preserve clearing reference_cycles for an explicitly selected reference_cycles
category.
custom_components/ha_washdata/www/ha-washdata-panel.js (1)

5701-5744: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Applying a setting or sweep value no longer refreshes the simulation, leaving the graph stale.

_pgApplyToSettings (clears overrides) and _pgApplySweepValue (sets the override to the applied value) both immediately update the threshold/param state and call this._render(), but neither re-runs the detail simulation. _pgDrawCanvas() derives the state band, event pins, and outcome/alerts panel entirely from this._pgDetail, which still reflects the pre-apply parameter values. Threshold lines and param inputs will show the new value while the state band/alerts/outcome still describe the old run, until the user manually presses Run again.

♻️ Suggested fix
         this._pgParamOverrides = {}; this._pgThreshStart = null; this._pgThreshStop = null;
         this._showToast(this._t('toast.settings_saved', {}, 'Settings saved; integration reloading'));
       } catch (e) {
         this._showToast(this._t('msg.toast_save_failed', {error: e.message || e}, 'Save failed: ' + (e.message || e)), 'error');
       }
     });
     this._render();
+    this._pgRerunDetail();
   }

and similarly in _pgApplySweepValue after staging the applied value.

🤖 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/www/ha-washdata-panel.js` around lines 5701 -
5744, Update _pgApplyToSettings and _pgApplySweepValue to rerun the detail
simulation after applying and updating the staged/live parameter values. Reuse
the existing simulation path triggered by the Run action, await it so
this._pgDetail reflects the new settings, then render the UI. Preserve the
current success, clearing, and error behavior.
🤖 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/playground.py`:
- Around line 1153-1160: Update the panel contract handling for the
stress_hit_cap alert so its label matches the dynamic h/m duration reported by
the backend. Replace the fixed “Hit 8 h safety cap” text with a generic
safety-cap label or parameterize it using the alert’s duration values, while
preserving the existing detail rendering.
- Around line 936-940: Update the stress idle override handling in the shown
synthesis flow to validate the numeric domain before assigning idle_w: convert
the override to a float, reject non-finite values including +inf, and enforce
the documented maximum bound while retaining the existing non-negative lower
clamp. Ensure invalid overrides cannot reach the synthetic power samples and
follow the established invalid-value behavior rather than leaving the stress
outcome unset.

In `@custom_components/ha_washdata/store_client.py`:
- Line 350: Update the list_brands method signature so include_pending and
page_size are keyword-only by placing * after the q parameter, while preserving
their existing defaults and behavior.

---

Outside diff comments:
In `@custom_components/ha_washdata/playground.py`:
- Around line 869-887: Update the trailing-window idle estimate around the
readings processing logic to account for timestamp gaps rather than weighting
samples equally. Detect gaps using datetime timestamps and either restrict
calculations to the contiguous tail after the latest gap or apply
duration-weighted percentile and variance calculations, while preserving the
existing fallback behavior for empty or insufficient data and returning
nonnegative idle and fluctuation values.
- Around line 978-983: Update the safety-cap branch around detector.force_end in
the playground flow to append a series sample at flush_ts after advancing the
cursor and forcing the detector to stop. Ensure the recorded timestamp matches
flush_ts so the reported duration does not extend beyond the final plotted
point.

In `@custom_components/ha_washdata/profile_store.py`:
- Around line 6175-6183: Update the replace-mode wipe logic so selecting only
“real_cycles” also clears the destination list that the import will populate:
clear reference_cycles when cycle_destination is "reference", and past_cycles
when it is "real_history". Preserve clearing reference_cycles for an explicitly
selected reference_cycles category.

In `@custom_components/ha_washdata/store_client.py`:
- Around line 323-348: Update the catalog query methods in
custom_components/ha_washdata/store_client.py:323-348 and :350-374 so
model_query and q filtering occur before applying page_size/limit and caching,
or fetch and cache a complete bounded catalog before filtering and paginating.
Preserve pagination behavior in the q path and ensure valid entries beyond the
initial query limit remain discoverable.
- Around line 597-600: Extend catalog cache invalidation beyond the create path:
update confirm_device and any catalog-affecting status/update flows to
invalidate after the write completes, including pending-device promotion. In the
catalog read/population logic, protect against in-flight pre-invalidation
fetches by checking a cache generation or equivalent freshness token before
storing results, so stale snapshots cannot repopulate the cache.

In `@custom_components/ha_washdata/www/ha-washdata-panel.js`:
- Around line 5701-5744: Update _pgApplyToSettings and _pgApplySweepValue to
rerun the detail simulation after applying and updating the staged/live
parameter values. Reuse the existing simulation path triggered by the Run
action, await it so this._pgDetail reflects the new settings, then render the
UI. Preserve the current success, clearing, and error behavior.
🪄 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: cb2b9f7b-ae16-4c9a-afc1-4331595f159e

📥 Commits

Reviewing files that changed from the base of the PR and between 5f9c83c and 5388595.

⛔ Files ignored due to path filters (36)
  • custom_components/ha_washdata/translations/panel/bg.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/bs.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/cs.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/da.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/de.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/el.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/en.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/es.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/et.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/fi.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/fr.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/hr.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/hu.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/is.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/it.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/ja.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/ko.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/lt.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/lv.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/mk.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/nb.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/nl.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/pl.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/pt-BR.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/pt.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/ro.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/ru.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sk.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sl.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sq.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sr-Latn.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sv.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/tr.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/uk.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/zh-Hans.json is excluded by !custom_components/ha_washdata/translations/**
  • tests/test_playground_stress_tail.py is excluded by !tests/**
📒 Files selected for processing (5)
  • CHANGELOG.md
  • custom_components/ha_washdata/playground.py
  • custom_components/ha_washdata/profile_store.py
  • custom_components/ha_washdata/store_client.py
  • custom_components/ha_washdata/www/ha-washdata-panel.js
📜 Review details
🧰 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/store_client.py
  • custom_components/ha_washdata/playground.py
  • custom_components/ha_washdata/profile_store.py
🪛 Ruff (0.15.21)
custom_components/ha_washdata/store_client.py

[warning] 350-350: Boolean-typed positional argument in function definition

(FBT001)


[warning] 350-350: Boolean default positional argument in function definition

(FBT002)

🔇 Additional comments (11)
CHANGELOG.md (1)

12-16: LGTM!

Also applies to: 18-18, 22-30

custom_components/ha_washdata/playground.py (1)

39-39: LGTM!

Also applies to: 89-95, 384-404, 448-471, 487-509, 530-571, 916-935, 941-977, 984-1017, 1116-1152, 1161-1162

custom_components/ha_washdata/store_client.py (1)

170-185: LGTM!

Also applies to: 202-240, 280-298, 389-407, 460-460, 517-517

custom_components/ha_washdata/profile_store.py (5)

6073-6083: Device-type gating formula now matches build_import_manifest.

device_type_match = (not source_dt) or (source_dt == local_device_type) aligns the apply-time gate with the manifest's analysis, resolving the previously flagged divergence.


1700-1778: LGTM!


3419-3449: LGTM!


5894-6032: LGTM!


6038-6338: Rest of async_import_data_selective reviewed — no further issues found.

Conflict resolution, dedup via id_pool/existing_ref_sources, and envelope-carry logic (Step 5) look correct; the only gap is the replace-mode wipe scope flagged above.

custom_components/ha_washdata/www/ha-washdata-panel.js (3)

5404-5422: LGTM! Accessible name added to the idle-termination toggle, resolving the prior review comment.


5446-5449: LGTM!


7661-7902: Selective export/import wizard tri-state tree and action handlers reviewed — no issues found.

Selection-state initialization, tri-state checkbox math, and the step machine (input → analyze → select) are internally consistent with the payload shape _wizSelectionPayload builds for the backend. The unvalidated source_device_type/local_device_type from the import manifest are correctly escaped before interpolation.

Also applies to: 10448-10587

Comment thread custom_components/ha_washdata/playground.py Outdated
Comment thread custom_components/ha_washdata/playground.py
Comment thread custom_components/ha_washdata/store_client.py Outdated
- playground: reject non-finite stress_idle_w override and clamp to
  [0, PLAYGROUND_STRESS_MAX_IDLE_W]; record a series sample at the force-stop
  timestamp so the plotted tail reaches the reported elapsed time
- panel: cap alert label is now the generic "Hit safety cap" (en + 34 langs),
  consistent with the dynamic duration in the detail
- store_client: list_brands/search_devices cache the full catalog (ceiling 500)
  and filter the prefix in memory, keeping entries past the old 60-row limit
  searchable; include_pending/page_size are keyword-only; confirm_device
  invalidates the catalog cache on a pending->approved promotion; catalog reads
  use a generation guard so an in-flight read cannot re-cache a pre-write snapshot
- profile_store: replace-mode wipe targets the list each ticked cycle category
  writes to, so real_cycles routed to the default reference destination wipes
  reference_cycles too
- tests: non-finite/huge override clamp, replace-mode reference-cycle wipe

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@3dg1luk43

Copy link
Copy Markdown
Owner Author

Thanks for the follow-up pass. Addressed in commit 1108c24:

Fixed

  • playground override domain (playground.py): non-finite (nan/inf) overrides are now rejected (auto-derived floor kept) and finite values clamped to [0, PLAYGROUND_STRESS_MAX_IDLE_W], so nothing absurd can reach the synthetic samples. Regression tests added (non-finite + huge).
  • cap label vs dynamic detail (lbl.pg_alert_stress_cap): the label is now the generic "Hit safety cap" (en + all 34 languages), so it no longer contradicts the dynamic h/m in the detail.
  • include_pending/page_size keyword-only on list_brands and search_devices (resolves the FBT001/FBT002 flags on the changed signatures).
  • forced-stop series point (playground.py): a _sample() is now recorded at the force-stop timestamp so the plotted tail reaches the reported elapsed time.
  • catalog completeness (store_client.py): both list_brands and search_devices now cache the full catalog (ceiling raised to 500, far above the real catalog) and filter the prefix in memory over the complete set, so entries past the old 60-row limit stay searchable for the whole TTL. A query still reads only the docs that exist, so no added reads for today's catalog.
  • cache invalidation (store_client.py): confirm_device now invalidates the catalog cache when it promotes a device pending -> approved, and every catalog read now captures a generation counter and only caches its result if no invalidation occurred while the query was in flight (closes the fill-after-invalidate race).
  • replace-mode wipe (profile_store.py): the replace wipe now targets the list each ticked cycle category actually writes to, so real_cycles routed to the default reference destination wipes reference_cycles too (not only when the reference_cycles category itself is ticked). Regression test added.

Intentionally not changed (rationale)

  • _derive_idle_level gap-awareness (playground.py): this returns a p7 (7th-percentile) standby floor, an order statistic that selects the low/between-burst readings and is inherently robust to how samples cluster in time. It is a diagnostic seed for a what-if replay, not a persisted energy/time integration, so the sample-count percentile is appropriate here (the guideline targets Σ P·dt). A naive contiguous-tail split would also misread change-only sensors, where a gap signals a stable (constant) power level i.e. standby, not an outage, and would discard valid standby readings. fluct_w is only cosmetic jitter for the synthetic tail.
  • re-simulate on Apply (ha-washdata-panel.js): commit 1ffd653 deliberately made the detail sim run only on an explicit Run, removing every _pgRerunDetail() auto-trigger including the one in _pgApplySweepValue, to avoid re-running the heavy replay on each parameter change. After an Apply the threshold lines/inputs reflect the new value and the user presses Run to see the new outcome; re-adding an auto-rerun here would revert that intentional design. Flagging for the maintainer to decide rather than overriding it.

@3dg1luk43

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
custom_components/ha_washdata/playground.py (2)

1006-1010: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Report manual override provenance accurately.

The non-finite override branch intentionally keeps the derived idle level, but this field still reports manual_override=True whenever an override was supplied. Track whether a finite override was actually applied, rather than checking only is not None.

Proposed fix
+        override_applied = False
         ...
         if math.isfinite(ov):
             idle_w = max(0.0, min(ov, PLAYGROUND_STRESS_MAX_IDLE_W))
+            override_applied = True
         ...
-                "manual_override": self.stress_idle_override is not None,
+                "manual_override": override_applied,
🤖 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/playground.py` around lines 1006 - 1010, Update
the stress outcome construction in the relevant override-handling flow to report
manual_override based on whether a finite override was applied, not merely
whether stress_idle_override is non-null. Track the finite-override application
state through the non-finite branch, while preserving the derived idle level
there, and use that state when populating self._stress_outcome.

39-39: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use NumPy for the playground numerical processing.

custom_components/ha_washdata/playground.py already imports NumPy, but these new calculations still use Python built-ins and math: sorted(window), manual mean/variance/square-root, math.isfinite(), and math.sin(). Port these to NumPy operations, e.g. np.sort, np.std, np.isnan/np.isfinite, and np.sin, so the new stress-tail path follows the Python-only NumPy rule.

Also applies to: 862-888, 936-945, 964-975

🤖 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/playground.py` at line 39, Update the numerical
processing in the playground stress-tail path to use the existing NumPy import:
replace sorted(window), manual mean/variance/square-root calculations,
math.isfinite(), and math.sin() with the corresponding NumPy operations. Remove
the now-unused math import while preserving the current calculation behavior.

Source: Coding guidelines

custom_components/ha_washdata/www/ha-washdata-panel.js (2)

8686-8690: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject non-finite (and negative) idle overrides at the input.

isNaN(v) does not catch Infinity — parseFloat("1e999") returns Infinity, which a type="number" field accepts, so _pgStressIdleW can become Infinity (and negatives pass despite min="0"). This contradicts the PR's stated goal of rejecting non-finite stress idle overrides. Use Number.isFinite and clamp to >= 0.

🛡️ Proposed guard
     if (pgStressIdleW) pgStressIdleW.addEventListener('input', () => {
       const v = parseFloat(pgStressIdleW.value);
-      this._pgStressIdleW = isNaN(v) ? null : v;
+      this._pgStressIdleW = Number.isFinite(v) ? Math.max(0, v) : null;
     });
🤖 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/www/ha-washdata-panel.js` around lines 8686 -
8690, Update the pgStressIdleW input handler to assign _pgStressIdleW only when
the parsed value is finite and non-negative; otherwise assign null. Replace the
current isNaN check with Number.isFinite and a v >= 0 condition, preserving the
existing parseFloat behavior.

10138-10138: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the registered set_options command type.

ha_washdata/ws_api.py registers ha_washdata/set_options, and ws_schema.py documents set_options; ws_set_options is only the backend function name. This call will hit unknown_command and make the settings revert error.

🐛 Proposed fix
-      this._ws({ type: `${_DOMAIN}/ws_set_options`, entry_id: eid, options: { [key]: val } })
+      this._ws({ type: `${_DOMAIN}/set_options`, entry_id: eid, options: { [key]: val } })
🤖 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/www/ha-washdata-panel.js` at line 10138, Update
the command type passed by the settings call in the panel’s _ws invocation from
the backend function name ws_set_options to the registered set_options command
type, while preserving the existing entry_id and options payload.
custom_components/ha_washdata/store_client.py (1)

324-387: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the shared cache-then-query-then-store pattern.

search_devices and list_brands duplicate the same cache-get → capture gen → run query → conditionally-cache-if-not-superseded sequence. The generation check in particular (if fetched is not None and gen == self._cache_gen) is subtle correctness logic that now lives in two places and must be kept in sync if the invalidation scheme evolves.

A small helper (e.g. _cached_query(key, ttl, build_sq)) that owns the get/gen-capture/run/put sequence would remove this duplication while keeping each caller's filter/sort/limit construction and in-memory prefix filter local.

🤖 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/store_client.py` around lines 324 - 387, The
cache-get, generation-capture, query, and conditional-cache sequence is
duplicated in search_devices and list_brands. Extract this shared behavior into
a helper such as _cached_query(key, ttl, build_sq) that builds or receives the
query, runs _run_query, returns an empty list on failure, and caches only
successful results when the captured generation still matches; update both
methods to use it while keeping their query construction and prefix filtering
local.
🤖 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/profile_store.py`:
- Around line 6181-6189: Update the replace-import flow around the shown
wipe_reference and wipe_past guards to apply reference_cycle_ids and
real_cycle_ids filtering to the source cycles before deciding whether to clear
local destinations. Reject the replacement when the resulting selected cycle set
for the targeted destination is empty, preserving existing data; add regression
coverage for empty filtered selections for both reference_cycles and
past_cycles.

---

Outside diff comments:
In `@custom_components/ha_washdata/playground.py`:
- Around line 1006-1010: Update the stress outcome construction in the relevant
override-handling flow to report manual_override based on whether a finite
override was applied, not merely whether stress_idle_override is non-null. Track
the finite-override application state through the non-finite branch, while
preserving the derived idle level there, and use that state when populating
self._stress_outcome.
- Line 39: Update the numerical processing in the playground stress-tail path to
use the existing NumPy import: replace sorted(window), manual
mean/variance/square-root calculations, math.isfinite(), and math.sin() with the
corresponding NumPy operations. Remove the now-unused math import while
preserving the current calculation behavior.

In `@custom_components/ha_washdata/store_client.py`:
- Around line 324-387: The cache-get, generation-capture, query, and
conditional-cache sequence is duplicated in search_devices and list_brands.
Extract this shared behavior into a helper such as _cached_query(key, ttl,
build_sq) that builds or receives the query, runs _run_query, returns an empty
list on failure, and caches only successful results when the captured generation
still matches; update both methods to use it while keeping their query
construction and prefix filtering local.

In `@custom_components/ha_washdata/www/ha-washdata-panel.js`:
- Around line 8686-8690: Update the pgStressIdleW input handler to assign
_pgStressIdleW only when the parsed value is finite and non-negative; otherwise
assign null. Replace the current isNaN check with Number.isFinite and a v >= 0
condition, preserving the existing parseFloat behavior.
- Line 10138: Update the command type passed by the settings call in the panel’s
_ws invocation from the backend function name ws_set_options to the registered
set_options command type, while preserving the existing entry_id and options
payload.
🪄 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: ffcfd19e-aafd-475f-8e2a-2ecdd0f7df5e

📥 Commits

Reviewing files that changed from the base of the PR and between 5388595 and 1108c24.

⛔ Files ignored due to path filters (37)
  • custom_components/ha_washdata/translations/panel/bg.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/bs.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/cs.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/da.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/de.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/el.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/en.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/es.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/et.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/fi.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/fr.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/hr.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/hu.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/is.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/it.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/ja.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/ko.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/lt.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/lv.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/mk.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/nb.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/nl.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/pl.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/pt-BR.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/pt.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/ro.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/ru.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sk.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sl.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sq.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sr-Latn.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sv.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/tr.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/uk.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/zh-Hans.json is excluded by !custom_components/ha_washdata/translations/**
  • tests/test_import_selective.py is excluded by !tests/**
  • tests/test_playground_stress_tail.py is excluded by !tests/**
📒 Files selected for processing (5)
  • custom_components/ha_washdata/const.py
  • custom_components/ha_washdata/playground.py
  • custom_components/ha_washdata/profile_store.py
  • custom_components/ha_washdata/store_client.py
  • custom_components/ha_washdata/www/ha-washdata-panel.js
📜 Review details
🧰 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/const.py
  • custom_components/ha_washdata/store_client.py
  • custom_components/ha_washdata/playground.py
  • custom_components/ha_washdata/profile_store.py
🔇 Additional comments (16)
custom_components/ha_washdata/playground.py (2)

89-96: LGTM!

Also applies to: 376-406, 479-511, 531-572, 917-935, 946-963, 976-991, 1124-1170


449-472: 🎯 Functional Correctness

No change needed. The chunked path already calls sim.run_stress_tail() when sim.stress_tail is true.

custom_components/ha_washdata/const.py (1)

998-1010: LGTM!

custom_components/ha_washdata/www/ha-washdata-panel.js (6)

1813-1814: LGTM!


5399-5426: LGTM!


5479-5481: LGTM!


6171-6184: LGTM!


5722-5743: LGTM!

Also applies to: 5951-5951, 10087-10087, 10448-10587


9999-10013: 🗄️ Data Integrity & Integration

No change needed for the export inventory shape.

get_export_inventory returns _scan_data(...) directly as the category map, while analyze_import wraps _scan_data(...) inside {categories: ...} — the two modal paths use the different shapes consistently.

custom_components/ha_washdata/store_client.py (7)

170-185: LGTM! Cache tuning constants and per-instance state are clear and well-documented.

Also applies to: 202-207


217-245: LGTM! The generation-counter approach correctly prevents an in-flight read from re-caching a pre-invalidation snapshot, and bounded eviction by soonest-to-expire is a sound simple policy.


285-303: LGTM! The None-on-failure contract is a real improvement over silently returning [] for a transient error, and all call sites in this file (get_profiles, get_cycles, search_devices, list_brands) have been updated to handle it.


324-359: 🎯 Functional Correctness | ⚡ Quick win

Cache key can collide across distinct queries when brand contains :.

The key is built by naive :-joining of fields: f"devices:{(brand or '').lower()}:{appliance_type or ''}:{int(include_pending)}:{page_size}". Since brand is free text stored on the community catalog (not restricted to an enum, unlike appliance_type), a brand containing a colon can produce the same formatted string as a different (brand, appliance_type) pair, e.g. brand="foo:bar", appliance_type="" and brand="foo", appliance_type="bar:" both yield "devices:foo:bar::0:500". That would serve one query's cached results for the other's search.

Use a tuple key (dicts support tuple keys, avoiding the ambiguous string-join entirely) instead of string concatenation.

🐛 Proposed fix using a tuple key
-        key = f"devices:{(brand or '').lower()}:{appliance_type or ''}:{int(include_pending)}:{page_size}"
+        key = ("devices", (brand or "").lower(), appliance_type or "", include_pending, page_size)

361-361: LGTM! include_pending/page_size are now keyword-only, addressing the earlier review feedback.


403-420: LGTM! get_config's cache-on-success-only behavior and the or [] fallbacks in get_profiles/get_cycles correctly adapt to _run_query's new None-on-failure contract.

Also applies to: 463-473, 511-530


610-613: LGTM! Catalog-cache invalidation is correctly scoped to writes that actually change catalog visibility (new brand/device creation, pending→approved promotion).

Also applies to: 886-888

Comment thread custom_components/ha_washdata/profile_store.py
- profile_store: apply the per-item id filters before the replace-mode guard and
  guard on the SELECTED (post-filter) cycle set, so a stale/empty id selection
  aborts instead of wiping a destination empty; Steps 2/3 reuse the selected sets
- playground: report manual_override provenance accurately (True only when a finite
  override was actually applied)
- panel: reject non-finite/negative idle overrides at the input (Number.isFinite +
  >= 0 clamp); fix the settings-revert action to use the registered set_options
  command type (was ws_set_options, which would hit unknown_command)
- store_client: extract the shared cache-get / generation-capture / query /
  conditional-store flow into _cached_catalog_query (one home for the in-flight
  generation guard, shared by list_brands and search_devices)
- tests: filtered-set replace guard, non-finite override provenance

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3dg1luk43 and others added 4 commits July 26, 2026 19:47
…rd change

The guard now skips only the sensor swap and applies the rest of the reload,
instead of returning early. That makes the full config-reload tail
(duration-ratio limits, state restoration, maintenance scheduler) run in the
blocked-state tests, which the minimal shared `manager` fixture never stubbed.
Add a `_stub_reload_ready` helper (the same stub set the idle-reload test uses)
and apply it in both blocked-state tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…etting

[FEATURE] Make the anti-wrinkle pulse gap configurable and show anti-wrinkle in the Playground
…325/#326)

The anti-wrinkle pulse-gap feature (PR #326) shipped 8 net-new panel keys in
en/de only (lbl.pg_anti_wrinkle, 5x pg_desc.anti_wrinkle_*,
setting.anti_wrinkle_idle_timeout.{label,doc}). Localize them into the
remaining 33 languages via domain-context translation, reusing each file's
existing anti-wrinkle terminology for consistency. Additive only; no machine
translation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ap guard + Playground (#325/#326)

Record PR #326 in the discrepancy/tech-debt register: the configurable
anti_wrinkle_idle_timeout setting, the maintainer-accepted sensor-swap reload
guard hardening (early-return -> skip-and-continue, blocked-state set widened),
and the Playground anti-wrinkle band + overrides. Prose note, table row, and a
full item-61 detail entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@3dg1luk43

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 26, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
custom_components/ha_washdata/ws_api.py (1)

3087-3113: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

export_data runs synchronously on the event loop, unlike the sibling inventory handler.

ws_get_export_inventory offloads manager.profile_store.get_export_inventory via hass.async_add_executor_job (Line 3016-3018), but here manager.profile_store.export_data(...) is called directly on the event loop (only the subsequent json.dumps is offloaded). For a device with a large cycle history this can block the loop while assembling the export payload.

♻️ Proposed fix: offload export_data to the executor
-        payload = manager.profile_store.export_data(
-            entry_data=dict(entry.data) if entry else {},
-            entry_options=dict(entry.options) if entry else {},
-            selection=selection,
-        )
+        payload = await hass.async_add_executor_job(
+            lambda: manager.profile_store.export_data(
+                entry_data=dict(entry.data) if entry else {},
+                entry_options=dict(entry.options) if entry else {},
+                selection=selection,
+            )
+        )
🤖 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/ws_api.py` around lines 3087 - 3113, Update
ws_export_config_selective so the synchronous manager.profile_store.export_data
call runs via hass.async_add_executor_job, matching ws_get_export_inventory;
pass entry_data, entry_options, and selection to the executor and retain the
existing JSON serialization and result handling.
custom_components/ha_washdata/__init__.py (1)

293-306: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale log text: says "to 3.7" but now migrates to minor_version 8.

The async_update_entry call was updated to set minor_version=8, but the following _log.info still hardcodes "to 3.7". This will produce misleading migration logs for any entry going through the bulk path (v1/v2/early-v3.x).

🐛 Fix the log message
     _log.info(
-        "Migrated WashData entry from version %s.%s to 3.7", version, minor_version
+        "Migrated WashData entry from version %s.%s to 3.8", version, minor_version
     )
🤖 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/__init__.py` around lines 293 - 306, Update the
migration log in the bulk migration path after async_update_entry to report the
current target version 3.8, matching the version and minor_version values passed
to hass.config_entries.async_update_entry.
🤖 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/frontend.py`:
- Around line 428-434: Update the task-draining logic in the teardown flow
around PANEL_TASK_KEY so asyncio.CancelledError remains ignored, but other
exceptions from awaiting the cancelled task are logged with their details before
teardown continues. Preserve the existing cancellation and non-propagating
behavior.
- Around line 376-410: Update async_register_panel to await the shared task
stored under PANEL_TASK_KEY through asyncio.shield, preventing cancellation of
one caller from cancelling the coalesced _do_register_panel task. Preserve the
existing False return for CancelledError only when the caller itself is
cancelled, while retaining current exception logging, task cleanup, and
successful result handling.

In `@custom_components/ha_washdata/manager.py`:
- Around line 1878-1913: Defer the initial sensor reading in the sensor-swap
branch of async_reload_config until after the detector configuration and
threshold fields are synchronized later in the reload. Ensure process_reading
uses the updated detector.config values when the new sensor’s current state is
processed, while preserving the existing listener setup and non-numeric-state
handling.
- Around line 271-286: Remove the unused STATE_USER_PAUSED entry from
_SENSOR_SWAP_BLOCKED_STATES, leaving STATE_PAUSED and the other detector states
unchanged.

---

Outside diff comments:
In `@custom_components/ha_washdata/__init__.py`:
- Around line 293-306: Update the migration log in the bulk migration path after
async_update_entry to report the current target version 3.8, matching the
version and minor_version values passed to
hass.config_entries.async_update_entry.

In `@custom_components/ha_washdata/ws_api.py`:
- Around line 3087-3113: Update ws_export_config_selective so the synchronous
manager.profile_store.export_data call runs via hass.async_add_executor_job,
matching ws_get_export_inventory; pass entry_data, entry_options, and selection
to the executor and retain the existing JSON serialization and result handling.
🪄 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: 835ea59a-a219-4642-ad9d-873990650f78

📥 Commits

Reviewing files that changed from the base of the PR and between 04fa7fd and c2f4285.

⛔ Files ignored due to path filters (43)
  • custom_components/ha_washdata/translations/panel/bg.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/bs.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/cs.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/da.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/de.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/el.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/en.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/es.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/et.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/fi.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/fr.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/hr.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/hu.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/is.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/it.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/ja.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/ko.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/lt.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/lv.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/mk.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/nb.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/nl.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/pl.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/pt-BR.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/pt.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/ro.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/ru.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sk.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sl.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sq.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sr-Latn.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/sv.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/tr.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/uk.json is excluded by !custom_components/ha_washdata/translations/**
  • custom_components/ha_washdata/translations/panel/zh-Hans.json is excluded by !custom_components/ha_washdata/translations/**
  • docs/internal/INTEGRATION_REFERENCE.md is excluded by !docs/**
  • playwright-tests/tests/feedback-review.spec.ts is excluded by !playwright-tests/**
  • tests/test_issue_331_relabel_resolves_feedback.py is excluded by !tests/**
  • tests/test_issue_68_anti_wrinkle.py is excluded by !tests/**
  • tests/test_manager.py is excluded by !tests/**
  • tests/test_migration_harness.py is excluded by !tests/**
  • tests/test_playground_overrides.py is excluded by !tests/**
  • tests/test_suggestion_stage2.py is excluded by !tests/**
📒 Files selected for processing (13)
  • CHANGELOG.md
  • custom_components/ha_washdata/__init__.py
  • custom_components/ha_washdata/config_flow.py
  • custom_components/ha_washdata/const.py
  • custom_components/ha_washdata/cycle_detector.py
  • custom_components/ha_washdata/frontend.py
  • custom_components/ha_washdata/learning.py
  • custom_components/ha_washdata/manager.py
  • custom_components/ha_washdata/playground.py
  • custom_components/ha_washdata/sensor.py
  • custom_components/ha_washdata/suggestion_engine.py
  • custom_components/ha_washdata/ws_api.py
  • custom_components/ha_washdata/www/ha-washdata-panel.js
💤 Files with no reviewable changes (2)
  • custom_components/ha_washdata/sensor.py
  • custom_components/ha_washdata/suggestion_engine.py
📜 Review details
🧰 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/config_flow.py
  • custom_components/ha_washdata/learning.py
  • custom_components/ha_washdata/__init__.py
  • custom_components/ha_washdata/frontend.py
  • custom_components/ha_washdata/cycle_detector.py
  • custom_components/ha_washdata/playground.py
  • custom_components/ha_washdata/manager.py
  • custom_components/ha_washdata/const.py
  • custom_components/ha_washdata/ws_api.py
🪛 Ruff (0.15.21)
custom_components/ha_washdata/frontend.py

[warning] 332-332: Do not catch blind exception: Exception

(BLE001)


[warning] 407-407: Do not catch blind exception: Exception

(BLE001)


[warning] 431-434: Use contextlib.suppress(asyncio.CancelledError, Exception) instead of try-except-pass

(SIM105)


[error] 433-434: try-except-pass detected, consider logging the exception

(S110)


[warning] 433-433: Do not catch blind exception: Exception

(BLE001)

custom_components/ha_washdata/playground.py

[warning] 128-128: Dynamically typed expressions (typing.Any) are disallowed in value

(ANN401)


[warning] 144-144: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 151-151: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (28)
custom_components/ha_washdata/const.py (2)

134-134: LGTM!

Also applies to: 382-387, 1002-1015


83-83: 🗄️ Data Integrity & Integration

No remaining running_dead_zone action needed.

The retired key is only declared for compatibility; migration strips persisted entries, and no active module imports DEFAULT_RUNNING_DEAD_ZONE, so it does not affect selective import/export.

custom_components/ha_washdata/playground.py (3)

39-39: LGTM!

Also applies to: 53-57, 69-73, 93-100, 154-172, 416-420, 436-439, 480-484, 503-506, 519-523, 533-544, 565-606, 896-923, 1062-1206, 1233-1240


951-1061: 🎯 Functional Correctness

No change needed for force-stop classification.

TerminationReason is a StrEnum, and captured records store the enum directly; the str(...) comparison resolves to "force_stopped" on both sides.


128-151: 📐 Maintainability & Code Quality

No lint policy requires a change here.

The repo does not configure Ruff, so these lines are not a repository lint failure.

			> Likely an incorrect or invalid review comment.
custom_components/ha_washdata/frontend.py (4)

19-19: LGTM!

Also applies to: 40-52


203-212: LGTM!


307-373: LGTM!


413-427: LGTM!

Also applies to: 436-451

CHANGELOG.md (1)

8-45: LGTM!

custom_components/ha_washdata/ws_api.py (7)

1057-1141: LGTM!


2069-2110: LGTM!


3002-3021: LGTM!


3131-3217: LGTM!


3224-3294: LGTM!


4189-4253: LGTM!


3065-3069: 🚀 Performance & Scalability

No protection change needed for ws_analyze_import.

ws_analyze_import already rejects payloads whose UTF-8 byte length exceeds _MAX_IMPORT_JSON_BYTES before calling json.loads(json_data).

custom_components/ha_washdata/www/ha-washdata-panel.js (5)

7830-7856: 🎯 Functional Correctness | ⚡ Quick win

Toggling the "Unlabelled" cycle group checkbox in the export/import tree silently does nothing.

data-prof="${_esc(g.profile)}" always serializes to a string (e.g. "null" or ""), but _wizGroupIds (below) compares that string against the raw object property gr.profile, which is the actual JS null for the unlabelled bucket. null === "null" (or null === "") is always false, so _wizGroupIds returns [] and the group-select checkbox becomes a no-op for any cycle group with no profile — even though the row correctly renders as checked/indeterminate and displays "Unlabelled" via g.profile || this._t(...).

🐛 Proposed fix: normalize the profile key on both sides
-          const key = `${cid}:${g.profile}`;
+          const key = `${cid}:${g.profile || ''}`;
           const expanded = m.expanded && m.expanded.has(key);
           ...
-              <input type="checkbox" data-maction="wiz-toggle-cycgroup" data-cat="${cid}" data-prof="${_esc(g.profile)}" ${gall ? 'checked' : ''} ${gsome ? 'data-indeterminate="1"' : ''}>
+              <input type="checkbox" data-maction="wiz-toggle-cycgroup" data-cat="${cid}" data-prof="${_esc(g.profile || '')}" ${gall ? 'checked' : ''} ${gsome ? 'data-indeterminate="1"' : ''}>
  _wizGroupIds(manifest, catId, prof) {
    const cat = (manifest.categories || {})[catId] || {};
-    const g = (cat.groups || []).find(gr => gr.profile === prof);
+    const g = (cat.groups || []).find(gr => (gr.profile || '') === prof);
    return g ? g.cycles.map(cy => String(cy.id)).filter(id => id !== 'null') : [];
  }

191-192: LGTM! Anti-wrinkle field additions, boolean/numeric param handling, and the idle-termination stress simulation UI (toggle, override field, canvas overlay, state-band/strip labels) are internally consistent and defensively coded against missing backend data.

Also applies to: 1509-1512, 5227-5234, 5371-5439, 5632-5635, 6008-6015, 6079-6091, 6197-6227, 6247-6248, 6463-6469, 6487-6491, 8705-8763


1697-1698: LGTM! Version/icon-url plumbing from get_constants into the header is consistently wired end-to-end.

Also applies to: 2357-2362, 3471-3496


7707-7829: LGTM! The rest of the selective export/import wizard (category tree, tri-state master toggle, export-select/import-wizard dispatch, mode/destination/conflict handling) is well-guarded and consistent aside from the _wizGroupIds issue flagged separately.

Also applies to: 7857-7948, 9229-9232, 10072-10086, 10254-10260, 10521-10660


2861-2864: LGTM! Removing the automatic _pgRerunDetail() calls here matches the author's documented decision to keep re-simulation explicit (user must press Run).

Also applies to: 5734-5756, 8680-8680, 10157-10161

custom_components/ha_washdata/__init__.py (2)

106-106: LGTM!

Also applies to: 141-141, 153-163, 421-439


153-163: 🗄️ Data Integrity & Integration

running_dead_zone config references are already removed.

custom_components/ha_washdata/config_flow.py (1)

150-150: LGTM!

custom_components/ha_washdata/cycle_detector.py (1)

126-128: LGTM!

Also applies to: 143-143, 309-310, 847-850

custom_components/ha_washdata/learning.py (1)

725-732: LGTM!

Also applies to: 806-884

custom_components/ha_washdata/manager.py (1)

91-218: LGTM!

Also applies to: 650-671, 742-746, 2021-2025, 2060-2060

Comment thread custom_components/ha_washdata/frontend.py
Comment thread custom_components/ha_washdata/frontend.py
Comment thread custom_components/ha_washdata/manager.py
Comment thread custom_components/ha_washdata/manager.py
… drop dead STATE_USER_PAUSED from sensor-swap guard

- asyncio.shield protects the shared _do_register_panel task so one
  setup_entry cancellation does not cancel the coalesced task for all
  other concurrent callers; re-raise CancelledError only when the caller
  itself is cancelled (current_task().cancelling() > 0)
- Split bare except-pass in async_unregister_panel drain: CancelledError
  stays silent, other exceptions are debug-logged for teardown visibility
- Remove STATE_USER_PAUSED from _SENSOR_SWAP_BLOCKED_STATES: CycleDetector
  never returns that value (user-pause keeps detector in STATE_PAUSED, which
  is already in the blocked set); update the parametrized test accordingly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@3dg1luk43

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 26, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
custom_components/ha_washdata/frontend.py (1)

398-415: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clear failed shared tasks so registration can retry.

When _do_register_panel returns False, this path leaves the completed task in hass.data[PANEL_TASK_KEY]; every later setup call re-awaits the same failed task, so the panel stays unavailable until unload or restart. Capture the task and remove the key on failure only if it is still the same task; use the same identity check in the exception cleanup to avoid deleting a newer replacement task.

Proposed fix
-    if PANEL_TASK_KEY not in hass.data:
-        hass.data[PANEL_TASK_KEY] = hass.async_create_task(
+    task = hass.data.get(PANEL_TASK_KEY)
+    if task is None:
+        task = hass.async_create_task(
             _do_register_panel(hass, src)
         )
+        hass.data[PANEL_TASK_KEY] = task

     try:
-        return bool(await asyncio.shield(hass.data[PANEL_TASK_KEY]))
+        result = bool(await asyncio.shield(task))
+        if not result and hass.data.get(PANEL_TASK_KEY) is task:
+            hass.data.pop(PANEL_TASK_KEY, None)
+        return result
...
-        hass.data.pop(PANEL_TASK_KEY, None)
+        if hass.data.get(PANEL_TASK_KEY) is task:
+            hass.data.pop(PANEL_TASK_KEY, None)
🤖 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/frontend.py` around lines 398 - 415, Update the
panel registration flow around PANEL_TASK_KEY to capture the shared task, remove
it from hass.data when the awaited result is False, and only remove it if the
stored task is still that same task. Apply the same identity-guarded cleanup in
the exception path so a newer replacement task is never deleted, while
preserving cancellation handling.
CHANGELOG.md (1)

42-42: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Move or relabel the historical 0.5.0 fix.

This bullet appears under the 0.5.3 release but states that the fix shipped in 0.5.0. Move it to the 0.5.0 section, or label it explicitly as a prior-release note, so users do not interpret it as a new 0.5.3 change.

🤖 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 `@CHANGELOG.md` at line 42, Relocate the overnight-cycle trimming bullet from
the 0.5.3 changelog section to the 0.5.0 section, or clearly label it as a
prior-release note while keeping its existing content intact.
🤖 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.

Outside diff comments:
In `@CHANGELOG.md`:
- Line 42: Relocate the overnight-cycle trimming bullet from the 0.5.3 changelog
section to the 0.5.0 section, or clearly label it as a prior-release note while
keeping its existing content intact.

In `@custom_components/ha_washdata/frontend.py`:
- Around line 398-415: Update the panel registration flow around PANEL_TASK_KEY
to capture the shared task, remove it from hass.data when the awaited result is
False, and only remove it if the stored task is still that same task. Apply the
same identity-guarded cleanup in the exception path so a newer replacement task
is never deleted, while preserving cancellation handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: baa780ca-4e42-41fc-ae1f-bb88744cc151

📥 Commits

Reviewing files that changed from the base of the PR and between c2f4285 and d6f2855.

⛔ Files ignored due to path filters (1)
  • tests/test_manager.py is excluded by !tests/**
📒 Files selected for processing (3)
  • CHANGELOG.md
  • custom_components/ha_washdata/frontend.py
  • custom_components/ha_washdata/manager.py
💤 Files with no reviewable changes (1)
  • custom_components/ha_washdata/manager.py
📜 Review details
🧰 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/frontend.py
🪛 Ruff (0.15.21)
custom_components/ha_washdata/frontend.py

[warning] 440-440: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (3)
custom_components/ha_washdata/frontend.py (2)

19-19: LGTM!

Also applies to: 40-52, 206-212, 277-336, 455-458


431-441: 🩺 Stability & Availability

No teardown race exists.

PANEL_TASK_KEY is only used to await in-flight registration; unloading does not replace it with a new setup task, so cancelling the popped task cannot delete a concurrent registration.

			> Likely an incorrect or invalid review comment.
CHANGELOG.md (1)

8-41: LGTM!

Also applies to: 43-45

… panel task cleanup

CHANGELOG: move the overnight-cycle trimming fix (#330) from the 0.5.3
section to 0.5.0 where it actually shipped; remove the confusing
"This fix shipped in 0.5.0" note that was only needed when it appeared
in a later release section.

frontend.py: capture the task reference before shielding so both cleanup
paths use an identity check (hass.data.get(PANEL_TASK_KEY) is task)
instead of an unconditional pop -- a concurrent newer task is never
deleted. Also clear the task when _do_register_panel returns False so
a subsequent setup_entry gets a fresh task and can retry, rather than
immediately re-awaiting the completed-failed task and getting False again.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@3dg1luk43

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 26, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
custom_components/ha_washdata/frontend.py (1)

352-356: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Localize the sidebar title.

sidebar_title="WashData" is user-visible text embedded in Python. Source it from strings.json and translations/en.json through the integration’s localization mechanism instead.

As per coding guidelines, “NO inline strings in Python code for UI text - All labels/descriptions must be in strings.json and translations/en.json.”

🤖 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/frontend.py` around lines 352 - 356, Update the
frontend panel registration in the relevant setup function to use the
integration’s localized translation key for sidebar_title instead of the inline
“WashData” text. Add the corresponding sidebar label to strings.json and
translations/en.json, following the existing localization structure.

Source: Coding guidelines

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

Outside diff comments:
In `@custom_components/ha_washdata/frontend.py`:
- Around line 352-356: Update the frontend panel registration in the relevant
setup function to use the integration’s localized translation key for
sidebar_title instead of the inline “WashData” text. Add the corresponding
sidebar label to strings.json and translations/en.json, following the existing
localization structure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ccf77e5e-b390-49d8-8b4d-f27910883837

📥 Commits

Reviewing files that changed from the base of the PR and between c2f4285 and 6e06281.

⛔ Files ignored due to path filters (1)
  • tests/test_manager.py is excluded by !tests/**
📒 Files selected for processing (3)
  • CHANGELOG.md
  • custom_components/ha_washdata/frontend.py
  • custom_components/ha_washdata/manager.py
💤 Files with no reviewable changes (1)
  • custom_components/ha_washdata/manager.py
📜 Review details
🧰 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/frontend.py
🪛 Ruff (0.15.21)
custom_components/ha_washdata/frontend.py

[warning] 410-410: Consider moving this statement to an else block

(TRY300)


[warning] 418-418: Do not catch blind exception: Exception

(BLE001)


[warning] 447-447: Do not catch blind exception: Exception

(BLE001)

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

203-212: LGTM!

Also applies to: 307-351, 357-372, 376-422, 425-465

CHANGELOG.md (1)

8-43: LGTM!

Also applies to: 400-401

@3dg1luk43
3dg1luk43 merged commit 50c0218 into main Jul 26, 2026
7 checks passed
This was referenced Aug 15, 2026
kdjkdjkdj pushed a commit to kdjkdjkdj/ha_washdata that referenced this pull request Oct 10, 2026
- playground: clamp negative stress_idle_w override to >= 0; drop redundant
  _derive_idle_level call
- playground: the three stress alert details are now translatable via
  detail_key/detail_params (en.json + all 34 languages) and rendered through _t;
  stress_hit_cap reports the actual elapsed duration instead of a hardcoded "8 h"
- profile_store: align async_import_data_selective device-type gate with
  build_import_manifest so the shown analysis and the applied gating cannot diverge
- panel: add an aria-label to the idle-termination toggle checkbox
- store_client: raise list_brands to a full-catalog cap so the cached in-memory
  prefix search stays complete as the brand catalog grows
- CHANGELOG: reword "idea by" to "proposed by"

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kdjkdjkdj pushed a commit to kdjkdjkdj/ha_washdata that referenced this pull request Oct 10, 2026
- playground: reject non-finite stress_idle_w override and clamp to
  [0, PLAYGROUND_STRESS_MAX_IDLE_W]; record a series sample at the force-stop
  timestamp so the plotted tail reaches the reported elapsed time
- panel: cap alert label is now the generic "Hit safety cap" (en + 34 langs),
  consistent with the dynamic duration in the detail
- store_client: list_brands/search_devices cache the full catalog (ceiling 500)
  and filter the prefix in memory, keeping entries past the old 60-row limit
  searchable; include_pending/page_size are keyword-only; confirm_device
  invalidates the catalog cache on a pending->approved promotion; catalog reads
  use a generation guard so an in-flight read cannot re-cache a pre-write snapshot
- profile_store: replace-mode wipe targets the list each ticked cycle category
  writes to, so real_cycles routed to the default reference destination wipes
  reference_cycles too
- tests: non-finite/huge override clamp, replace-mode reference-cycle wipe

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kdjkdjkdj pushed a commit to kdjkdjkdj/ha_washdata that referenced this pull request Oct 10, 2026
- profile_store: apply the per-item id filters before the replace-mode guard and
  guard on the SELECTED (post-filter) cycle set, so a stale/empty id selection
  aborts instead of wiping a destination empty; Steps 2/3 reuse the selected sets
- playground: report manual_override provenance accurately (True only when a finite
  override was actually applied)
- panel: reject non-finite/negative idle overrides at the input (Number.isFinite +
  >= 0 clamp); fix the settings-revert action to use the registered set_options
  command type (was ws_set_options, which would hit unknown_command)
- store_client: extract the shared cache-get / generation-capture / query /
  conditional-store flow into _cached_catalog_query (one home for the in-flight
  generation guard, shared by list_brands and search_devices)
- tests: filtered-set replace guard, non-finite override provenance

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kdjkdjkdj pushed a commit to kdjkdjkdj/ha_washdata that referenced this pull request Oct 10, 2026
- profile_store: extract _usable_reference_pairs (shared by _add_reference_cycle_nosave
  and the selective-import guard). The replace guard now requires at least one USABLE
  trace for reference-bound cycles, so an all-degenerate selection can no longer wipe a
  destination and then silently add nothing.
- store_client: coalesce concurrent cache misses with a per-key single-flight map in
  _cached_catalog_query, so simultaneous panel misses share one Firestore read.
- tests: all-degenerate reference-trace replace guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kdjkdjkdj pushed a commit to kdjkdjkdj/ha_washdata that referenced this pull request Oct 10, 2026
- profile_store: _usable_reference_pairs rejects malformed (non-list) trace rows before
  calling len(); the selective import is now non-corrupting under malformed input - each
  per-record add in Steps 2/3 skips-and-logs instead of aborting mid-refill,
  _add_reference_cycle_nosave ignores a malformed sampling_interval, and the real-history
  path skips records missing start_time/duration, so a replace can never wipe a
  destination and then fail part-way
- store_client: the single-flight catalog query is now generation-scoped (a query started
  before an invalidation is not joined by a later caller, which would get a pre-write
  snapshot) and cancellation-isolated (asyncio.shield + a self-cleaning slot) via a new
  _fetch_and_cache helper
- tests: concurrent-miss coalescing

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kdjkdjkdj pushed a commit to kdjkdjkdj/ha_washdata that referenced this pull request Oct 10, 2026
…dg1luk43#327)

CodeRabbit follow-up: a reference cycle exported after a prior import already carries
meta.source="store:<id>". The import re-prefixed it for both the dedup key and the stored
source, producing "store:store:<id>" and accumulating another "store:" on each round-trip,
which made the dedup key and stamped source drift apart. Added _bare_store_id to strip an
existing "store:" prefix so the provenance stays canonical (single prefix); re-imports and
round-trips now dedup correctly instead of double-weighting the envelope. Test added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kdjkdjkdj pushed a commit to kdjkdjkdj/ha_washdata that referenced this pull request Oct 10, 2026
- profile_store: use contextlib.suppress(TypeError, ValueError) for the malformed
  sampling_interval instead of try/except/pass (Ruff SIM105)
- profile_store: surface dropped records in the import summary via skipped_cycles, so a
  record removed by the per-record safety net is no longer invisible to the caller

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kdjkdjkdj pushed a commit to kdjkdjkdj/ha_washdata that referenced this pull request Oct 10, 2026
CodeRabbit follow-up: stripping only one "store:" prefix from the incoming id left
historical double-prefixed persisted values ("store:store:<id>") unchanged in
existing_ref_sources, so a later canonical "store:<id>" import missed dedup and added a
duplicate. _bare_store_id now strips EVERY leading "store:" and existing_ref_sources is
built via the canonical dedup key, so a legacy double-prefixed value and a fresh
single-prefixed import collapse to the same key. Test added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kdjkdjkdj pushed a commit to kdjkdjkdj/ha_washdata that referenced this pull request Oct 10, 2026
…dg1luk43#327)

CodeRabbit follow-up: the replace guard checked only presence of selected real cycles, but
Step 3 skips real-history records missing start_time/duration, so an all-malformed
selection would wipe past_cycles and refill nothing. Added _any_real_importable (the
real-history analog of _any_usable), requiring at least one record with both fields before
the wipe. Test added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kdjkdjkdj pushed a commit to kdjkdjkdj/ha_washdata that referenced this pull request Oct 10, 2026
…p set (PR 3dg1luk43#327)

CodeRabbit follow-up: building existing_ref_sources called .get() on any truthy meta, so a
legacy/raw-imported reference cycle whose meta is a list/string raised AttributeError and
blocked every selective import. Now guarded with an isinstance(meta, dict) check. Test added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kdjkdjkdj pushed a commit to kdjkdjkdj/ha_washdata that referenced this pull request Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants