SAFETY-136: a gentler loop on a slow meter until the last move shows #244

Merged
admin merged 2 commits from SAFETY-136 into release/1.0 2026-11-09 22:35:22 +01:00
Owner

SAFETY-136: less export when Home Assistant readings arrive 10-15 s late on a 5 s meter, through a calmer loop (not detection).

What. On a meter whose readings come 4 s or more apart (judged from the readings that arrive, no new option), the loop runs at half its gain after a move of 150 W or more, until the meter has fallen by half of that move or 2 cycles have passed. With no lag the meter shows the move at the next reading, so nothing waits. Meters of 1-2 s are untouched. app/settle.py, three small calls in cycle() / read_inputs().

Numbers (SAFETY-129 sim, real cycle(), 5 s meter; export Wh summed over flat, oven, kettle + hob, owner shape over the oven and over the heat pump, step down, step up):

base head
no lag 64.4 66.5 (import 62.0 -> 62.0, quarter peak 26 -> 26 W)
10 s late 339.9 (import 363.0) 189.0, -44 % (import 223.3)
15 s late 363.4 (import 280.9) 295.5, -19 % (import 287.7)

10 s and 15 s together: -31 %. At 15 s the oven and the owner's oven are not better; the kettle and the steps are. All 63 runs at 1 s and 2 s meters give the same commands as base; the recorded-day digests at 1 s and 2 s are identical. See DECISIONS/SAFETY-136-slow-meter-settle.md and the dev report for the candidate table and every scenario.

