SAFETY-83: peak guard outranks the maintenance drain at N=1
Defect. At N=1 the drain is the site SET at P_MAINTENANCE (60). The peak floor is a limit at P_PEAK (50), and arbiter clause 2 applies only limits at or above the winner. So the floor was ignored and the house imported what the drain left (PR #154 review, point 2).
Fix.Controller._peak_over_drain() (main.py): at N=1, while a DRAIN owns the setpoint and the peak claim's lo is above the drain rate, that peak claim is refiled at P_MAINTENANCE, so the arbiter applies it. Target = the floor, inside the safety limits. A safety SET (100) still parks everything. Where the floor is at or below the drain rate, nothing changes. N=2 is untouched (the SAFETY-78 drain floor already does this there).
The order in effect is now the same at N=1 and N=2: safety limits and the SAFETY-73 refusal > peak guard (SAFETY-79 margin) > maintenance > modes > trade > loop (see the comment beside the priorities in arbiter.py, and DECISIONS/SAFETY-83).
Banked quarters, N=1, real Controller (test_safety83 part 1)
limit
drain
margin
house 5000
6000
7000
2500
2000
0
3000 → 2500
4000 → 2500
5000 → 2500
2500
2000
50
3000 → 2450
4000 → 2450
5000 → 2450
3500
2500
0
2500 → 2500
3500 → 3500 (first quarter 3514 → 3500)
4500 → 3500
3500
2500
50
2500 → 2500
3500 → 3450
4500 → 3450
(before → after, steady quarter; W). All quarters ≤ aim + 1 W. The drain stays in phase drain and the battery gives at least the drain rate on every cycle. With a falling SoC it still reaches its floor and moves to charge.
Import never worse than without the drain (owner's settings, part 2): houses 1200, 3000, 5000, 6500 and 7000 W. Each quarter with the drain banks ≤ max(aim, the same quarter without the drain). Below the drain rate the battery still gives 2500 W and exports the rest (owner decision 2026-09-29).
Identity (tools/safety78_identity.py --dump on 35cdffe, --diff here)
n1_drain: 480 cycles, 96 changed, 96 with the guard above the drain, 0 others, first changed t=1920 s
n1_drain_modes: 480 cycles, 96 changed, 96 with the guard above the drain, 0 others, first changed t=1920 s
n1_drain_owner: 480 cycles, 288 changed, 96 with the guard above the drain, 192 others (0 with a different target or command; the rest differ only in claims, integrator, log length, newest log line), first changed t=960 s
n2_drain: 480 cycles, 0 changed
n2_drain_owner: 480 cycles, 0 changed
n1_drain_owner is new (limit 3500, drain 2500, margin 50, a house 1200 → 3000 → 6500 → 3000 → -1500 W). The 192 "others" follow the 6500 W episode. Commands and targets are identical again. Only the idle loop's integrator and the loop claim (one cycle), and the peak guard's log line ("heading for 2050 W" instead of "2367 W", since the quarter was held lower), differ. Old N=1 digests on 35cdffe still match 55fcfc4a…/93867eb4… with the extended tool. test_safety78 part 5 is re-pinned, and N=2 is pinned to the 35cdffe values. test_safety83 part 4 checks the rule on every cycle: refiled and target = floor exactly where the guard wanted more; elsewhere the peak stays at P_PEAK and target = drain rate.
Not changed (recorded in DECISIONS/SAFETY-83)
Between the drain rate and the aim, an N=1 house still imports what the drain leaves (kept by the identity requirement). At N=2 the loop covers it.
A maintenance HOLD (0 W, battery full) still outranks the peak guard at N=1. This is pinned by a test. Owner question.
Checks
Full suite locally in goodwe-addon-checks:1: 53/53 (test_freeze on the local stub, since the sibling sim is not local). No TEST-23. No live hosts.
Mutations
Fresh copies, purged with tools/mutation_run.sh, graded over test_safety83.py and test_safety78.py:
#
mutation (main.py _peak_over_drain)
result
M1
call removed (the defect)
killed: s83 parts 1–4, s78 part 5
M2
c.lo > rate → c.lo >= rate
killed by the boundary check (s83 part 4). Survived before that check was added.
M3
c.lo > rate → c.lo > 0 (refile whenever engaged)
killed: s83 part 4, s78 part 5
M4
N=1 guard dropped (refile at N=2 too)
killed: s83 part 4 (N=2), s78 part 5
M5
refiled at P_SAFETY (100)
killed: s83 part 4 (the claim check, the boundary, and "a refiled floor stays under a safety SET"), s78 part 5
M6
phase check dropped (a HOLD refiled too)
killed: s83 part 4 HOLD check
M7
refiled at P_PEAK+1 (51, below the drain)
killed: s83 parts 1–4, s78 part 5
M8
rate = 0
killed: s83 part 4, s78 part 5
M9
refiled as a SET at the floor, not a limit
killed: s83 parts 1 and 4, s78 part 5
The lost-reading run in part 3 alone did not kill M5 (nothing is refiled there), so a direct check was added.
## SAFETY-83: peak guard outranks the maintenance drain at N=1
**Defect.** At N=1 the drain is the site SET at `P_MAINTENANCE` (60). The peak floor is a limit at `P_PEAK` (50), and arbiter clause 2 applies only limits at or above the winner. So the floor was ignored and the house imported what the drain left (PR #154 review, point 2).
**Fix.** `Controller._peak_over_drain()` (main.py): at N=1, while a DRAIN owns the setpoint and the peak claim's `lo` is above the drain rate, that peak claim is refiled at `P_MAINTENANCE`, so the arbiter applies it. Target = the floor, inside the safety limits. A safety SET (100) still parks everything. Where the floor is at or below the drain rate, nothing changes. N=2 is untouched (the SAFETY-78 `drain` floor already does this there).
The order in effect is now the same at N=1 and N=2: safety limits and the SAFETY-73 refusal > peak guard (SAFETY-79 margin) > maintenance > modes > trade > loop (see the comment beside the priorities in arbiter.py, and DECISIONS/SAFETY-83).
### Banked quarters, N=1, real Controller (test_safety83 part 1)
| limit | drain | margin | house 5000 | 6000 | 7000 |
|---|---|---|---|---|---|
| 2500 | 2000 | 0 | 3000 → **2500** | 4000 → **2500** | 5000 → **2500** |
| 2500 | 2000 | 50 | 3000 → **2450** | 4000 → **2450** | 5000 → **2450** |
| 3500 | 2500 | 0 | 2500 → 2500 | 3500 → **3500** (first quarter 3514 → 3500) | 4500 → **3500** |
| 3500 | 2500 | 50 | 2500 → 2500 | 3500 → **3450** | 4500 → **3450** |
(before → after, steady quarter; W). All quarters ≤ aim + 1 W. The drain stays in phase `drain` and the battery gives at least the drain rate on every cycle. With a falling SoC it still reaches its floor and moves to charge.
**Import never worse than without the drain** (owner's settings, part 2): houses 1200, 3000, 5000, 6500 and 7000 W. Each quarter with the drain banks ≤ max(aim, the same quarter without the drain). Below the drain rate the battery still gives 2500 W and exports the rest (owner decision 2026-09-29).
### Identity (`tools/safety78_identity.py --dump` on 35cdffe, `--diff` here)
```
n1_drain: 480 cycles, 96 changed, 96 with the guard above the drain, 0 others, first changed t=1920 s
n1_drain_modes: 480 cycles, 96 changed, 96 with the guard above the drain, 0 others, first changed t=1920 s
n1_drain_owner: 480 cycles, 288 changed, 96 with the guard above the drain, 192 others (0 with a different target or command; the rest differ only in claims, integrator, log length, newest log line), first changed t=960 s
n2_drain: 480 cycles, 0 changed
n2_drain_owner: 480 cycles, 0 changed
```
`n1_drain_owner` is new (limit 3500, drain 2500, margin 50, a house 1200 → 3000 → 6500 → 3000 → -1500 W). The 192 "others" follow the 6500 W episode. Commands and targets are identical again. Only the idle loop's integrator and the loop claim (one cycle), and the peak guard's log line ("heading for 2050 W" instead of "2367 W", since the quarter was held lower), differ. Old N=1 digests on 35cdffe still match 55fcfc4a…/93867eb4… with the extended tool. test_safety78 part 5 is re-pinned, and N=2 is pinned to the 35cdffe values. test_safety83 part 4 checks the rule on every cycle: refiled and target = floor exactly where the guard wanted more; elsewhere the peak stays at P_PEAK and target = drain rate.
### Not changed (recorded in DECISIONS/SAFETY-83)
- Between the drain rate and the aim, an N=1 house still imports what the drain leaves (kept by the identity requirement). At N=2 the loop covers it.
- A maintenance HOLD (0 W, battery full) still outranks the peak guard at N=1. This is pinned by a test. Owner question.
### Checks
Full suite locally in `goodwe-addon-checks:1`: 53/53 (test_freeze on the local stub, since the sibling sim is not local). No TEST-23. No live hosts.
### Mutations
Fresh copies, purged with `tools/mutation_run.sh`, graded over test_safety83.py and test_safety78.py:
| # | mutation (main.py `_peak_over_drain`) | result |
|---|---|---|
| M1 | call removed (the defect) | killed: s83 parts 1–4, s78 part 5 |
| M2 | `c.lo > rate` → `c.lo >= rate` | killed by the boundary check (s83 part 4). Survived before that check was added. |
| M3 | `c.lo > rate` → `c.lo > 0` (refile whenever engaged) | killed: s83 part 4, s78 part 5 |
| M4 | N=1 guard dropped (refile at N=2 too) | killed: s83 part 4 (N=2), s78 part 5 |
| M5 | refiled at P_SAFETY (100) | killed: s83 part 4 (the claim check, the boundary, and "a refiled floor stays under a safety SET"), s78 part 5 |
| M6 | phase check dropped (a HOLD refiled too) | killed: s83 part 4 HOLD check |
| M7 | refiled at P_PEAK+1 (51, below the drain) | killed: s83 parts 1–4, s78 part 5 |
| M8 | `rate = 0` | killed: s83 part 4, s78 part 5 |
| M9 | refiled as a SET at the floor, not a limit | killed: s83 parts 1 and 4, s78 part 5 |
The lost-reading run in part 3 alone did not kill M5 (nothing is refiled there), so a direct check was added.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01UBtqPrG1mDLXzTEBnhEkkV
At N=1 the drain is the site SET at P_MAINTENANCE (60), so the peak floor
(P_PEAK, 50) was not applied and the house imported what the drain left.
Where the floor asks for more than the drain rate, main.py now refiles the
peak claim at P_MAINTENANCE, so it wins; safety SETs still park everything.
Nothing else changes: safety78_identity --diff against 35cdffe changes only
cycles where the guard wanted more than the drain; N=2 is byte-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A floor exactly at the drain rate stays at P_PEAK (kills `lo >= rate`), and
part 4's N=2 check no longer shares its failure list with N=1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kills M5 (refiled at P_SAFETY) on its own sentence, not only through the
per-cycle claim check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewed at 56cafefa7. The fix is correct. Three findings, all about the branch or its tests. The dev agent is gone, so I am pushing the fixes to this branch myself.
Verified (no action)
Safety SET wins. A SET at 100 makes clause 2 ignore every limit below 100, so the refiled claim at 60 cannot move a stop. The direct check kills a refile at P_SAFETY.
No lo > hi clash with the safety limits. At N=1 the only P_SAFETY limits are device and supervised. The peak floor is already capped at site_rating_w, which is the smaller of the two, so the refiled lo stays under their hi (part 3, max_w 3000). pack and refused are filed at N>1 only.
Floor at or below the drain rate: nothing changes (boundary check, identity diff).
N=2: the len(self.inverters) != 1 return plus part 4 and the s78 part 5 digests.
Status page: no new strings. The Why line becomes the existing "…, raised for …" (why_raised).
Docs: sized to purpose. DOCS.md gets one sentence and history is in DECISIONS.
Quarter edge: the refile has no state and is recomputed every cycle. A probe with steps at 700, 850 and 880 s into each quarter gave the same quarters with and without the fix.
Findings
Branch is behind release/1.0. It does not contain #156 (OPT-11 P4, 1145a93). Fix: merge release/1.0 into the branch the normal way, then run the full suite again.
No test for a month peak banked above the limit during a drain. Owner decision (SAFETY-79): the aim is quarter_ceiling_w() − margin, under a banked peak too. With the aim changed to cap − margin (the banked peak ignored), test_safety83 and test_safety78 still passed. Fix: new part 1 check. With 4500 W banked, a 7500 W house now banks 4450 W (was 5000 W before the fix), and a 6500 W house stays at 4000 W.
No test that the guard lifting a drain raises no false 'Load too high' alarm (SAFETY-81). The steady-load runs alone never saturate the guard, so a "no alarm" check there proves nothing (ALARM_HOLD_S → 0 survived it). Fix: check that the part 1 runs raise no alarm. Add a spiky house (5 s spikes to 11000 W that saturate the guard) that must raise no alarm either. Also check that the max_w 3000 overload in part 3 does raise one.
Mutations (mine, fresh copies, graded on test_safety83 + test_safety78)
mutation
result
refile call removed
killed (s83 ×6, s78 part 5)
refiled at P_PEAK
killed (s83 parts 1–4, s78)
refiled at P_SAFETY
killed (s83 part 4 claim, boundary, safety-SET check; s78)
refiled at P_MAINTENANCE−1
killed (s83 parts 1–4, s78)
refiled at P_MAINTENANCE+1
killed by the claim pins (s83 part 4, s78). Same behaviour at N=1
lo > rate condition dropped
killed (s83 part 4, boundary; s78)
lo > rate → lo >= rate
killed (boundary check only)
applied at N=2 too
killed (s83 part 4 N=2, s78)
margin ignored (margin_w=0.0 into decide)
killed (s83 parts 1, 2; s78)
margin added to the condition (lo > rate + 50)
killed (boundary check)
phase check dropped (HOLD refiled)
killed (s83 HOLD check)
owns_setpoint check dropped
survived. Equivalent: every DRAIN result has owns_setpoint=True (maintenance.py). Kept as a mirror of _drain_floor()
rate = 0
killed (s83, s78)
aim ignores banked peak (peakguard)
survived → finding 2. Killed by the new check (also by test_peak_margin)
ALARM_HOLD_S → 0
survived → finding 3. Killed by the new spiky check
load_too_high always True
survives s83 (the hold covers it). Killed by test_safety81
## Review round 1 (SAFETY-83)
Reviewed at 56cafefa7. The fix is correct. Three findings, all about the branch or its tests. The dev agent is gone, so I am pushing the fixes to this branch myself.
### Verified (no action)
- **Safety SET wins.** A SET at 100 makes clause 2 ignore every limit below 100, so the refiled claim at 60 cannot move a stop. The direct check kills a refile at P_SAFETY.
- **No `lo > hi` clash with the safety limits.** At N=1 the only P_SAFETY limits are `device` and `supervised`. The peak floor is already capped at `site_rating_w`, which is the smaller of the two, so the refiled `lo` stays under their `hi` (part 3, max_w 3000). `pack` and `refused` are filed at N>1 only.
- **Floor at or below the drain rate:** nothing changes (boundary check, identity diff).
- **N=2:** the `len(self.inverters) != 1` return plus part 4 and the s78 part 5 digests.
- **Status page:** no new strings. The Why line becomes the existing "…, raised for …" (`why_raised`).
- **Docs:** sized to purpose. DOCS.md gets one sentence and history is in DECISIONS.
- **Quarter edge:** the refile has no state and is recomputed every cycle. A probe with steps at 700, 850 and 880 s into each quarter gave the same quarters with and without the fix.
### Findings
1. **Branch is behind `release/1.0`.** It does not contain #156 (OPT-11 P4, 1145a93). Fix: merge `release/1.0` into the branch the normal way, then run the full suite again.
2. **No test for a month peak banked above the limit during a drain.** Owner decision (SAFETY-79): the aim is `quarter_ceiling_w() − margin`, under a banked peak too. With the aim changed to `cap − margin` (the banked peak ignored), test_safety83 and test_safety78 still passed. Fix: new part 1 check. With 4500 W banked, a 7500 W house now banks 4450 W (was 5000 W before the fix), and a 6500 W house stays at 4000 W.
3. **No test that the guard lifting a drain raises no false 'Load too high' alarm (SAFETY-81).** The steady-load runs alone never saturate the guard, so a "no alarm" check there proves nothing (ALARM_HOLD_S → 0 survived it). Fix: check that the part 1 runs raise no alarm. Add a spiky house (5 s spikes to 11000 W that saturate the guard) that must raise no alarm either. Also check that the max_w 3000 overload in part 3 does raise one.
### Mutations (mine, fresh copies, graded on test_safety83 + test_safety78)
| mutation | result |
|---|---|
| refile call removed | killed (s83 ×6, s78 part 5) |
| refiled at P_PEAK | killed (s83 parts 1–4, s78) |
| refiled at P_SAFETY | killed (s83 part 4 claim, boundary, safety-SET check; s78) |
| refiled at P_MAINTENANCE−1 | killed (s83 parts 1–4, s78) |
| refiled at P_MAINTENANCE+1 | killed by the claim pins (s83 part 4, s78). Same behaviour at N=1 |
| `lo > rate` condition dropped | killed (s83 part 4, boundary; s78) |
| `lo > rate` → `lo >= rate` | killed (boundary check only) |
| applied at N=2 too | killed (s83 part 4 N=2, s78) |
| margin ignored (`margin_w=0.0` into decide) | killed (s83 parts 1, 2; s78) |
| margin added to the condition (`lo > rate + 50`) | killed (boundary check) |
| phase check dropped (HOLD refiled) | killed (s83 HOLD check) |
| `owns_setpoint` check dropped | survived. Equivalent: every DRAIN result has `owns_setpoint=True` (maintenance.py). Kept as a mirror of `_drain_floor()` |
| `rate = 0` | killed (s83, s78) |
| aim ignores banked peak (peakguard) | **survived** → finding 2. Killed by the new check (also by test_peak_margin) |
| ALARM_HOLD_S → 0 | **survived** → finding 3. Killed by the new spiky check |
| `load_too_high` always True | survives s83 (the hold covers it). Killed by test_safety81 |
test_safety83 part 1: under a month peak banked above the limit the drain
is lifted to peak - margin, and a drain the guard raises (steady or with
5 s spikes that saturate it) gives no 'Load too high' alarm; the part 3
overload still does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixed. Merged release/1.0 (#156, 1145a93) into the branch in 79b9402, with no conflicts. Full suite on the merged tree: 54/54 test files pass in goodwe-addon-checks:1. The s78 part 5 digests and the s83 identity checks are unchanged.
Fixed (bc6d744). New test_safety83 part 1 check: with 4500 W banked (cap 3500, margin 50, drain 2500), a 7500 W house banks 4450 W (5000 W before the fix), and a 6500 W house stays at 4000 W. With the aim changed to cap − margin this check fails.
Fixed (bc6d744). No 'Load too high' alarm on any part 1 run, or on a 6000 W house with 5 s spikes to 11000 W that saturate the guard. The part 3 overload (max_w 3000) must still raise the alarm. With ALARM_HOLD_S set to 0 the new check fails; with the alarm switched off, the overload check fails.
Nothing is open. I will merge once the required check is green on bc6d744.
## Review round 2: fixes pushed (head bc6d744)
1. **Fixed.** Merged `release/1.0` (#156, 1145a93) into the branch in 79b9402, with no conflicts. Full suite on the merged tree: 54/54 test files pass in `goodwe-addon-checks:1`. The s78 part 5 digests and the s83 identity checks are unchanged.
2. **Fixed** (bc6d744). New test_safety83 part 1 check: with 4500 W banked (cap 3500, margin 50, drain 2500), a 7500 W house banks 4450 W (5000 W before the fix), and a 6500 W house stays at 4000 W. With the aim changed to `cap − margin` this check fails.
3. **Fixed** (bc6d744). No 'Load too high' alarm on any part 1 run, or on a 6000 W house with 5 s spikes to 11000 W that saturate the guard. The part 3 overload (max_w 3000) must still raise the alarm. With ALARM_HOLD_S set to 0 the new check fails; with the alarm switched off, the overload check fails.
Nothing is open. I will merge once the required check is green on bc6d744.
admin
merged commit 9d5b7d129a into release/1.02026-09-30 14:58:42 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
SAFETY-83: peak guard outranks the maintenance drain at N=1
Defect. At N=1 the drain is the site SET at
P_MAINTENANCE(60). The peak floor is a limit atP_PEAK(50), and arbiter clause 2 applies only limits at or above the winner. So the floor was ignored and the house imported what the drain left (PR #154 review, point 2).Fix.
Controller._peak_over_drain()(main.py): at N=1, while a DRAIN owns the setpoint and the peak claim'slois above the drain rate, that peak claim is refiled atP_MAINTENANCE, so the arbiter applies it. Target = the floor, inside the safety limits. A safety SET (100) still parks everything. Where the floor is at or below the drain rate, nothing changes. N=2 is untouched (the SAFETY-78drainfloor already does this there).The order in effect is now the same at N=1 and N=2: safety limits and the SAFETY-73 refusal > peak guard (SAFETY-79 margin) > maintenance > modes > trade > loop (see the comment beside the priorities in arbiter.py, and DECISIONS/SAFETY-83).
Banked quarters, N=1, real Controller (test_safety83 part 1)
(before → after, steady quarter; W). All quarters ≤ aim + 1 W. The drain stays in phase
drainand the battery gives at least the drain rate on every cycle. With a falling SoC it still reaches its floor and moves to charge.Import never worse than without the drain (owner's settings, part 2): houses 1200, 3000, 5000, 6500 and 7000 W. Each quarter with the drain banks ≤ max(aim, the same quarter without the drain). Below the drain rate the battery still gives 2500 W and exports the rest (owner decision 2026-09-29).
Identity (
tools/safety78_identity.py --dumpon35cdffe,--diffhere)n1_drain_owneris new (limit 3500, drain 2500, margin 50, a house 1200 → 3000 → 6500 → 3000 → -1500 W). The 192 "others" follow the 6500 W episode. Commands and targets are identical again. Only the idle loop's integrator and the loop claim (one cycle), and the peak guard's log line ("heading for 2050 W" instead of "2367 W", since the quarter was held lower), differ. Old N=1 digests on35cdffestill match 55fcfc4a…/93867eb4… with the extended tool. test_safety78 part 5 is re-pinned, and N=2 is pinned to the35cdffevalues. test_safety83 part 4 checks the rule on every cycle: refiled and target = floor exactly where the guard wanted more; elsewhere the peak stays at P_PEAK and target = drain rate.Not changed (recorded in DECISIONS/SAFETY-83)
Checks
Full suite locally in
goodwe-addon-checks:1: 53/53 (test_freeze on the local stub, since the sibling sim is not local). No TEST-23. No live hosts.Mutations
Fresh copies, purged with
tools/mutation_run.sh, graded over test_safety83.py and test_safety78.py:_peak_over_drain)c.lo > rate→c.lo >= ratec.lo > rate→c.lo > 0(refile whenever engaged)rate = 0The lost-reading run in part 3 alone did not kill M5 (nothing is refiled there), so a direct check was added.
🤖 Generated with Claude Code
https://claude.ai/code/session_01UBtqPrG1mDLXzTEBnhEkkV
admin referenced this pull request2026-09-29 23:09:29 +02:00
Review round 1 (SAFETY-83)
Reviewed at
56cafefa7. The fix is correct. Three findings, all about the branch or its tests. The dev agent is gone, so I am pushing the fixes to this branch myself.Verified (no action)
lo > hiclash with the safety limits. At N=1 the only P_SAFETY limits aredeviceandsupervised. The peak floor is already capped atsite_rating_w, which is the smaller of the two, so the refiledlostays under theirhi(part 3, max_w 3000).packandrefusedare filed at N>1 only.len(self.inverters) != 1return plus part 4 and the s78 part 5 digests.why_raised).Findings
release/1.0. It does not contain #156 (OPT-11 P4,1145a93). Fix: mergerelease/1.0into the branch the normal way, then run the full suite again.quarter_ceiling_w() − margin, under a banked peak too. With the aim changed tocap − margin(the banked peak ignored), test_safety83 and test_safety78 still passed. Fix: new part 1 check. With 4500 W banked, a 7500 W house now banks 4450 W (was 5000 W before the fix), and a 6500 W house stays at 4000 W.Mutations (mine, fresh copies, graded on test_safety83 + test_safety78)
lo > ratecondition droppedlo > rate→lo >= ratemargin_w=0.0into decide)lo > rate + 50)owns_setpointcheck droppedowns_setpoint=True(maintenance.py). Kept as a mirror of_drain_floor()rate = 0load_too_highalways TrueReview round 2: fixes pushed (head
bc6d744)release/1.0(#156,1145a93) into the branch in79b9402, with no conflicts. Full suite on the merged tree: 54/54 test files pass ingoodwe-addon-checks:1. The s78 part 5 digests and the s83 identity checks are unchanged.bc6d744). New test_safety83 part 1 check: with 4500 W banked (cap 3500, margin 50, drain 2500), a 7500 W house banks 4450 W (5000 W before the fix), and a 6500 W house stays at 4000 W. With the aim changed tocap − marginthis check fails.bc6d744). No 'Load too high' alarm on any part 1 run, or on a 6000 W house with 5 s spikes to 11000 W that saturate the guard. The part 3 overload (max_w 3000) must still raise the alarm. With ALARM_HOLD_S set to 0 the new check fails; with the alarm switched off, the overload check fails.Nothing is open. I will merge once the required check is green on
bc6d744.