SAFETY-78: maintenance drain at N=2 no longer stalls #154

Merged
admin merged 4 commits from safety-78-drain-at-two into release/1.0 2026-09-29 22:20:30 +02:00
Owner

Fixes the stalled maintenance drain at N=2 (OPT-11 P1 F1, UX-13 P3 check 5, OPT-11 P3 F4, MAINT-05 §6).

Root cause

At N>=2 the drain pins its pack (unit SET, P_MAINTENANCE) and the site target is split around the pin. Whenever the site target is below the pin, the other pack has to charge. The allocator refuses opposite directions (SAFETY-25 §0a), so the site goes to 0 W and logs ARBITER: REFUSED every cycle. This happens when the house is below the drain rate or there is a grid-charge trade. It also happens above it: after one refusal the loop restarts from 0 W, and its 400 W slew step is again below the 2000 W pin. On 5a4cdc2 the rig refuses 60/60 cycles at 1200, 2000 and 3000 W, and n2_maint refuses 392 of 540.

Fix

  • main.py: at N>=2, while a pack drains, it files a site limit drain, lo = what the pin places (allocator.pin_w(): the drain rate inside that pack's own hard limits), at P_MAINTENANCE. The site target is never below the pin, so the other pack only adds power in the same direction. Opposite splits stay forbidden.
  • The priority order comes from the arbiter itself: a safety SET (100) parks everything; the peak floor (50) and the drain floor are both applied, so the higher one wins; a trade or the loop below the drain is lifted to it.
  • A draining pack that can place nothing (a refusal or a dropped link) adds no floor, and one line is logged: "maintenance: X cannot give power now, so its monthly cycle waits".
  • modetrade.decide(draining=): no grid charger while a pack drains (why cycle). Battery card: a new Why, "{name} is emptying for its monthly cycle. This one does not charge." (EN/NL).
  • Design point: below the drain rate the drained pack exports the rest, as the drain already does at N=1 (maintenance.py: "forced discharge, exports the surplus"). Holding it to house-load speed would make N=2 differ from N=1, so that is an owner question. Details in DECISIONS/SAFETY-78-drain-at-two.md.

Tests (test_safety78.py)

  1. N=2, house 1200 / 2000 / 3000 / -1500 W: A at 2000 W on every cycle, B >= 0, no opposite split, no refusal, no ARBITER line. Above the drain rate B covers the rest (grid at aim). The drain ends and moves to charge. Non-vacuous: without the floor, the same fixture refuses 24/24 cycles.
  2. A refuses to give power: one wait line in 120 cycles, and B covers the house. A dropped (writes rejected): no floor, B 1500 W, one line.
  3. Modes on, B Cheap hours only in a cheap run: the baseline charges; during the drain there is no charge, no trade and why cycle, and B's card shows other_cycle. Modes off: the site trade buys, and B never charges.
  4. Peak guard binds during the drain (house 6000 W, B Off): the site goes to the guard floor, A stays at 2000 W and B gives the shortfall. A lost meter (safety SET) sets both to 0. Arbiter order checks.
  5. N=1 identity: tools/safety78_identity.py hashes commands, reasons, roles, trades, every claim and the newest action-log line (drain under below/at/above/surplus/over-limit houses, modes off and on). It is byte-identical to 5a4cdc2: the same file was run on both trees, and the digests are pinned. Bites: filing the floor at N=1 moves the digest. The existing identity digests are unchanged, except n2_maint (test_modes part 1), which changes on purpose and is re-pinned with that reason.
  • Updated: test_arbiter / test_peakguard 10f–10g (new site source drain, with its argument), test_maintenance part 12 (was the defect's specification, now the fix), test_batteries_screen.

Mutations (each on a fresh copy, graded over test_safety78, test_maintenance, test_modes, test_batteries_screen, test_arbiter, test_alloc, test_peakguard) — 13/13 killed

# mutation killed by
M1 drain claim never filed test_safety78 part 1
M2 floor also filed at N=1 test_safety78 part 5 (identity)
M3 floor = raw drain rate, not the pin inside the pack's limits part 2
M4 floor at P_LOOP part 3 (modes-off trade)
M5 floor at P_SAFETY part 4 (lost meter)
M6 wait line logged every cycle part 2
M7 wait line never logged part 2
M8 liveness ignored for the floor part 2 (dropped link)
M9 modetrade lets a grid charge through while draining part 3
M10 main passes draining=False part 3
M11 card: other_cycle branch removed part 3
M12 card names the wrong battery part 3
M13 pin_w unclamped part 2

Risks / not done

  • At N=1 a drain (60) still outranks the peak floor (50), so the guard does not add to a drain there. This is pre-existing and kept by the identity requirement.
  • Today's plan can still list a grid block for the draining battery (P3 F4 second bullet), and the battery that is waiting its turn still reads "Not planned yet" (UX-13 F-B). Not changed.
  • A maintenance CHARGE in a cheap window at N=2 (forced hi = -floor on one pack) could meet the loop asking the other pack to give. That is not this card and not measured here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UBtqPrG1mDLXzTEBnhEkkV

Fixes the stalled maintenance drain at N=2 (OPT-11 P1 F1, UX-13 P3 check 5, OPT-11 P3 F4, MAINT-05 §6). ## Root cause At N>=2 the drain pins its pack (unit SET, P_MAINTENANCE) and the site target is split around the pin. Whenever the site target is below the pin, the other pack has to charge. The allocator refuses opposite directions (SAFETY-25 §0a), so the site goes to 0 W and logs `ARBITER: REFUSED` every cycle. This happens when the house is below the drain rate or there is a grid-charge trade. It also happens **above** it: after one refusal the loop restarts from 0 W, and its 400 W slew step is again below the 2000 W pin. On 5a4cdc2 the rig refuses 60/60 cycles at 1200, 2000 and 3000 W, and `n2_maint` refuses 392 of 540. ## Fix - `main.py`: at N>=2, while a pack drains, it files a **site limit `drain`, lo = what the pin places** (`allocator.pin_w()`: the drain rate inside that pack's own hard limits), at P_MAINTENANCE. The site target is never below the pin, so the other pack only adds power in the **same** direction. Opposite splits stay forbidden. - The priority order comes from the arbiter itself: a safety SET (100) parks everything; the peak floor (50) and the drain floor are both applied, so the higher one wins; a trade or the loop below the drain is lifted to it. - A draining pack that can place nothing (a refusal or a dropped link) adds no floor, and one line is logged: "maintenance: X cannot give power now, so its monthly cycle waits". - `modetrade.decide(draining=)`: no grid charger while a pack drains (why `cycle`). Battery card: a new Why, "{name} is emptying for its monthly cycle. This one does not charge." (EN/NL). - **Design point:** below the drain rate the drained pack **exports the rest**, as the drain already does at N=1 (`maintenance.py`: "forced discharge, exports the surplus"). Holding it to house-load speed would make N=2 differ from N=1, so that is an owner question. Details in `DECISIONS/SAFETY-78-drain-at-two.md`. ## Tests (`test_safety78.py`) 1. N=2, house 1200 / 2000 / 3000 / -1500 W: A at 2000 W on every cycle, B >= 0, no opposite split, no refusal, no ARBITER line. Above the drain rate B covers the rest (grid at aim). The drain ends and moves to charge. Non-vacuous: without the floor, the same fixture refuses 24/24 cycles. 2. A refuses to give power: one wait line in 120 cycles, and B covers the house. A dropped (writes rejected): no floor, B 1500 W, one line. 3. Modes on, B Cheap hours only in a cheap run: the baseline charges; during the drain there is no charge, no trade and why `cycle`, and B's card shows `other_cycle`. Modes off: the site trade buys, and B never charges. 4. Peak guard binds during the drain (house 6000 W, B Off): the site goes to the guard floor, A stays at 2000 W and B gives the shortfall. A lost meter (safety SET) sets both to 0. Arbiter order checks. 5. **N=1 identity:** `tools/safety78_identity.py` hashes commands, reasons, roles, trades, every claim and the newest action-log line (drain under below/at/above/surplus/over-limit houses, modes off and on). It is byte-identical to 5a4cdc2: the same file was run on both trees, and the digests are pinned. Bites: filing the floor at N=1 moves the digest. The existing identity digests are unchanged, except `n2_maint` (test_modes part 1), which changes on purpose and is re-pinned with that reason. - Updated: test_arbiter / test_peakguard 10f–10g (new site source `drain`, with its argument), test_maintenance part 12 (was the defect's specification, now the fix), test_batteries_screen. ## Mutations (each on a fresh copy, graded over test_safety78, test_maintenance, test_modes, test_batteries_screen, test_arbiter, test_alloc, test_peakguard) — 13/13 killed | # | mutation | killed by | |---|---|---| | M1 | drain claim never filed | test_safety78 part 1 | | M2 | floor also filed at N=1 | test_safety78 part 5 (identity) | | M3 | floor = raw drain rate, not the pin inside the pack's limits | part 2 | | M4 | floor at P_LOOP | part 3 (modes-off trade) | | M5 | floor at P_SAFETY | part 4 (lost meter) | | M6 | wait line logged every cycle | part 2 | | M7 | wait line never logged | part 2 | | M8 | liveness ignored for the floor | part 2 (dropped link) | | M9 | modetrade lets a grid charge through while draining | part 3 | | M10 | main passes `draining=False` | part 3 | | M11 | card: `other_cycle` branch removed | part 3 | | M12 | card names the wrong battery | part 3 | | M13 | `pin_w` unclamped | part 2 | ## Risks / not done - At N=1 a drain (60) still outranks the peak floor (50), so the guard does not add to a drain there. This is pre-existing and kept by the identity requirement. - Today's plan can still list a grid block for the draining battery (P3 F4 second bullet), and the battery that is waiting its turn still reads "Not planned yet" (UX-13 F-B). Not changed. - A maintenance CHARGE in a cheap window at N=2 (forced `hi = -floor` on one pack) could meet the loop asking the other pack to give. That is not this card and not measured here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01UBtqPrG1mDLXzTEBnhEkkV
admin added 1 commit 2026-09-29 19:53:21 +02:00
SAFETY-78: maintenance drain at N=2 no longer stalls
checks / unit (pull_request) Failing after 17m13s
infra-pin / fresh (pull_request) Successful in 2s
5d27f8496f
At two inverters the drain pins its pack and the site target is split
around the pin. Below the pin the other pack had to charge, the allocator
refused the opposite directions, and the site sat at 0 W with an ARBITER
line every cycle (house below or above the drain rate, or a grid charge).

main.py now files a site limit `drain` (lo = what the pin places, at
P_MAINTENANCE) at N>=2 while a pack drains, so the other pack only adds
power in the same direction. A drain that cannot give power waits with
one log line. With battery modes on, no grid charge while a pack drains,
and the other battery's card says why. N=1 is byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
admin added 1 commit 2026-09-29 20:14:36 +02:00
SAFETY-78: test_safety78 points at MUTATION.md (test_bytecode)
checks / unit (pull_request) Successful in 15m23s
infra-pin / fresh (pull_request) Successful in 1s
af39c10893
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
admin added 1 commit 2026-09-29 21:13:38 +02:00
SAFETY-78 review: card truth while the drain waits, more checks
checks / unit (pull_request) Successful in 16m15s
infra-pin / fresh (pull_request) Successful in 1s
044fc0d5bd
- homeview: no "This one does not charge" while the draining battery waits
  (no floor then, so the other battery may charge from solar).
- test_safety78: one wait line across a probe, a second episode logs again,
  the card while waiting, and a seeded sweep of the floor against every
  other claim (part 6).
- DECISIONS: the wait case, and the measured N=1 peak and N=2 charge
  follow-ups.

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

Review of PR #154 (SAFETY-78) at af39c10

I checked the head af39c10 and base 5a4cdc2 with git ls-remote and the API. CI was green. I ran everything in goodwe-addon-checks:1. There was no TEST-23 run and no contact with live hosts.

1. The drain floor against the peak and safety rules: resolved (one card fix pushed)

  • Seeded sweep, 20000 claim lists (new test_safety78.py part 6). Each list has a drain pin on A, with or without a pack limit that narrows it; B On, Off or refusing; a random loop SET; trades of -3000, +1000 and +4000 W; a random peak floor; and sometimes a safety stop. Results:

    • 0 opposite splits and 0 contradictions.
    • The safety stop always parks both batteries.
    • The target is never below the pin, and B never charges.
    • A peak floor above the drain is always met.
    • Off B gives only when the peak floor is above the drain, never for the drain itself. This held on 1.7k cases (17.9k in a 200k run).
    • There is no site hi source, so the floor can't be pulled into a lo > hi.
  • Peak guard with SAFETY-79 margin, real Controller at N=2. House 6000 W, B Off, limit 2500 W. Quarter imports:

    margin quarter import
    0 2480 / 2500 / 2500 W
    50 2430 / 2450 / 2450 W
    300 2179 / 2200 / 2200 W

    In every run A stays at 2000 W, B only adds, with 0 refusals and 0 opposite splits. With B in Cheap hours or Leftover solar, the loop covers the house itself (import 1.5 kW), so the guard isn't needed.

  • Grid charge. A modes-off trade that buys is lifted to the floor (your part 3). With modes on there's no charger (cycle). No export is added beyond what the pin already did at N=1: B only adds power when a higher claim asks for it.

  • Safety stop, lost meter, SAFETY-73. A refusal limit narrows the pin (pin_w) and ends in a wait. A probe SET outranks the pin (pinned_by), so no floor is filed. A lost meter parks both (part 4).

  • Defect found and fixed (homeview.py). While the drain waits (A refuses or its link is dropped), no floor is filed and B may charge from solar. B's card still said "This one does not charge." Measured: house -1500 W, A cut, B at -1500 W, and the card still showed other_cycle. other_cycle is now skipped for a pack in _drain_waits. The new check kills the revert.

2. N=1: the drain outranks the peak guard: real breach path, follow-up (not fixed, identity kept)

N=1, limit 2500 W, drain 2000 W. Quarter import means from the real Controller:

house with drain without drain
4000 W 2000 W 0 W
5000 W 3000 W 0 W
6000 W 4000 W 1000 W

In safety78_identity the guard asks for +3173 W and gets 2000 W. So any house above drain + limit - margin (about 4450 W here) breaches while a drain runs, and the breach is 1:1 with the extra house load. At a 12-month average capacity tariff of about 50 EUR/kW-year (my assumption, check the DSO card), a +1.5 kW month costs about 6 EUR. On the owner's 0.12.0 options (limit 3500, drain 2500 W), the breach starts at a house of about 5950 W.

3. The maintenance CHARGE at N=2: stalls; follow-up, not the same small pattern

With s79.MAINT and a cheap price, A goes drain, then charge:

sites house refused result
N=2 800 W 141 of 261 charge cycles site 0 W (failsafe), 40 ARBITER lines
N=2 3000 W 141 of 261 charge cycles site 0 W (failsafe), 40 ARBITER lines
N=2 -1500 W 1 fine
N=1 any 0 fine

The cause is the mirror of this one: A's unit hi = -floor forces A to charge while the loop asks B to give. The mirror fix is a site hi, and that crosses the peak guard's site lo into lo > hi, which parks at 0 W (the SAFETY-07 trap). shaping_suspended() would have to guard it. So it isn't the same small pattern. I recorded it in the SAFETY-78 decision record's "Not changed" list, with the numbers.

4. Export below the drain rate: matches N=1 and DOCS

Below the rate, B is at 0 and A exports the rest (your part 1). That matches maintenance.py ("exports the surplus") and the new DOCS line.

Recommendation to the owner: keep exporting. Assume a 10 kWh pack drained from 60 % to 11 % (4.9 kWh) at the owner's 2500 W:

  • The drain takes about 2 h. With a 500 W house, about 3.9 kWh is exported at 0.04 EUR instead of saving an import at about 0.19 to 0.25 EUR. That costs about 0.6 to 0.8 EUR per cycle, or 7 to 10 EUR a year per pack.
  • Holding the drain to house load would stretch it to about 10 h. On a sunny start (the owner's maintenance_start_hour 10 UTC is midday) it wouldn't move at all, and it would hit the 12 h "drain stuck" timeout.
  • The cheaper lever is an evening start hour. That is a setting, not code.

5. The re-pinned n2_maint digest: only the fixed behaviour

I compared all 540 rows on both trees:

  • Base refused cycles 0 to 391 back to back (failsafe -> 0 W). The branch refused none, A was at 2000 W, and there were 0 opposite splits.
  • After the base recovers, commands differ only on cycles 392 to 421. That is the base slewing up from 0 at 400 W per cycle while the branch was already settled.
  • From cycle 422 the commands are identical. Only the frozen integrator's target differs, 3820 vs 3800 W, because the two runs had different histories.

6. Identity and mutations: pass

  • safety78_identity.py gives the same result on 5a4cdc2 and af39c10: n1_drain 55fcfc4a…, n1_drain_modes 93867eb4….
  • I sampled 5 of your mutations (M1, M3, M5, M8, M13) on fresh copies: all killed.
  • My own:
mutation result
R2: floor as lo=hi=drain killed (parts 1 and 2, n2_maint)
R3: other_cycle above peak killed (test_batteries_screen)
R1: drop the pinned_by check survived, killed by the new part 2 probe check (the wait line was logged again after each probe)
R6: _drain_waits never reset survived, killed by the new two-episode check
R7: card ignores the wait (reverts my fix) killed by the new card check
R4: drop owns_setpoint equivalent (a DRAIN result always owns the setpoint)
R5: floor at P_PEAK equivalent (both are site los above every site SET at N≥2)

7. Text and records: pass after edits

  • The card line (EN and NL) and the wait line are short and plain, and they follow the neighbouring keys (ze/haar).
  • DOCS is true. DECISIONS is concise. I added one sentence on the wait case and the "Not changed" numbers for 2 and 3.

Follow-ups to file: N=1 drain versus peak guard (point 2), and the N=2 maintenance charge stall (point 3).

Reviewer's fixes are pushed as 044fc0d: the homeview card fix, test_safety78 parts 2 and 6, and DECISIONS. The full suite passes locally in goodwe-addon-checks:1, all 51 files. I merge only when CI is green on that head.

## Review of PR #154 (SAFETY-78) at af39c10 I checked the head af39c10 and base 5a4cdc2 with `git ls-remote` and the API. CI was green. I ran everything in `goodwe-addon-checks:1`. There was no TEST-23 run and no contact with live hosts. ### 1. The `drain` floor against the peak and safety rules: resolved (one card fix pushed) - **Seeded sweep, 20000 claim lists (new `test_safety78.py` part 6).** Each list has a drain pin on A, with or without a pack limit that narrows it; B On, Off or refusing; a random loop SET; trades of -3000, +1000 and +4000 W; a random peak floor; and sometimes a safety stop. Results: - 0 opposite splits and 0 contradictions. - The safety stop always parks both batteries. - The target is never below the pin, and B never charges. - A peak floor above the drain is always met. - Off B gives only when the peak floor is above the drain, never for the drain itself. This held on 1.7k cases (17.9k in a 200k run). - There is no site `hi` source, so the floor can't be pulled into a `lo > hi`. - **Peak guard with SAFETY-79 margin, real Controller at N=2.** House 6000 W, B Off, limit 2500 W. Quarter imports: | margin | quarter import | |---|---| | 0 | 2480 / 2500 / 2500 W | | 50 | 2430 / 2450 / 2450 W | | 300 | 2179 / 2200 / 2200 W | In every run A stays at 2000 W, B only adds, with 0 refusals and 0 opposite splits. With B in Cheap hours or Leftover solar, the loop covers the house itself (import 1.5 kW), so the guard isn't needed. - **Grid charge.** A modes-off trade that buys is lifted to the floor (your part 3). With modes on there's no charger (`cycle`). No export is added beyond what the pin already did at N=1: B only adds power when a higher claim asks for it. - **Safety stop, lost meter, SAFETY-73.** A refusal limit narrows the pin (`pin_w`) and ends in a wait. A probe SET outranks the pin (`pinned_by`), so no floor is filed. A lost meter parks both (part 4). - **Defect found and fixed (`homeview.py`).** While the drain waits (A refuses or its link is dropped), no floor is filed and B may charge from solar. B's card still said "This one does not charge." Measured: house -1500 W, A cut, B at -1500 W, and the card still showed `other_cycle`. `other_cycle` is now skipped for a pack in `_drain_waits`. The new check kills the revert. ### 2. N=1: the drain outranks the peak guard: real breach path, follow-up (not fixed, identity kept) N=1, limit 2500 W, drain 2000 W. Quarter import means from the real Controller: | house | with drain | without drain | |---|---|---| | 4000 W | 2000 W | 0 W | | 5000 W | 3000 W | 0 W | | 6000 W | 4000 W | 1000 W | In `safety78_identity` the guard asks for +3173 W and gets 2000 W. So any house above drain + limit - margin (about 4450 W here) breaches while a drain runs, and the breach is 1:1 with the extra house load. At a 12-month average capacity tariff of about 50 EUR/kW-year (my assumption, check the DSO card), a +1.5 kW month costs about 6 EUR. On the owner's 0.12.0 options (limit 3500, drain 2500 W), the breach starts at a house of about 5950 W. ### 3. The maintenance CHARGE at N=2: stalls; follow-up, not the same small pattern With `s79.MAINT` and a cheap price, A goes drain, then charge: | sites | house | refused | result | |---|---|---|---| | N=2 | 800 W | 141 of 261 charge cycles | site 0 W (`failsafe`), 40 ARBITER lines | | N=2 | 3000 W | 141 of 261 charge cycles | site 0 W (`failsafe`), 40 ARBITER lines | | N=2 | -1500 W | 1 | fine | | N=1 | any | 0 | fine | The cause is the mirror of this one: A's unit `hi = -floor` forces A to charge while the loop asks B to give. The mirror fix is a site `hi`, and that crosses the peak guard's site `lo` into `lo > hi`, which parks at 0 W (the SAFETY-07 trap). `shaping_suspended()` would have to guard it. So it isn't the same small pattern. I recorded it in the SAFETY-78 decision record's "Not changed" list, with the numbers. ### 4. Export below the drain rate: matches N=1 and DOCS Below the rate, B is at 0 and A exports the rest (your part 1). That matches `maintenance.py` ("exports the surplus") and the new DOCS line. **Recommendation to the owner: keep exporting.** Assume a 10 kWh pack drained from 60 % to 11 % (4.9 kWh) at the owner's 2500 W: - The drain takes about 2 h. With a 500 W house, about 3.9 kWh is exported at 0.04 EUR instead of saving an import at about 0.19 to 0.25 EUR. That costs about 0.6 to 0.8 EUR per cycle, or 7 to 10 EUR a year per pack. - Holding the drain to house load would stretch it to about 10 h. On a sunny start (the owner's `maintenance_start_hour` 10 UTC is midday) it wouldn't move at all, and it would hit the 12 h "drain stuck" timeout. - The cheaper lever is an evening start hour. That is a setting, not code. ### 5. The re-pinned `n2_maint` digest: only the fixed behaviour I compared all 540 rows on both trees: - Base refused cycles 0 to 391 back to back (`failsafe -> 0 W`). The branch refused none, A was at 2000 W, and there were 0 opposite splits. - After the base recovers, commands differ only on cycles 392 to 421. That is the base slewing up from 0 at 400 W per cycle while the branch was already settled. - From cycle 422 the commands are identical. Only the frozen integrator's target differs, 3820 vs 3800 W, because the two runs had different histories. ### 6. Identity and mutations: pass - `safety78_identity.py` gives the same result on 5a4cdc2 and af39c10: `n1_drain` 55fcfc4a…, `n1_drain_modes` 93867eb4…. - I sampled 5 of your mutations (M1, M3, M5, M8, M13) on fresh copies: all killed. - My own: | mutation | result | |---|---| | R2: floor as `lo=hi=drain` | killed (parts 1 and 2, `n2_maint`) | | R3: `other_cycle` above `peak` | killed (test_batteries_screen) | | R1: drop the `pinned_by` check | survived, killed by the new part 2 probe check (the wait line was logged again after each probe) | | R6: `_drain_waits` never reset | survived, killed by the new two-episode check | | R7: card ignores the wait (reverts my fix) | killed by the new card check | | R4: drop `owns_setpoint` | equivalent (a DRAIN result always owns the setpoint) | | R5: floor at P_PEAK | equivalent (both are site `lo`s above every site SET at N≥2) | ### 7. Text and records: pass after edits - The card line (EN and NL) and the wait line are short and plain, and they follow the neighbouring keys (ze/haar). - DOCS is true. DECISIONS is concise. I added one sentence on the wait case and the "Not changed" numbers for 2 and 3. Follow-ups to file: N=1 drain versus peak guard (point 2), and the N=2 maintenance charge stall (point 3). Reviewer's fixes are pushed as 044fc0d: the homeview card fix, test_safety78 parts 2 and 6, and DECISIONS. The full suite passes locally in `goodwe-addon-checks:1`, all 51 files. I merge only when CI is green on that head.
admin added 1 commit 2026-09-29 21:54:02 +02:00
Merge release/1.0 into safety-78-drain-at-two
checks / unit (pull_request) Successful in 18m49s
infra-pin / fresh (pull_request) Successful in 2s
516bc8b4ec
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

release/1.0 moved to 4f25b24 (#155 SAFETY-81). I merged it in normally as 516bc8b, with no conflicts. On the merged tree the full suite passes locally (52/52), and safety78_identity on 4f25b24 still matches the pinned digests. CI is green on 516bc8b. All 7 points are resolved; see the comment above. Merging with head_commit_id pinned.

release/1.0 moved to 4f25b24 (#155 SAFETY-81). I merged it in normally as 516bc8b, with no conflicts. On the merged tree the full suite passes locally (52/52), and `safety78_identity` on 4f25b24 still matches the pinned digests. CI is green on 516bc8b. All 7 points are resolved; see the comment above. Merging with head_commit_id pinned.
admin merged commit 35cdffe766 into release/1.0 2026-09-29 22:20:30 +02:00
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#154