Changelog line (for the release, not committed: the version gate refuses an ## Unreleased block):
With a 5 s meter and Home Assistant readings 10 to 15 seconds late, control now follows the meter more gently after a change, so less power goes to the grid when loads switch on and off (SAFETY-136).

Tests: test_safety136.py. No option, no page text.

Mutants (MUTATION.md, purged copies, test_safety136.py; all red): slow() always true (1-2 s identity and the interval checks fail); WAIT_CYCLES 0 (9 fail); half gain removed (8 fail); readings counted one in five (the export and the waits fail).

Existing test changed: test_safety129.py part 1 "without the gate the export goes below -1 kW" compares with the tree before SAFETY-128; with this card the same ungated run already exports only -645 W (not -1 325 W), so that one run now switches the settle off (no_settle). Nothing else in it changed.

Local suite on this head (c50e227): goodwe-addon-checks:1, --network none, pin 3b4ff84 sim, GOODWE_REQUIRE_HWSIM=1, .gitea/ci/run-tests.sh: 103 files, 0 failed, 1 754 s.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC

SAFETY-136: less export when Home Assistant readings arrive 10-15 s late on a 5 s meter, through a calmer loop (not detection). **What.** On a meter whose readings come 4 s or more apart (judged from the readings that arrive, no new option), the loop runs at half its gain after a move of 150 W or more, until the meter has fallen by half of that move or 2 cycles have passed. With no lag the meter shows the move at the next reading, so nothing waits. Meters of 1-2 s are untouched. `app/settle.py`, three small calls in `cycle()` / `read_inputs()`. **Numbers** (SAFETY-129 sim, real `cycle()`, 5 s meter; export Wh summed over flat, oven, kettle + hob, owner shape over the oven and over the heat pump, step down, step up): | | base | head | |---|---|---| | no lag | 64.4 | 66.5 (import 62.0 -> 62.0, quarter peak 26 -> 26 W) | | 10 s late | 339.9 (import 363.0) | 189.0, -44 % (import 223.3) | | 15 s late | 363.4 (import 280.9) | 295.5, -19 % (import 287.7) | 10 s and 15 s together: -31 %. At 15 s the oven and the owner's oven are not better; the kettle and the steps are. All 63 runs at 1 s and 2 s meters give the same commands as base; the recorded-day digests at 1 s and 2 s are identical. See DECISIONS/SAFETY-136-slow-meter-settle.md and the dev report for the candidate table and every scenario. **Changelog line (for the release, not committed: the version gate refuses an `## Unreleased` block):** With a 5 s meter and Home Assistant readings 10 to 15 seconds late, control now follows the meter more gently after a change, so less power goes to the grid when loads switch on and off ([[SAFETY-136]]). Tests: `test_safety136.py`. No option, no page text. **Mutants** (MUTATION.md, purged copies, `test_safety136.py`; all red): `slow()` always true (1-2 s identity and the interval checks fail); `WAIT_CYCLES` 0 (9 fail); half gain removed (8 fail); readings counted one in five (the export and the waits fail). **Existing test changed:** `test_safety129.py` part 1 "without the gate the export goes below -1 kW" compares with the tree before SAFETY-128; with this card the same ungated run already exports only -645 W (not -1 325 W), so that one run now switches the settle off (`no_settle`). Nothing else in it changed. Local suite on this head (c50e227): `goodwe-addon-checks:1`, `--network none`, pin 3b4ff84 sim, `GOODWE_REQUIRE_HWSIM=1`, `.gitea/ci/run-tests.sh`: 103 files, 0 failed, 1 754 s. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
admin added 1 commit 2026-11-09 15:19:07 +01:00
SAFETY-136: a gentler loop on a slow meter until the last move shows
checks / unit (pull_request) Successful in 26m18s
infra-pin / fresh (pull_request) Successful in 2s
c50e2279dd
On a meter whose readings come 4 s or more apart, the loop runs at half its
gain after a move of 150 W or more, until the meter has fallen by half of the
move or 2 cycles have passed. With readings 10 s late on a 5 s meter the export
falls 44 % over oven, kettle and hob, owner shape, steps; 1-2 s meters are
untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
Author
Owner

Independent review (Opus) of c50e227: MERGE-READY if the owner accepts trade-offs T1-T3. Not merged.

Reproduced. I reran 157 sim runs per tree with the dev's harness. All 136 of the dev's runs give identical row hashes, base and head. Export sums at a 5 s meter:

  • no lag: 64.4 -> 66.5 Wh
  • 10 s late: 339.9 -> 189.0 Wh (-44 %)
  • 15 s late: 363.4 -> 295.5 Wh (-19 %)

Every 1-2 s run is identical to base, and the "readings late" seconds are identical in all 157 runs. CI run 1591 is green on c50e227. The local suite (hwsim 3b4ff84, --network none) gives 103 files, 0 failed.

Trade-offs (the owner decides):

  • T1, 15 s late is only -19 %. I tried four variants: 3 cycles, gain share 0.35, 3 cycles + 0.35, 3 cycles + 0.25. Each was worse at 15 s (301-335 Wh) and/or worse on the slow-battery harm runs. 3 cycles + 0.25 drives dead12_p5_n1_harm import to 850 Wh with a 1.5 kW quarter. A gain share of 0.35 helps only at 10 s (165 Wh). The current constants are the best point I found.
  • T2, harm runs (+17 Wh import, +11.5 Wh export, one quarter 51 -> 124 W). That costs under 1 cent on the owner's tariff. The highest quarter in any head run is 507 W, the same as base, so no run can raise a month peak. In real use the cost is bounded: at most 2 cycles at half gain, about 5-10 W on one quarter per event. The peak guard, readings late, charge and hold are claims outside compute(); they are not slowed, and any override ends the wait.
  • T3, "slow" from HA arrivals. HA gives no arrival signal for a repeated value (p1.py and freshness.py record the measurement), so the code cannot see a better one. I simulated a 1 s meter whose reading reaches the add-on only when it changes:
    • 20 rig scenarios (holds of 4-30 s, night, PV noon, 8 and 12 s dead times): same commands as base in all. On a 1 s meter the move shows at the next reading, so each wait ends before the half gain matters.
    • Recorded day at 1 s, N=1, with HA delivery: "slow" in 33 % of cycles, 83 cycles at half gain, 15 422 commands in both trees (4 016 with another value, at most 190 W apart), and a net cost of 0.61 EUR in both.

Findings (all low, none blocks):

  • F1 test_safety136.py part 5, last check: "a 1 s meter whose value changes every 5 s: not slow". That is true only when every reading arrives. On ha_signed the same meter is slow. Please reword it so it does not read as a production guarantee.
  • F2 ctl_cycle.py:745-746 and settle.py:67: two mutants survive test_safety136. One drops the reset on an override or mode trade. The other makes the "shown" test abs(), so it no longer checks the direction. Add one check each.
  • F3 settle.py:3,7 says "4 s or more" and "under 4 s", and DECISIONS says "1-3 s". The rule is a median gap over 3.75 s; make the wording match.
  • F4 DECISIONS "What it costs": "quarter at most 35 W, once 73 W". dead10_p6 also rises +40 W (-6 -> 34 W).
  • Info: no separate export limit exists. A same-direction disturbance during a wait is followed at half gain for at most 2 cycles, by design.

Full report: reports/SAFETY-136-review-report.md.

**Independent review (Opus) of c50e227: MERGE-READY if the owner accepts trade-offs T1-T3. Not merged.** **Reproduced.** I reran 157 sim runs per tree with the dev's harness. All 136 of the dev's runs give identical row hashes, base and head. Export sums at a 5 s meter: - no lag: 64.4 -> 66.5 Wh - 10 s late: 339.9 -> 189.0 Wh (-44 %) - 15 s late: 363.4 -> 295.5 Wh (-19 %) Every 1-2 s run is identical to base, and the "readings late" seconds are identical in all 157 runs. CI run 1591 is green on c50e227. The local suite (hwsim 3b4ff84, `--network none`) gives 103 files, 0 failed. **Trade-offs (the owner decides):** - **T1, 15 s late is only -19 %.** I tried four variants: 3 cycles, gain share 0.35, 3 cycles + 0.35, 3 cycles + 0.25. Each was worse at 15 s (301-335 Wh) and/or worse on the slow-battery harm runs. 3 cycles + 0.25 drives `dead12_p5_n1_harm` import to 850 Wh with a 1.5 kW quarter. A gain share of 0.35 helps only at 10 s (165 Wh). The current constants are the best point I found. - **T2, harm runs** (+17 Wh import, +11.5 Wh export, one quarter 51 -> 124 W). That costs under 1 cent on the owner's tariff. The highest quarter in any head run is 507 W, the same as base, so no run can raise a month peak. In real use the cost is bounded: at most 2 cycles at half gain, about 5-10 W on one quarter per event. The peak guard, readings late, charge and hold are claims outside `compute()`; they are not slowed, and any override ends the wait. - **T3, "slow" from HA arrivals.** HA gives no arrival signal for a repeated value (`p1.py` and `freshness.py` record the measurement), so the code cannot see a better one. I simulated a 1 s meter whose reading reaches the add-on only when it changes: - 20 rig scenarios (holds of 4-30 s, night, PV noon, 8 and 12 s dead times): **same commands as base in all**. On a 1 s meter the move shows at the next reading, so each wait ends before the half gain matters. - Recorded day at 1 s, N=1, with HA delivery: "slow" in 33 % of cycles, 83 cycles at half gain, 15 422 commands in both trees (4 016 with another value, at most 190 W apart), and a net cost of 0.61 EUR in both. **Findings (all low, none blocks):** - **F1** `test_safety136.py` part 5, last check: "a 1 s meter whose value changes every 5 s: not slow". That is true only when every reading arrives. On `ha_signed` the same meter *is* slow. Please reword it so it does not read as a production guarantee. - **F2** `ctl_cycle.py:745-746` and `settle.py:67`: two mutants survive `test_safety136`. One drops the reset on an override or mode trade. The other makes the "shown" test `abs()`, so it no longer checks the direction. Add one check each. - **F3** `settle.py:3,7` says "4 s or more" and "under 4 s", and DECISIONS says "1-3 s". The rule is a median gap over 3.75 s; make the wording match. - **F4** DECISIONS "What it costs": "quarter at most 35 W, once 73 W". `dead10_p6` also rises +40 W (-6 -> 34 W). - Info: no separate export limit exists. A same-direction disturbance during a wait is followed at half gain for at most 2 cycles, by design. Full report: `reports/SAFETY-136-review-report.md`.
admin added 1 commit 2026-11-09 21:16:51 +01:00
SAFETY-136 round 2: tests for override and direction, true wording
checks / unit (pull_request) Successful in 26m2s
infra-pin / fresh (pull_request) Successful in 2s
0f9c119bcf
F1 the on-change case is tested as slow, F2 override and direction checks,
F3 the 3.75 s rule in the texts, F4 the quarter figures and the live note.
No behaviour change.

Co-Authored-By: Claude <noreply@anthropic.com>
admin merged commit a8ecf171d6 into release/1.0 2026-11-09 22:35:22 +01:00
Author
Owner

Round 2 re-check (Opus): MERGED as a8ecf17 into release/1.0, pinned to head 0f9c119.

  • Only the settle.py docstring changed in app/ since c50e227. F1-F4 are fixed.
  • My two surviving mutants are now red: removing the override reset fails 2b, and the sign-blind "shown" test fails part 1.
  • Dropping only or mode_trade still survives. I left it without a test: a mode trade is a P_ARBITRAGE SET claim that outranks the loop, so the arbiter's target is not loop_w, moved is true and the wait resets anyway, and the loop's gain does not reach the wire during a trade.
  • CI 1593 checks / unit is green on 0f9c119. The local suite on 0f9c119 (hwsim 3b4ff84, --network none) gives 103 files, 0 failed.
**Round 2 re-check (Opus): MERGED** as a8ecf17 into `release/1.0`, pinned to head 0f9c119. - Only the `settle.py` docstring changed in `app/` since c50e227. F1-F4 are fixed. - My two surviving mutants are now red: removing the override reset fails 2b, and the sign-blind "shown" test fails part 1. - Dropping only `or mode_trade` still survives. I left it without a test: a mode trade is a P_ARBITRAGE SET claim that outranks the loop, so the arbiter's target is not `loop_w`, `moved` is true and the wait resets anyway, and the loop's gain does not reach the wire during a trade. - CI 1593 `checks / unit` is green on 0f9c119. The local suite on 0f9c119 (hwsim 3b4ff84, `--network none`) gives 103 files, 0 failed.
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/goodwe-addon#244