SPIKE-04a: inverter B is 4600 W FROM ITS DATASHEET, and the copies are pinned
checks / unit (pull_request) Successful in 4m51s

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
This commit is contained in:
glenn schrooyen
2026-09-04 22:19:31 +02:00
co-authored by Claude Opus 5
parent 3934ad7ea5
commit a9993aeda2
9 changed files with 247 additions and 14 deletions
+61
View File
@@ -2,6 +2,67 @@
## Unreleased
**Inverter B's rating is 4600 W, FROM ITS DATASHEET, and every documented copy
of it now says so (SPIKE-04a).** Three places stated 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 that
was never measured, never read off a datasheet, and simply wrong. 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**. No control
behaviour changes here; this is documentation and its pin.
* **Serial characters 6-8 read `ESU`** → platform 105, AA55, the supported set.
Not `ESN`/`ESC` (ES G2, platform 745). SPIKE-01 does not gate this site.
⚠️ A passed platform gate is **not** a proven control surface: `specs.md`
already calls other platform-105 serials *"same protocol family; untested
silicon"*, and this repo's `0x056E`/`0x056F` map was reverse-engineered from a
passive tap on **one BPS unit**. That check is the bench half of SPIKE-04.
* **⚠️ 4600 W IS FROM A DATASHEET AND IS NOT A MEASUREMENT, and 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
4600 × 0.94 ≈ **4324 W**, and the charge direction moves the other way
(≈ 4894 W drawn AC-side). **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 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: they must be
findable, they must agree, and they must say **4600**. It also grades the
**provenance beside the number** — the datasheet it came from, 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, because `0x0518` appeared
in `config.yaml` and `DOCS.md` for unrelated reasons before this card: a
check whose data could not reach the failure it named, in the file that
exists to complain about those. Four mutations were run against the fixed
version — the probe back to 3000 (0-byte delta), the `DOCS.md` caveat
deleted, `config.yaml` alone "corrected" to 4324 (0-byte delta), and the
probe's entity renamed so the regex finds nothing — and all four are red.
* **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 the defect `test_peakguard.py`'s own §10g precondition exists to prevent.
They are **illustrative, not B's rating, and no longer a placeholder for it**.
* **Two consequences with numbers, recorded in `goodwe/NOTES.md` §1.1b for the
cards that own them.** At 94 % battery-to-AC a round trip is ≈ 88 %, 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. And **< 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 a
full recalibration of B moves 33 % more energy and **MAINT-05's 12 h `drain`
phase timeout was derived on A**; it should be re-checked against B rather
than assumed to carry.
**Every pack is recalibrated on its own schedule, and only one moves power at a
time (MAINT-05).** §12's monthly cycle drains a pack to empty and refills it to
true its coulomb counter. There was exactly one `Maintenance` state machine and