SPIKE-04a — the desk half of SPIKE-04. Documentation and its pin. No control behaviour changes.
Companion PR in admin/goodwe: the same card's NOTES.md §1.1b, where the figures actually live.
What was wrong
Three places stated inverter B's rating and all three said 3000 W — a number nobody had measured or read off anything:
site
was
now
goodwe_controller/config.yaml roster example
3000
4600
goodwe_controller/DOCS.md roster example
3000
4600
tools/supervisor_shape_probe.py §C, "a blob an operator would POST"
3000
4600
The owner supplied B's model and serial on 2026-09-04: a GoodWe GW5048D-ES, serial 55048ESU22AW0992, max charging / discharging power 4600 W. Serial characters 6-8 are ESU → platform 105, AA55, the supported set — so SPIKE-01 does not gate this site.
⚠️ 4600 W is FROM A DATASHEET and is not a measurement
The datasheet prints it under Battery Input Data, so it is probably DC, while max_w is graded against 0x0518 — the inverter's own AC power. If it is DC the AC ceiling is nearer 4324 W.
Neither reading is written in as fact. SAFETY-20's min(max_w, dev_max) already bounds it correctly under either, once dev_max is read off the unit, so max_w: 4600 is what to configure today. goodwe/NOTES.md §1.1b tabulates both readings — including that the DC reading moves the two directions in opposite senses (discharge down to ~4324 W, charge up to ~4894 W AC-side), so a single "derate by 94 %" would be the wrong correction.
A new check, because a documentation fix without one drifts again
test_validate.py now pins all three documented rosters: findable, in agreement, and equal to 4600 — with an explicit len(...) == 3 guard so a regex that stopped matching cannot leave "they all agree" green on the two sites still being read.
It also grades the provenance beside the number — the datasheet, the 0x0518 it is graded against, and the 4324 W the DC reading would make it — within 2500 characters of the roster, not file-wide.
⚠️The first version searched whole files and was already vacuous on two of them.0x0518 appeared once in config.yaml and once in DOCS.mdbefore this card, for unrelated reasons, so "0x0518" in src was green no matter what happened to inverter B's rating. A check whose data cannot reach the failure it names, in the file that exists to complain about those. The window is the fix — and it immediately found that the DOCS.md caveat did not name 0x0518 at all, which it now does.
Mutations — bytecode purged and counted at 0 before and after each
mutation
byte delta
result
M1
probe's operator blob 4600 → 3000
0 B
RED — value and agreement
M2
DOCS.md's DC/AC caveat deleted
−157 B
RED — provenance
M3
config.yaml alone "corrected" to 4324
0 B
RED — agreement
M4
probe entity renamed, regex finds nothing
—
RED — the vacuity guard fires
Restored → green. The two 0-byte-delta mutations change a printed answer (the per-site dict in the check output), so the edit is demonstrably running.
The probe itself was re-run against Supervisor 2026.08.0's own AppOptions validator with the 4600 in place: all cases passed, exit 0.
⚠️ The test fixtures keep 3000 W on purpose, and now say why at the line
test_inverter.py, test_maintenance.py, test_peakguard.py and test_watchdog.py build a second inverter at 3000 W against A's 5000 W.
The only property those checks consume is that B's number is unmistakably not A's. The real pair is a 400 W gap — narrower than slew_w, inside the range where an off-by-one-unit bug and ordinary control arithmetic produce indistinguishable numbers. Collapsing them onto the real values would buy realism those checks do not use and cost them their separating power, which is precisely what test_peakguard.py's own §10g PRECONDITION exists to prevent. Their comments no longer call 3000 W a hypothesis about B's rating; they call it illustrative, and not a placeholder either — the placeholder era ended on 2026-09-04.
Two consequences carried into the changelog
94 % battery-to-AC ⇒ ~88 % round trip, so A charging 3 kW while B discharges 3 kW burns ≈ 360 W continuously with the meter at zero. SAFETY-31 argues its threshold from "a few watts" and has no figure; this is one.
< 13 W night self-consumption per unit (≈ 26 W for two ≈ 0.6 kWh/day), which nothing in this add-on accounts for anywhere.
The site is asymmetric in opposite directions — A 5000 W / 15.36 kWh nominal, B 4600 W / 20.48 kWh: ~33 % more energy, ~8 % less power. No stability implication (ARCH-01's ḡ = Σαᵢgᵢ is a convex combination), but MAINT-05's 12 h drain phase timeout was derived on A and a full B recalibration moves 33 % more energy.
⚠️ Honest survivors
goodwe/NOTES.md §1.1b, where all of this is actually recorded, is graded by nothing here. Measured, not assumed: rewriting its 16 occurrences of 4600 to 3000and turning every FROM DATASHEET label into MEASURED ON THE UNIT leaves all 19 files GREEN. This CI cannot fetch admin/goodwe — private, and GITHUB_TOKEN is minted for this repository only, as .gitea/workflows/checks.yml already states — so the cross-repo check is not buildable here.
No check can see whether 4600 is the right number. Everything above pins a datasheet reading against itself. Only the bench half of SPIKE-04 can say whether the unit agrees, and the DC-vs-AC ambiguity is the live example of why.
The constant is still named B_RATING_HYPOTHESIS in four test files, which reads as a hypothesis about B's rating when it is now an illustrative fixture. Left alone deliberately: renaming ~20 sites across test_peakguard.py and test_inverter.py would collide hard with the open SAFETY-32 PR (#48), which rewrites 1255 lines of the first. The comment directly above each definition says what the value is and is not.
Verification
19/19 green in goodwe-addon-ci, rsynced to a plain path, mounted :ro, bytecode purged and verified 0 before and 0 after.
test_peakguard.py passed on the baseline run, on the change, and on the final run — no red from it at any point, so CI-06's flake did not need isolation re-runs.
Baseline on release/1.0 @ 3934ad7 was run first and was also 19/19, so the green is a comparison and not an assertion.
Do not merge on my account. Not merged, and release/1.0 untouched.
**SPIKE-04a — the desk half of SPIKE-04.** Documentation and its pin. **No control behaviour changes.**
Companion PR in `admin/goodwe`: the same card's `NOTES.md` §1.1b, where the figures actually live.
---
## What was wrong
Three places stated inverter B's rating and all three said **3000 W** — a number nobody had measured or read off anything:
| site | was | now |
|---|---|---|
| `goodwe_controller/config.yaml` roster example | 3000 | **4600** |
| `goodwe_controller/DOCS.md` roster example | 3000 | **4600** |
| `tools/supervisor_shape_probe.py` §C, "a blob an operator would POST" | 3000 | **4600** |
The owner supplied B's model and serial on 2026-09-04: a **GoodWe GW5048D-ES**, serial `55048ESU22AW0992`, **max charging / discharging power 4600 W**. Serial characters 6-8 are **`ESU`** → platform 105, AA55, the supported set — so **SPIKE-01 does not gate this site**.
## ⚠️ 4600 W is FROM A DATASHEET and is not a measurement
The datasheet prints it under **Battery Input Data**, so it is probably **DC**, while `max_w` is graded against `0x0518` — *the inverter's own AC power*. If it is DC the AC ceiling is nearer **4324 W**.
**Neither reading is written in as fact.** SAFETY-20's `min(max_w, dev_max)` already bounds it correctly under either, once `dev_max` is read off the unit, so `max_w: 4600` is what to configure today. `goodwe/NOTES.md` §1.1b tabulates both readings — including that the DC reading moves the two directions in *opposite* senses (discharge down to ~4324 W, charge *up* to ~4894 W AC-side), so a single "derate by 94 %" would be the wrong correction.
## A new check, because a documentation fix without one drifts again
`test_validate.py` now pins all three documented rosters: **findable, in agreement, and equal to 4600** — with an explicit `len(...) == 3` guard so a regex that stopped matching cannot leave *"they all agree"* green on the two sites still being read.
It also grades the **provenance beside the number** — the datasheet, the `0x0518` it is graded against, and the 4324 W the DC reading would make it — **within 2500 characters of the roster, not file-wide.**
> ⚠️ **The first version searched whole files and was already vacuous on two of them.** `0x0518` appeared once in `config.yaml` and once in `DOCS.md` **before this card**, for unrelated reasons, so `"0x0518" in src` was green no matter what happened to inverter B's rating. A check whose data cannot reach the failure it names, in the file that exists to complain about those. The window is the fix — and it immediately found that the `DOCS.md` caveat did not name `0x0518` at all, which it now does.
### Mutations — bytecode purged and **counted at 0** before and after each
| | mutation | byte delta | result |
|---|---|---|---|
| M1 | probe's operator blob 4600 → 3000 | **0 B** | **RED** — value *and* agreement |
| M2 | `DOCS.md`'s DC/AC caveat deleted | −157 B | **RED** — provenance |
| M3 | `config.yaml` alone "corrected" to 4324 | **0 B** | **RED** — agreement |
| M4 | probe entity renamed, regex finds nothing | — | **RED** — the vacuity guard fires |
Restored → green. The two 0-byte-delta mutations change a **printed** answer (the per-site dict in the check output), so the edit is demonstrably running.
The probe itself was re-run against **Supervisor 2026.08.0's own `AppOptions` validator** with the 4600 in place: *all cases passed, exit 0*.
## ⚠️ The test fixtures keep 3000 W on purpose, and now say why at the line
`test_inverter.py`, `test_maintenance.py`, `test_peakguard.py` and `test_watchdog.py` build a second inverter at **3000 W** against A's 5000 W.
The only property those checks consume is that B's number is **unmistakably** not A's. The real pair is a **400 W** gap — narrower than `slew_w`, inside the range where an off-by-one-unit bug and ordinary control arithmetic produce indistinguishable numbers. Collapsing them onto the real values would buy realism those checks do not use and **cost them their separating power**, which is precisely what `test_peakguard.py`'s own §10g PRECONDITION exists to prevent. Their comments no longer call 3000 W a *hypothesis about B's rating*; they call it **illustrative**, and not a placeholder either — the placeholder era ended on 2026-09-04.
## Two consequences carried into the changelog
* **94 % battery-to-AC ⇒ ~88 % round trip**, so A charging 3 kW while B discharges 3 kW burns **≈ 360 W continuously** with the meter at zero. SAFETY-31 argues its threshold from *"a few watts"* and has no figure; this is one.
* **< 13 W night self-consumption per unit** (≈ 26 W for two ≈ 0.6 kWh/day), which nothing in this add-on accounts for anywhere.
* The site is asymmetric in **opposite directions** — A 5000 W / 15.36 kWh nominal, B 4600 W / 20.48 kWh: ~33 % more energy, ~8 % less power. No stability implication (ARCH-01's ḡ = Σαᵢgᵢ is a convex combination), but **MAINT-05's 12 h `drain` phase timeout was derived on A** and a full B recalibration moves 33 % more energy.
## ⚠️ Honest survivors
1. **`goodwe/NOTES.md` §1.1b, where all of this is actually recorded, is graded by nothing here.** Measured, not assumed: rewriting its 16 occurrences of `4600` to `3000` **and** turning every `FROM DATASHEET` label into `MEASURED ON THE UNIT` leaves **all 19 files GREEN**. This CI cannot fetch `admin/goodwe` — private, and `GITHUB_TOKEN` is minted for this repository only, as `.gitea/workflows/checks.yml` already states — so the cross-repo check is not buildable here.
2. **No check can see whether 4600 is the *right* number.** Everything above pins a datasheet reading against itself. Only the bench half of SPIKE-04 can say whether the unit agrees, and the DC-vs-AC ambiguity is the live example of why.
3. **The constant is still named `B_RATING_HYPOTHESIS`** in four test files, which reads as a hypothesis about B's rating when it is now an illustrative fixture. Left alone deliberately: renaming ~20 sites across `test_peakguard.py` and `test_inverter.py` would collide hard with the **open SAFETY-32 PR (#48)**, which rewrites 1255 lines of the first. The comment directly above each definition says what the value is and is not.
## Verification
* **19/19 green** in `goodwe-addon-ci`, rsynced to a plain path, mounted `:ro`, bytecode purged and verified **0 before and 0 after**.
* `test_peakguard.py` passed on the baseline run, on the change, and on the final run — no red from it at any point, so CI-06's flake did not need isolation re-runs.
* Baseline on `release/1.0` @ 3934ad7 was run first and was also 19/19, so the green is a comparison and not an assertion.
**Do not merge on my account.** Not merged, and `release/1.0` untouched.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
The desk half of SPIKE-04. No control behaviour changes; this is documentation
and the check that stops it drifting again.
Three places stated inverter B's rating - config.yaml's commented roster
example, DOCS.md's, and tools/supervisor_shape_probe.py's "a blob an operator
would POST" - and all three said 3000 W, a number nobody had measured or read
off anything. The owner supplied B's model and serial on 2026-09-04: a GoodWe
GW5048D-ES, serial `55048ESU22AW0992`, max charging and discharging power
4600 W. Serial characters 6-8 are `ESU`, platform 105 / AA55, so SPIKE-01 does
not gate this site. All three copies now read 4600.
⚠️ 4600 W IS FROM A DATASHEET AND IS NOT A MEASUREMENT, and the one ambiguity
is recorded rather than guessed. The datasheet prints it under BATTERY INPUT
DATA, so it is probably DC, while max_w is graded against 0x0518 - the
inverter's own AC power. If it is DC the AC ceiling is nearer 4324 W. Neither
reading is written in as fact; SAFETY-20's min(max_w, dev_max) already bounds
it correctly under either, once dev_max is read off the unit, so `max_w: 4600`
is what an operator should configure today. goodwe/NOTES.md §1.1b carries the
full record with the date and the method on every figure.
A NEW CHECK, BECAUSE A DOCUMENTATION FIX WITHOUT ONE DRIFTS AGAIN.
test_validate.py now pins all three documented rosters: findable, in agreement,
and equal to 4600 - with an explicit len(...) == 3 guard so that a regex which
stopped matching cannot leave "they all agree" green on the two sites still
read. It also grades the PROVENANCE beside the number (the datasheet, the
0x0518 it is graded against, and the 4324 W the DC reading would make it)
WITHIN 2500 characters of the roster rather than file-wide.
⚠️ The first version of that provenance check searched whole files and was
ALREADY VACUOUS on two of them: 0x0518 appeared once in config.yaml and once in
DOCS.md before this card, for unrelated reasons, so `"0x0518" in src` was green
no matter what happened next to B's rating. That is a check whose data cannot
reach the failure it names, in the file that exists to complain about them. It
is fixed, and the window is what fixes it - deleting the caveat beside the
number now goes red even where the same token survives elsewhere in the file.
The windowed version immediately found that the DOCS.md caveat did not name
0x0518 at all; DOCS.md now says it.
Four mutations, bytecode purged and counted at 0 before and after each, all
RED (two of them 0-byte deltas that change a printed answer):
M1 probe's operator blob 4600 -> 3000 0 B RED (value + agreement)
M2 DOCS.md's DC/AC caveat deleted -157 B RED (provenance)
M3 config.yaml alone 4600 -> 4324 0 B RED (agreement)
M4 probe entity renamed, regex finds nothing RED (the vacuity guard)
Restored, green again. The probe itself was re-run against Supervisor
2026.08.0's own AppOptions validator with the 4600: all cases passed, exit 0.
⚠️ THE TEST FIXTURES KEEP 3000 W ON PURPOSE AND NOW SAY WHY AT THE LINE.
test_inverter.py, test_maintenance.py, test_peakguard.py and test_watchdog.py
build a second inverter at 3000 W against A's 5000 W. The only property those
checks consume is that B's number is unmistakably not A's, and the real pair is
a 400 W gap - narrower than slew_w, inside the range where an off-by-one-unit
bug and ordinary control arithmetic produce indistinguishable numbers.
Collapsing them onto the real values would buy realism those checks do not use
and cost them their separating power, which is exactly what test_peakguard.py's
own §10g PRECONDITION exists to prevent. Their comments no longer call 3000 W a
hypothesis about B's rating; they call it illustrative, and not a placeholder
either - the placeholder era ended on 2026-09-04.
Also recorded in the changelog for the cards that own them: 94 % battery-to-AC
=> ~88 % round trip => ~360 W burned continuously to circulate 3 kW, which is
the number SAFETY-31 argues its threshold from without having one; < 13 W night
self-consumption per unit, which nothing accounts for; and the A/B asymmetry in
opposite directions (~33 % more energy, ~8 % less power), which means MAINT-05's
12 h drain timeout was derived on A and should be re-checked against B.
⚠️ Honest survivors:
- goodwe/NOTES.md §1.1b, where all of this is actually recorded, is graded by
nothing here. Rewriting its 16 occurrences of 4600 to 3000 and turning every
FROM DATASHEET label into MEASURED ON THE UNIT leaves all 19 files GREEN.
This CI cannot fetch admin/goodwe - private, and GITHUB_TOKEN is minted for
this repository only, as .gitea/workflows/checks.yml already states - so the
cross-repo check is not buildable here.
- No check can see whether 4600 is the RIGHT number. Everything above pins a
datasheet reading against itself. Only the bench half of SPIKE-04 can say
whether the unit agrees, and the DC-vs-AC ambiguity is the live example.
- The constant is still named B_RATING_HYPOTHESIS in four test files, which
reads as a hypothesis about B's rating when it is now an illustrative
fixture. It is left alone deliberately: renaming ~20 sites across
test_peakguard.py and test_inverter.py would collide hard with the open
SAFETY-32 pull request, which rewrites 1255 lines of the first. The comment
directly above each definition says what the value is and is not.
Suite: 19/19 green in goodwe-addon-ci, mounted :ro, bytecode purged and
verified 0 before and after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
admin
merged commit 67e649ad6b into release/1.02026-09-05 02:55:25 +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.
SPIKE-04a — the desk half of SPIKE-04. Documentation and its pin. No control behaviour changes.
Companion PR in
admin/goodwe: the same card'sNOTES.md§1.1b, where the figures actually live.What was wrong
Three places stated inverter B's rating and all three said 3000 W — a number nobody had measured or read off anything:
goodwe_controller/config.yamlroster examplegoodwe_controller/DOCS.mdroster exampletools/supervisor_shape_probe.py§C, "a blob an operator would POST"The owner supplied B's model and serial on 2026-09-04: a GoodWe GW5048D-ES, serial
55048ESU22AW0992, max charging / discharging power 4600 W. Serial characters 6-8 areESU→ platform 105, AA55, the supported set — so SPIKE-01 does not gate this site.⚠️ 4600 W is FROM A DATASHEET and is not a measurement
The datasheet prints it under Battery Input Data, so it is probably DC, while
max_wis graded against0x0518— the inverter's own AC power. If it is DC the AC ceiling is nearer 4324 W.Neither reading is written in as fact. SAFETY-20's
min(max_w, dev_max)already bounds it correctly under either, oncedev_maxis read off the unit, somax_w: 4600is what to configure today.goodwe/NOTES.md§1.1b tabulates both readings — including that the DC reading moves the two directions in opposite senses (discharge down to ~4324 W, charge up to ~4894 W AC-side), so a single "derate by 94 %" would be the wrong correction.A new check, because a documentation fix without one drifts again
test_validate.pynow pins all three documented rosters: findable, in agreement, and equal to 4600 — with an explicitlen(...) == 3guard so a regex that stopped matching cannot leave "they all agree" green on the two sites still being read.It also grades the provenance beside the number — the datasheet, the
0x0518it is graded against, and the 4324 W the DC reading would make it — within 2500 characters of the roster, not file-wide.Mutations — bytecode purged and counted at 0 before and after each
DOCS.md's DC/AC caveat deletedconfig.yamlalone "corrected" to 4324Restored → green. The two 0-byte-delta mutations change a printed answer (the per-site dict in the check output), so the edit is demonstrably running.
The probe itself was re-run against Supervisor 2026.08.0's own
AppOptionsvalidator with the 4600 in place: all cases passed, exit 0.⚠️ The test fixtures keep 3000 W on purpose, and now say why at the line
test_inverter.py,test_maintenance.py,test_peakguard.pyandtest_watchdog.pybuild a second inverter at 3000 W against A's 5000 W.The only property those checks consume is that B's number is unmistakably not A's. The real pair is a 400 W gap — narrower than
slew_w, inside the range where an off-by-one-unit bug and ordinary control arithmetic produce indistinguishable numbers. Collapsing them onto the real values would buy realism those checks do not use and cost them their separating power, which is precisely whattest_peakguard.py's own §10g PRECONDITION exists to prevent. Their comments no longer call 3000 W a hypothesis about B's rating; they call it illustrative, and not a placeholder either — the placeholder era ended on 2026-09-04.Two consequences carried into the changelog
drainphase timeout was derived on A and a full B recalibration moves 33 % more energy.⚠️ Honest survivors
goodwe/NOTES.md§1.1b, where all of this is actually recorded, is graded by nothing here. Measured, not assumed: rewriting its 16 occurrences of4600to3000and turning everyFROM DATASHEETlabel intoMEASURED ON THE UNITleaves all 19 files GREEN. This CI cannot fetchadmin/goodwe— private, andGITHUB_TOKENis minted for this repository only, as.gitea/workflows/checks.ymlalready states — so the cross-repo check is not buildable here.B_RATING_HYPOTHESISin four test files, which reads as a hypothesis about B's rating when it is now an illustrative fixture. Left alone deliberately: renaming ~20 sites acrosstest_peakguard.pyandtest_inverter.pywould collide hard with the open SAFETY-32 PR (#48), which rewrites 1255 lines of the first. The comment directly above each definition says what the value is and is not.Verification
goodwe-addon-ci, rsynced to a plain path, mounted:ro, bytecode purged and verified 0 before and 0 after.test_peakguard.pypassed on the baseline run, on the change, and on the final run — no red from it at any point, so CI-06's flake did not need isolation re-runs.release/1.0@3934ad7was run first and was also 19/19, so the green is a comparison and not an assertion.Do not merge on my account. Not merged, and
release/1.0untouched.🤖 Generated with Claude Code
https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU