Cuts 0.11.0 from release/1.0 @ 94da37f. Two files: config.yaml and CHANGELOG.md. Nothing is deployed, installed, tagged, or pushed to master — shipping is a separate pull request release/1.0 → master, per docs/RELEASE.md, which landed in this very range.
Why a minor bump and not a patch
TEL-11 adds an option: pv_entity. 75 keys → 76, options: and schema: in step — the same test 0.8.0, 0.9.0 and 0.10.0 were each cut on.
⚠️ And unlike 0.10.0, nothing here can refuse a stored options blob. The key is str? (optional) with an options: default of "", validate.py does not mention it at all, no key was removed, no default moved, no enum widened. A blob that starts the add-on on 0.10.1 starts it on 0.11.0. No SystemExit(78) to plan around on this hop.
The contents table has two rows, and the window had four merges
c9aaf7f
SAFETY-56 — text only, behaviour byte-identical
94da37f
TEL-11 — new optional option, unset on every existing install
CI-9's follow-up (9f6b9f1) and SEC-2 (239e120) are excluded, under PKG-15's rule: AUDIENCE, NOT IMPORTANCE. Neither file is under goodwe_controller/, neither is in the built image (the Dockerfile copies app/ and run.sh), neither is in predeploy_check.py's RUNTIME tuple, and there is no version of this add-on on which an operator can observe either.
CI-9's follow-up changes .gitea/workflows/checks.yml only, and is comment-only within it. ## 0.10.0 excluded CI-9 itself on exactly that reasoning; excluding the follow-up on any other reasoning would make the rule mean nothing.
SEC-2 is the closer call and the section argues it in full rather than asserting it. SEC-2 is by some distance the more consequential of the two — it is what made master refuse a direct push, and it is why this release reaches anyone as a pull request rather than as a push to the branch the Supervisor clones, which is how 0.10.1 got there. It is nonetheless purely a procedure for whoever cuts a release: both files sit outside goodwe_controller/, no install can tell whether that document exists, and an operator reading it learns nothing about 0.11.0 that helps them decide whether to take it. Excluded under the same sentence as CI-9 — importance to this repository is not the test; observability on an installed add-on is.
Both are named in the section with pointers to where they are written down (PR #102 + the comment block at the top of checks.yml; PR #108 + docs/RELEASE.md itself). The section states that a future CI or packaging ticket which changes what the shipped image contains does belong in the table.
What the two entries let an operator decide
### SAFETY-56 — three messages stop blaming healthy hardware. Behaviour byte-identical, and the section cites the evidence rather than the claim: tools/safety56_behaviour_trace.py hashes a sentence-free document built from 2390 one-second reads replayed out of the committed 2026-09-21 bus capture, a 66-cell command × register grid stepped 120 times each, and six driven controller worlds — 6030175ed2ab9a41… on release/1.0 and on this tree, while the sentences differ, and the tool refuses to report "behaviour unchanged" when the sentence halves agree. Reproduced in the CI image on this tree: 6030175ed2ab9a4143e86e89a71d45c8ab041ac1c00121a3e3e023cd2b08996c, capture_reads 2390, sweep_cells 66, worlds 6.
The three surfaces are named in a table with the old and the new wording side by side:
the STALE_READING: line from app/freshness.py, quoted verbatim onto the status page by stale_note() and into the action log by _read_unit();
the SAFETY-37: ERROR line in the add-on log, from app/main.py;
the readiness row on the diagnostics page — the one that read <entity> is missing or not numeric and thereby asserted a false fact about a named entity that was present, numeric and updating.
It also states what the fix does not give you — which of the two worlds it was — and that SoC is stated in these sentences and never wired into them.
### TEL-11 — pv_entity. States plainly, in its own bolded sentence, that it is not a control input and does not change what the loop does: not read by the control law, allocator, arbiter, peak guard, maintenance cycle or any watchdog, not part of inputs_ok, and its status row is a caution and never a bad row, so a telemetry channel can never put a battery site into NOT READY.
Unset behaves exactly as today, and the section prints the trace table rather than asserting it. The three TEL-11 rows re-measured in the CI image on this tree:
unset control 41 cells 846a10a5b89e321086d535825925721e2f791012e8405948d4fd38d3f9d0be5f
set control 41 cells 846a10a5b89e321086d535825925721e2f791012e8405948d4fd38d3f9d0be5f
unset telemetry 42 cells a443a6ceaffe072b44bcd5f2ad946c1ed6c9e01b69e278eca631f7f4d7892164
set telemetry 42 cells 4e6d1cb289a957223ad0205f732a6fec6110ce1f2032c98ad60694e67625274f
Unset is byte-identical in both documents — stronger than "the control path is untouched". Set is byte-identical in the control document while telemetry moves. And absence is published as unknown, not as 0 and not skipped, so a recorder gap is distinguishable from a genuine zero; the section gives the four-state table and explains why a dropped row would manufacture the "there was no surplus" conclusion the channel exists to test.
Three corrections below the new section, without rewriting history
All three quotations are what the 0.10.1 image actually prints, so all three are left standing — a reader on 0.10.1 has to recognise them — each now scoped to 0.10.1 and pointed at ## 0.11.0:
the ## 0.10.1 preamble said SAFETY-56 and SAFETY-58 are "both open";
the ### SAFETY-57 entry said its accusatory diagnosis half was "deliberately untouched here", and its reading rule still asserted "the register really is not following";
the ## 0.10.0 preamble named SAFETY-56 and SAFETY-57 as the two defects the field history found, and gave a forward pointer for SAFETY-57 only.
Both SAFETY-56's and TEL-11's reviewers flagged this.
Verified
The version gate, all four parts of part 11, by running it — not by reading it.
ok 11a. the slicer CLOSES … and it is not empty: 302 lines under '0.11.0'
ok …and it carries EXACTLY ONE contents table
ok 11b. the newest `## ` heading is a VERSION and not `Unreleased`
ok …and it is exactly config.yaml's `version:` ('0.11.0' in config.yaml, '0.11.0' in CHANGELOG.md)
ok 11c. RETIRED_PROVIDERS read off app/price.py — ['eneco_eddy', 'engie_dynamic', 'totalenergies_dynamic']
ok …and every one of the 3 withdrawn id(s) is named in `## 0.11.0`
ok 11d. exactly 1 `## Unreleased` block(s) remain BELOW the newest section
all checks passed
⚠️11c is satisfied honestly, not by appeasement. The three ids are named inside a block that is explicitly scoped to "if you are crossing from a version older than 0.10.0", says in as many words that neither refusal is new in 0.11.0 and neither is news to a site already on 0.10.0 or 0.10.1, states that they are restated only because this is now the newest section and a reader who opens it and stops would otherwise never meet the one class of thing that can stop the add-on starting, and points at ## 0.10.0 for the full account — what to set instead of each id, which tariff a site left on one has really been running, and why all three stay in the price_provider enum. 11d's 0.8.0-era block is untouched; PKG-08 still owns it.
Full suite: 31/31 in the CI image, checks.yml's exact command, purged tree, :ro, one file at a time, under /home/homelab/. (31, not 30 — TEL-11 added test_pv.py.)
tools/predeploy_check.py --deployed-version 0.10.1 now reports a real version change, with the contrast run to prove the claim:
on this branch (b11bea7):
deployed on the box : 0.10.1
shipped in this tree: 0.11.0
⚠️ SKIPPED: --options was not given …
NOT CLEARED (exit 2 — the validator half, correctly, did not run)
on 94da37f, same command:
deployed on the box : 0.10.1
shipped in this tree: 0.10.1
07ba3433ed TEL-11 …
8a6acd9fa2 SAFETY-56 …
❌ THE DEPLOY WOULD BE A NO-OP … 2 commit(s) touching runtime files … since eb88646899
REFUSED (exit 1)
The 2 runtime commits it names are TEL-11 and SAFETY-56 — independent confirmation that CI-9's follow-up and SEC-2 touch no runtime file, which is the exclusion argument above.
⚠️The validator half was NOT run against a current live options blob, and here is exactly what was and was not done. The live blob is reachable only through the Supervisor API on the box, and this branch contacted no box. Run against the stale local export at /home/homelab/live-options.json — dated 2026-09-07, i.e. before both the 0.10.0 and the 0.10.1 installs, so it predates the defaults those versions merged in — the tool answers live options pass this version's validator / CLEARED for deploy / exit 0. Treat that as a weak positive only. The owner should export the current blob and re-run before pressing Update. Independently, the structural argument stands on its own: the only option-surface change in this release is one str? key with an options: default, and validate.py does not mention it.
The second commit
b11bea7 is self-review, text only: the note added to ### SAFETY-57 said the reading rule it qualifies is "two paragraphs up" when it is in the paragraph directly above. A changelog that misdirects a reader to a paragraph that is not there is the small end of exactly the defect this cut fixes three sentences of elsewhere, so it is corrected rather than left. The branch's diff against 94da37f is still the same two files.
Everything under Verified above was re-run on b11bea7: version gate all four parts of 11, 31/31, and the predeploy_check.py pair.
Not done, deliberately
No deploy, no install, no ha command, no write to /addons/, no contact with 192.168.2.158 or 192.168.2.221. master untouched. Not merged and not tagged.
Cuts **0.11.0** from `release/1.0` @ `94da37f`. **Two files: `config.yaml` and `CHANGELOG.md`.** Nothing is deployed, installed, tagged, or pushed to `master` — shipping is a separate pull request `release/1.0` → `master`, per `docs/RELEASE.md`, which landed in this very range.
## Why a minor bump and not a patch
[[TEL-11]] adds an **option**: `pv_entity`. **75 keys → 76**, `options:` and `schema:` in step — the same test 0.8.0, 0.9.0 and 0.10.0 were each cut on.
⚠️ And unlike 0.10.0, **nothing here can refuse a stored options blob**. The key is `str?` (optional) with an `options:` default of `""`, `validate.py` does not mention it at all, no key was removed, no default moved, no enum widened. A blob that starts the add-on on 0.10.1 starts it on 0.11.0. No `SystemExit(78)` to plan around on this hop.
## The contents table has two rows, and the window had four merges
| | |
|---|---|
| `c9aaf7f` | SAFETY-56 — text only, behaviour byte-identical |
| `94da37f` | TEL-11 — new optional option, unset on every existing install |
**CI-9's follow-up (`9f6b9f1`) and SEC-2 (`239e120`) are excluded, under PKG-15's rule: AUDIENCE, NOT IMPORTANCE.** Neither file is under `goodwe_controller/`, neither is in the built image (the `Dockerfile` copies `app/` and `run.sh`), neither is in `predeploy_check.py`'s `RUNTIME` tuple, and there is no version of this add-on on which an operator can observe either.
* **CI-9's follow-up** changes `.gitea/workflows/checks.yml` only, and is comment-only within it. `## 0.10.0` excluded CI-9 itself on exactly that reasoning; excluding the follow-up on any other reasoning would make the rule mean nothing.
* **SEC-2** is the closer call and the section argues it in full rather than asserting it. SEC-2 is by some distance the more consequential of the two — it is what made `master` refuse a direct push, and it is *why this release reaches anyone as a pull request rather than as a push to the branch the Supervisor clones*, which is how 0.10.1 got there. It is nonetheless **purely a procedure for whoever cuts a release**: both files sit outside `goodwe_controller/`, no install can tell whether that document exists, and an operator reading it learns nothing about 0.11.0 that helps them decide whether to take it. Excluded under the same sentence as CI-9 — **importance to this repository is not the test; observability on an installed add-on is.**
Both are named in the section with pointers to where they *are* written down (PR #102 + the comment block at the top of `checks.yml`; PR #108 + `docs/RELEASE.md` itself). The section states that a future CI or packaging ticket which changes what the shipped image contains **does** belong in the table.
## What the two entries let an operator decide
**`### SAFETY-56` — three messages stop blaming healthy hardware.** Behaviour byte-identical, and the section cites the evidence rather than the claim: `tools/safety56_behaviour_trace.py` hashes a sentence-free document built from **2390 one-second reads** replayed out of the committed 2026-09-21 bus capture, a **66-cell command × register grid stepped 120 times each**, and **six driven controller worlds** — `6030175ed2ab9a41…` on `release/1.0` and on this tree, while the sentences differ, and the tool *refuses* to report "behaviour unchanged" when the sentence halves agree. **Reproduced in the CI image on this tree: `6030175ed2ab9a4143e86e89a71d45c8ab041ac1c00121a3e3e023cd2b08996c`, `capture_reads 2390`, `sweep_cells 66`, `worlds 6`.**
The three surfaces are named in a table with the old and the new wording side by side:
1. the **`STALE_READING:` line** from `app/freshness.py`, quoted verbatim onto the **status page** by `stale_note()` and into the **action log** by `_read_unit()`;
2. the **`SAFETY-37:` ERROR line** in the add-on log, from `app/main.py`;
3. the **readiness row** on the diagnostics page — the one that read `<entity> is missing or not numeric` and thereby asserted a false fact about a named entity that was present, numeric and updating.
It also states what the fix does *not* give you — which of the two worlds it was — and that SoC is stated in these sentences and never wired into them.
**`### TEL-11` — `pv_entity`.** States plainly, in its own bolded sentence, that **it is not a control input and does not change what the loop does**: not read by the control law, allocator, arbiter, peak guard, maintenance cycle or any watchdog, not part of `inputs_ok`, and its status row is a caution and never a `bad` row, so a telemetry channel can never put a battery site into NOT READY.
Unset behaves exactly as today, and the section prints the trace table rather than asserting it. **The three TEL-11 rows re-measured in the CI image on this tree:**
```
unset control 41 cells 846a10a5b89e321086d535825925721e2f791012e8405948d4fd38d3f9d0be5f
set control 41 cells 846a10a5b89e321086d535825925721e2f791012e8405948d4fd38d3f9d0be5f
unset telemetry 42 cells a443a6ceaffe072b44bcd5f2ad946c1ed6c9e01b69e278eca631f7f4d7892164
set telemetry 42 cells 4e6d1cb289a957223ad0205f732a6fec6110ce1f2032c98ad60694e67625274f
```
Unset is byte-identical in **both** documents — stronger than "the control path is untouched". Set is byte-identical in the control document while telemetry moves. And **absence is published as `unknown`, not as 0 and not skipped**, so a recorder gap is distinguishable from a genuine zero; the section gives the four-state table and explains why a dropped row would manufacture the "there was no surplus" conclusion the channel exists to test.
## Three corrections below the new section, without rewriting history
All three quotations are what the **0.10.1 image actually prints**, so all three are left standing — a reader on 0.10.1 has to recognise them — each now scoped to 0.10.1 and pointed at `## 0.11.0`:
* the `## 0.10.1` preamble said SAFETY-56 and SAFETY-58 are *"both open"*;
* the `### SAFETY-57` entry said its accusatory diagnosis half was *"deliberately untouched here"*, and its reading rule still asserted *"the register really is not following"*;
* the `## 0.10.0` preamble named SAFETY-56 and SAFETY-57 as the two defects the field history found, and gave a forward pointer for SAFETY-57 only.
Both SAFETY-56's and TEL-11's reviewers flagged this.
## Verified
**The version gate, all four parts of part 11, by running it — not by reading it.**
```
ok 11a. the slicer CLOSES … and it is not empty: 302 lines under '0.11.0'
ok …and it carries EXACTLY ONE contents table
ok 11b. the newest `## ` heading is a VERSION and not `Unreleased`
ok …and it is exactly config.yaml's `version:` ('0.11.0' in config.yaml, '0.11.0' in CHANGELOG.md)
ok 11c. RETIRED_PROVIDERS read off app/price.py — ['eneco_eddy', 'engie_dynamic', 'totalenergies_dynamic']
ok …and every one of the 3 withdrawn id(s) is named in `## 0.11.0`
ok 11d. exactly 1 `## Unreleased` block(s) remain BELOW the newest section
all checks passed
```
⚠️ **11c is satisfied honestly, not by appeasement.** The three ids are named inside a block that is explicitly scoped to *"if you are crossing from a version older than 0.10.0"*, says in as many words that **neither refusal is new in 0.11.0 and neither is news to a site already on 0.10.0 or 0.10.1**, states that they are restated only because this is now the newest section and a reader who opens it and stops would otherwise never meet the one class of thing that can stop the add-on starting, and points at `## 0.10.0` for the full account — what to set instead of each id, which tariff a site left on one has really been running, and why all three stay in the `price_provider` enum. **11d's 0.8.0-era block is untouched; PKG-08 still owns it.**
**Full suite: 31/31** in the CI image, `checks.yml`'s exact command, purged tree, `:ro`, one file at a time, under `/home/homelab/`. (31, not 30 — TEL-11 added `test_pv.py`.)
**`tools/predeploy_check.py --deployed-version 0.10.1` now reports a real version change**, with the contrast run to prove the claim:
```
on this branch (b11bea7):
deployed on the box : 0.10.1
shipped in this tree: 0.11.0
⚠️ SKIPPED: --options was not given …
NOT CLEARED (exit 2 — the validator half, correctly, did not run)
on 94da37f, same command:
deployed on the box : 0.10.1
shipped in this tree: 0.10.1
07ba3433ed TEL-11 …
8a6acd9fa2 SAFETY-56 …
❌ THE DEPLOY WOULD BE A NO-OP … 2 commit(s) touching runtime files … since eb88646899
REFUSED (exit 1)
```
The 2 runtime commits it names are TEL-11 and SAFETY-56 — independent confirmation that CI-9's follow-up and SEC-2 touch no runtime file, which is the exclusion argument above.
⚠️ **The validator half was NOT run against a current live options blob, and here is exactly what was and was not done.** The live blob is reachable only through the Supervisor API on the box, and this branch contacted no box. Run against the stale local export at `/home/homelab/live-options.json` — dated **2026-09-07**, i.e. **before both the 0.10.0 and the 0.10.1 installs**, so it predates the defaults those versions merged in — the tool answers `live options pass this version's validator` / `CLEARED for deploy` / exit 0. **Treat that as a weak positive only.** The owner should export the current blob and re-run before pressing *Update*. Independently, the structural argument stands on its own: the only option-surface change in this release is one `str?` key with an `options:` default, and `validate.py` does not mention it.
## The second commit
`b11bea7` is self-review, text only: the note added to `### SAFETY-57` said the reading rule it qualifies is *"two paragraphs up"* when it is in the paragraph directly above. A changelog that misdirects a reader to a paragraph that is not there is the small end of exactly the defect this cut fixes three sentences of elsewhere, so it is corrected rather than left. The branch's diff against `94da37f` is still the same two files.
Everything under **Verified** above was re-run on `b11bea7`: version gate all four parts of 11, **31/31**, and the `predeploy_check.py` pair.
## Not done, deliberately
No deploy, no install, no `ha` command, no write to `/addons/`, no contact with 192.168.2.158 or 192.168.2.221. `master` untouched. Not merged and not tagged.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
Two files, and no third. config.yaml 0.10.1 -> 0.11.0; CHANGELOG.md gains
`## 0.11.0` and three corrections to sections below it.
A MINOR BUMP, NOT A PATCH. TEL-11 adds an option - `pv_entity`, 75 keys -> 76 in
`options:` and `schema:` together - which is the same test 0.8.0, 0.9.0 and
0.10.0 were cut on. Nothing here can refuse a stored blob: the key is `str?`
with an `options:` default of "", validate.py does not mention it, no default
moved and no enum widened, so a blob that starts the add-on on 0.10.1 starts it
on 0.11.0.
THE TABLE HAS TWO ROWS AND THE WINDOW HAD FOUR MERGES. CI-9's follow-up
(9f6b9f1, `.gitea/workflows/checks.yml` only) and SEC-2 (239e120,
`docs/RELEASE.md` + README.md) are excluded under PKG-15's rule - AUDIENCE, NOT
IMPORTANCE. Neither file is under `goodwe_controller/`, neither is in the built
image, neither is in predeploy_check.py's RUNTIME tuple, and there is no version
of this add-on on which an operator can observe either. SEC-2 is the closer call
and the section argues it in full rather than asserting it: it is the more
consequential ticket of the two - it is why this release is a pull request
rather than a push to master - and it is still a procedure for whoever cuts a
release, not something the person deciding whether to press Update can act on.
Both are named in the section with pointers to PR #102 / checks.yml and PR #108
/ docs/RELEASE.md.
WHAT THE ENTRIES LET AN OPERATOR DECIDE. SAFETY-56: behaviour byte-identical
(6030175ed2ab..., reproduced in the CI image over 2390 capture reads, a 66-cell
grid stepped 120 times and six driven worlds), three surfaces named with the old
and new wording side by side - the STALE_READING line quoted onto the action log
and status page, the SAFETY-37 ERROR log line, and the readiness row that
asserted a false fact about a named entity. TEL-11: unset is byte-identical in
BOTH the control and telemetry documents, set is byte-identical in the control
document only; the three TEL-11 rows of that table were re-measured in the CI
image on this tree (control 846a10a5... twice, telemetry a443a6ce... / 4e6d1cb2...).
It is stated plainly that pv_entity is not a control input and does not change
what the loop does, and that absence publishes `unknown` rather than 0.
THREE CORRECTIONS BELOW THE NEW SECTION, WITHOUT REWRITING HISTORY. The 0.10.1
preamble said SAFETY-56 was open; the SAFETY-57 entry said its accusatory
diagnosis half was "deliberately untouched"; the 0.10.0 preamble left SAFETY-56
with no pointer while SAFETY-57 had one. All three quotations are what the
0.10.1 image actually prints and are left standing - a reader on 0.10.1 has to
recognise them - each now scoped to 0.10.1 and pointed at `## 0.11.0`.
VERIFIED. test_version_gate.py part 11 all four: 11a one contents table and the
slicer closed (302 lines under '0.11.0'), 11b 0.11.0 == config.yaml, 11c all
three RETIRED_PROVIDERS ids named - scoped to a pre-0.10.0 crossing and pointing
at `## 0.10.0`, which is where the evidence is - 11d still exactly one
`## Unreleased` below, PKG-08's, untouched. 31/31 in the CI image, checks.yml's
exact command, purged tree, `:ro`.
Not deployed and not installed. Shipping is a separate pull request from
release/1.0 into master, per docs/RELEASE.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
Self-review. The note added to `### SAFETY-57` said the reading rule it is
qualifying is "two paragraphs up"; it is in the paragraph directly above - the
same one the note follows. A changelog that misdirects a reader to a paragraph
that is not there is the small end of exactly the defect this cut is fixing
three sentences of elsewhere, so it is corrected rather than left.
Text only, one line pair, still two files in the branch's diff.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
## 0.10.0's new forward pointer sends the reader the wrong way down the file
Non-blocking. One word, and I am not amending the head for it — see the last paragraph.
CHANGELOG.md line 712-713, in the sentence added by this PR at the end of ## 0.10.0's no-op paragraph:
SAFETY-57's fix is in ## 0.10.1 above, which is what a patch number is for. ⚠️AND SAFETY-56'S IS IN ## 0.11.0, which is where it went because it landed after 0.10.1 had already been cut. Both defects named in this paragraph are now fixed; this section, and ## 0.10.1 below it, describe images that still contain them, and are left saying so.
Section order in the shipped file is newest-first: ## 0.11.0 at line 3, ## 0.10.1 at line 305, ## 0.10.0 at line 489. So ## 0.10.1 is above## 0.10.0, not below it.
The antecedent of "it" is ambiguous. Read as "below ## 0.11.0" the sentence is true; read as "below this section" — the natural reading of "X, and Y below it", and the one a reader lands on because "this section" is the subject being enumerated — it is inverted. What a reader does wrongly: someone in ## 0.10.0 who wants the other section that still describes the defective image scrolls down into ## 0.9.0, ## Unreleased and ## 0.8.0 and does not find ## 0.10.1, which is 184 lines up. The same paragraph two lines earlier gets it right — "## 0.10.1 above" — so the file contradicts itself inside one paragraph.
This is the same defect class as the one b11bea7 exists to fix ("the note said the reading rule it qualifies is 'two paragraphs up' when it is in the paragraph directly above"), which is why it is worth naming rather than leaving.
I am merging without amending it. Changing the head would discard the checks / unit green I traced to this PR (run 827) and cost another 8m13s for a one-word locator in a superseded section — while SAFETY-56's fix, which the reference install wants tonight, waits. Suggested wording for a later touch of this file: this section, and \## 0.10.1` above it,`.
## `## 0.10.0`'s new forward pointer sends the reader the wrong way down the file
**Non-blocking. One word, and I am not amending the head for it — see the last paragraph.**
`CHANGELOG.md` line 712-713, in the sentence added by this PR at the end of `## 0.10.0`'s no-op paragraph:
> **SAFETY-57's fix is in `## 0.10.1` above**, which is what a patch number is for. ⚠️ **AND SAFETY-56'S IS IN `## 0.11.0`**, which is where it went because it landed after 0.10.1 had already been cut. Both defects named in this paragraph are now fixed; this section, and **`## 0.10.1` below it**, describe images that still contain them, and are left saying so.
Section order in the shipped file is newest-first: `## 0.11.0` at line 3, `## 0.10.1` at line 305, `## 0.10.0` at line 489. So `## 0.10.1` is **above** `## 0.10.0`, not below it.
The antecedent of *"it"* is ambiguous. Read as *"below `## 0.11.0`"* the sentence is true; read as *"below this section"* — the natural reading of *"X, and Y below it"*, and the one a reader lands on because *"this section"* is the subject being enumerated — it is inverted. **What a reader does wrongly:** someone in `## 0.10.0` who wants the other section that still describes the defective image scrolls *down* into `## 0.9.0`, `## Unreleased` and `## 0.8.0` and does not find `## 0.10.1`, which is 184 lines *up*. The same paragraph two lines earlier gets it right — *"`## 0.10.1` above"* — so the file contradicts itself inside one paragraph.
This is the same defect class as the one `b11bea7` exists to fix (*"the note said the reading rule it qualifies is 'two paragraphs up' when it is in the paragraph directly above"*), which is why it is worth naming rather than leaving.
**I am merging without amending it.** Changing the head would discard the `checks / unit` green I traced to this PR (run 827) and cost another 8m13s for a one-word locator in a superseded section — while SAFETY-56's fix, which the reference install wants tonight, waits. Suggested wording for a later touch of this file: `this section, and \`## 0.10.1\` above it,`.
The stale-options caveat: confirmed adequate in operator-facing text, and here is exactly why
Flagged, not blocking — checking the thing the brief asked me to check rather than taking the PR body's word for it.
tools/predeploy_check.py's validator half was run only against /home/homelab/live-options.json, exported 2026-09-07, i.e. before both the 0.10.0 and the 0.10.1 installs, so it predates the defaults those two versions merged in. It returned CLEARED. The PR body calls that a weak positive and says the owner should export the current blob first. That caveat is in the PR body — the agent's report — and nowhere in CHANGELOG.md.
I checked whether that matters, because a caveat that lives only in a pull request is invisible to whoever presses Update six months from now. It does not, for two reasons:
The changelog never claims a clearance. I grepped the whole ## 0.11.0 section (lines 1-304) for predeploy, CLEARED, live options, blob, export. The only operative sentence is line 74: "tools/predeploy_check.py checks both against your live options blob before you press Update, and on this tree it reports a real version change rather than 0.10.1's no-op." That is an instruction in the imperative about the reader's own blob, not a report of a run the agent did. So there is no false statement in operator-facing text to correct, and no reader can mistake a stale CLEARED for their own result — they were never shown one.
The corrective action is the instruction. The caveat's whole content is "export the current blob and re-run before pressing Update", and line 74 already tells the reader to run the tool against their live blob before pressing Update. The reader who follows the changelog does the right thing without ever needing to know the agent's own run was stale.
What a reader would fail to do if this were wrong: press Update believing a validator had already cleared their actual stored configuration. Line 74's phrasing forecloses that, so the failure mode is not reachable from the shipped text.
The structural argument stands independently and I verified it directly: the only option-surface change in this release is one str? key with an options: default of "", and validate.py does not mention pv_entity at all. Recording it here so the caveat is attached to the merged pull request and not only to a report.
## The stale-options caveat: confirmed adequate in operator-facing text, and here is exactly why
**Flagged, not blocking** — checking the thing the brief asked me to check rather than taking the PR body's word for it.
`tools/predeploy_check.py`'s validator half was run only against `/home/homelab/live-options.json`, exported **2026-09-07**, i.e. before both the 0.10.0 and the 0.10.1 installs, so it predates the defaults those two versions merged in. It returned `CLEARED`. The PR body calls that a weak positive and says the owner should export the current blob first. **That caveat is in the PR body — the agent's report — and nowhere in `CHANGELOG.md`.**
I checked whether that matters, because a caveat that lives only in a pull request is invisible to whoever presses *Update* six months from now. It does not, for two reasons:
1. **The changelog never claims a clearance.** I grepped the whole `## 0.11.0` section (lines 1-304) for `predeploy`, `CLEARED`, `live options`, `blob`, `export`. The only operative sentence is line 74: *"`tools/predeploy_check.py` checks both against your live options blob before you press *Update*, and on this tree it reports a real version change rather than 0.10.1's no-op."* That is an **instruction in the imperative about the reader's own blob**, not a report of a run the agent did. So there is no false statement in operator-facing text to correct, and no reader can mistake a stale CLEARED for their own result — they were never shown one.
2. **The corrective action is the instruction.** The caveat's whole content is *"export the current blob and re-run before pressing Update"*, and line 74 already tells the reader to run the tool against *their* live blob before pressing *Update*. The reader who follows the changelog does the right thing without ever needing to know the agent's own run was stale.
**What a reader would fail to do if this were wrong:** press *Update* believing a validator had already cleared their actual stored configuration. Line 74's phrasing forecloses that, so the failure mode is not reachable from the shipped text.
The structural argument stands independently and I verified it directly: the only option-surface change in this release is one `str?` key with an `options:` default of `""`, and `validate.py` does not mention `pv_entity` at all. **Recording it here so the caveat is attached to the merged pull request and not only to a report.**
Review record — what I re-measured, and how I established the green is this PR's own
Reviewed at head b11bea70d3256704f21b840b87793be743cb53c0, confirmed against the remote (git ls-remote → refs/heads/PKG-17), in a clean clone of my own. Merging.
The green (CI-10)
Statuses are keyed by head SHA on this forge, and this repo has already produced the failure mode: runs 816 and 817 share head b0b24e5d41 across two different pull requests (109 and 110). So I read the run behind the status rather than the status.
checks / unit (pull_request) → /actions/runs/827/jobs/1952. Run 827: head_sha b11bea70… (matches), event pull_request, path checks.yml@refs/pull/114/head, run_attempt 1 — an original execution, not a replayed payload. The job log settles it from the run's own expression evaluation and checkout, not from metadata:
expression 'format('{0}', github.event.pull_request.number)' evaluated to '%!t(string=114)'
expression 'format('{0}', github.base_ref)' evaluated to '%!t(string=release/1.0)'
base release/1.0 94da37fc7205b1c678f8ed6ee34df007548f5265
head refs/pull/114/head b11bea70d3256704f21b840b87793be743cb53c0
GRADED: refs/pull/114/head merged into release/1.0
The payload names PR 114, base release/1.0, base SHA matching this PR's base. Not a laundered skipped: 31 test files all PASS with real per-file durations (test_loop.py 94s, test_peakguard.py 76s, test_watchdog.py 66s), 8m13s wall. test_pv.py and test_version_gate.py both present and passing. I did not rerun anything.
Re-measured on this tree, not read off the PR body
Both spot-checks were run in goodwe-addon-checks:1, -v …:/w:ro, PYTHONDONTWRITEBYTECODE=1, purged tree under /home/homelab/.
Version gate, all four parts of part 11 — reproduced independently:
ok 11a … it is not empty: 302 lines under '0.11.0'
ok …and it carries EXACTLY ONE contents table
ok 11b … exactly config.yaml's `version:` ('0.11.0' in config.yaml, '0.11.0' in CHANGELOG.md)
ok 11c … every one of the 3 withdrawn id(s) is named in `## 0.11.0`
ok 11d. exactly 1 `## Unreleased` block(s) remain BELOW the newest section
all checks passed
SAFETY-56 behaviour hash — recomputed, matches the claim character for character:
TEL-11 trace rows — all four recomputed, all four match:
unset control 41 cells 846a10a5b89e321086d535825925721e2f791012e8405948d4fd38d3f9d0be5f
set control 41 cells 846a10a5b89e321086d535825925721e2f791012e8405948d4fd38d3f9d0be5f
unset telemetry 42 cells a443a6ceaffe072b44bcd5f2ad946c1ed6c9e01b69e278eca631f7f4d7892164
set telemetry 42 cells 4e6d1cb289a957223ad0205f732a6fec6110ce1f2032c98ad60694e67625274f
These were measured, not copied.
Identity — the thing that actually makes the Supervisor offer the update
config.yaml is a one-line diff: version: "0.10.1" → "0.11.0". Diffed against live master (0f14c2a) as well as against the base: slug: goodwe_controller unchanged, and so are name, ingress_port: 8099, panel_title, panel_icon, arch, init, startup, boot, homeassistant_api, hassio_api, url. No repeat of the September swap — the add-on keeps its options and its /data.
Option surface parsed rather than counted by eye: 75 → 76, options: and schema: in step, pv_entity = "" / str?. validate.py does not mention the key. A blob that starts 0.10.1 starts 0.11.0.
The CI-9 / SEC-2 exclusion — rule checked against the mechanism, not the prose
The rule is audience, not importance, and all three of its tests hold mechanically:
RUNTIME in predeploy_check.py is app/, translations/, config.yaml, Dockerfile, run.sh, requirements.txt, build.yaml — neither .gitea/workflows/checks.yml, README.md nor docs/RELEASE.md is in it.
Dockerfile copies exactly app/ and run.sh.
Touched files, from git: CI-9 follow-up → .gitea/workflows/checks.yml only. SEC-2 → README.md + docs/RELEASE.md only. Both outside goodwe_controller/.
Applied consistently: ## 0.10.1 already excludes CI-9 and PKG-15 on the identical sentence. Excluding SEC-2 is defensible — it is the more consequential ticket, and the section says so in as many words rather than hiding it, then excludes it on observability and points at PR #108 and docs/RELEASE.md. A release note is not a repository changelog; the reader is deciding whether to press Update on a regulating battery, and no install can tell whether docs/RELEASE.md exists.
The contents table's merge order is correct against history: c9aaf7f (SAFETY-56) then 94da37f (TEL-11), the four merges since the 0.10.1 cut b0b24e5 being those two plus 9f6b9f1 and 239e120. The preamble's claim that control.py, allocator.py, arbiter.py, arbitrage.py, clamp.py, peakguard.py, maintenance.py and watchdog.py are absent from this release's diff is true — I checked the file list of all four merges.
Operator text
Read as someone on 0.10.1. Both things the brief cares about are stated in their own bolded sentences, not buried: TEL-11 is "not a control input and it does not change what the loop does", unset means "reads no entity, announces no entity, publishes no topic, shows no status row"; SAFETY-56 is "TEXT ONLY, BEHAVIOUR BYTE-IDENTICAL" in the contents table itself. The three stale-wording corrections are all three present and all three maintain the distinction that matters: the original quotations are left standing verbatim as unchanged context, with scoping appended — what the 0.10.1 image prints stays recognisable to someone reading it on 0.10.1.
11c is satisfied honestly and not by appeasement. The three withdrawn ids are named inside a block scoped to "if you are crossing from a version older than 0.10.0" which states in terms that neither refusal is new in 0.11.0 and neither is news to a site on 0.10.0 or 0.10.1, and points at ## 0.10.0 for the account. That is truthful for a reader on 0.10.1, not merely gate-passing.
One accuracy note, below the threshold for its own comment
The SAFETY-56 table is captioned "THE THREE SURFACES" but its three rows span four surfaces — row 1 covers the action log and the status page, because freshness.py's STALE_READING: line is quoted onto both. The row says so explicitly and names both, so nobody is misdirected and every surface is findable; "three wordings" would just be the exact word. Not worth a commit.
Findings
Two, both non-blocking, posted separately: the ## 0.10.0 forward pointer's inverted direction word, and the placement of the stale-options caveat. Neither touches whether the Supervisor offers 0.11.0 or what an operator is told about SAFETY-56 or TEL-11.
## Review record — what I re-measured, and how I established the green is this PR's own
Reviewed at head `b11bea70d3256704f21b840b87793be743cb53c0`, confirmed against the remote (`git ls-remote` → `refs/heads/PKG-17`), in a clean clone of my own. **Merging.**
### The green (CI-10)
Statuses are keyed by head SHA on this forge, and this repo has already produced the failure mode: runs **816** and **817** share head `b0b24e5d41` across **two different pull requests** (109 and 110). So I read the run behind the status rather than the status.
`checks / unit (pull_request)` → `/actions/runs/827/jobs/1952`. Run 827: `head_sha b11bea70…` (matches), `event pull_request`, `path checks.yml@refs/pull/114/head`, **`run_attempt 1`** — an original execution, not a replayed payload. The job log settles it from the run's own expression evaluation and checkout, not from metadata:
```
expression 'format('{0}', github.event.pull_request.number)' evaluated to '%!t(string=114)'
expression 'format('{0}', github.base_ref)' evaluated to '%!t(string=release/1.0)'
base release/1.0 94da37fc7205b1c678f8ed6ee34df007548f5265
head refs/pull/114/head b11bea70d3256704f21b840b87793be743cb53c0
GRADED: refs/pull/114/head merged into release/1.0
```
The payload names **PR 114**, base **`release/1.0`**, base SHA matching this PR's base. Not a laundered `skipped`: **31 test files all `PASS`** with real per-file durations (`test_loop.py` 94s, `test_peakguard.py` 76s, `test_watchdog.py` 66s), 8m13s wall. `test_pv.py` and `test_version_gate.py` both present and passing. I did **not** rerun anything.
### Re-measured on this tree, not read off the PR body
Both spot-checks were run in `goodwe-addon-checks:1`, `-v …:/w:ro`, `PYTHONDONTWRITEBYTECODE=1`, purged tree under `/home/homelab/`.
**Version gate, all four parts of part 11** — reproduced independently:
```
ok 11a … it is not empty: 302 lines under '0.11.0'
ok …and it carries EXACTLY ONE contents table
ok 11b … exactly config.yaml's `version:` ('0.11.0' in config.yaml, '0.11.0' in CHANGELOG.md)
ok 11c … every one of the 3 withdrawn id(s) is named in `## 0.11.0`
ok 11d. exactly 1 `## Unreleased` block(s) remain BELOW the newest section
all checks passed
```
**SAFETY-56 behaviour hash** — recomputed, matches the claim character for character:
```
6030175ed2ab9a4143e86e89a71d45c8ab041ac1c00121a3e3e023cd2b08996c
capture_reads 2390 sweep_cells 66 worlds 6
```
**TEL-11 trace rows** — all four recomputed, all four match:
```
unset control 41 cells 846a10a5b89e321086d535825925721e2f791012e8405948d4fd38d3f9d0be5f
set control 41 cells 846a10a5b89e321086d535825925721e2f791012e8405948d4fd38d3f9d0be5f
unset telemetry 42 cells a443a6ceaffe072b44bcd5f2ad946c1ed6c9e01b69e278eca631f7f4d7892164
set telemetry 42 cells 4e6d1cb289a957223ad0205f732a6fec6110ce1f2032c98ad60694e67625274f
```
These were measured, not copied.
### Identity — the thing that actually makes the Supervisor offer the update
`config.yaml` is a **one-line diff**: `version: "0.10.1"` → `"0.11.0"`. Diffed against live `master` (`0f14c2a`) as well as against the base: **`slug: goodwe_controller` unchanged**, and so are `name`, `ingress_port: 8099`, `panel_title`, `panel_icon`, `arch`, `init`, `startup`, `boot`, `homeassistant_api`, `hassio_api`, `url`. No repeat of the September swap — the add-on keeps its options and its `/data`.
Option surface parsed rather than counted by eye: **75 → 76**, `options:` and `schema:` in step, `pv_entity` = `""` / `str?`. `validate.py` does not mention the key. A blob that starts 0.10.1 starts 0.11.0.
### The CI-9 / SEC-2 exclusion — rule checked against the mechanism, not the prose
The rule is *audience, not importance*, and all three of its tests hold mechanically:
* `RUNTIME` in `predeploy_check.py` is `app/`, `translations/`, `config.yaml`, `Dockerfile`, `run.sh`, `requirements.txt`, `build.yaml` — neither `.gitea/workflows/checks.yml`, `README.md` nor `docs/RELEASE.md` is in it.
* `Dockerfile` copies exactly `app/` and `run.sh`.
* Touched files, from git: CI-9 follow-up → `.gitea/workflows/checks.yml` only. SEC-2 → `README.md` + `docs/RELEASE.md` only. Both outside `goodwe_controller/`.
Applied consistently: `## 0.10.1` already excludes CI-9 and PKG-15 on the identical sentence. **Excluding SEC-2 is defensible** — it is the more consequential ticket, and the section says so in as many words rather than hiding it, then excludes it on observability and points at PR #108 and `docs/RELEASE.md`. A release note is not a repository changelog; the reader is deciding whether to press *Update* on a regulating battery, and no install can tell whether `docs/RELEASE.md` exists.
The contents table's merge order is correct against history: `c9aaf7f` (SAFETY-56) then `94da37f` (TEL-11), the four merges since the 0.10.1 cut `b0b24e5` being those two plus `9f6b9f1` and `239e120`. The preamble's claim that `control.py`, `allocator.py`, `arbiter.py`, `arbitrage.py`, `clamp.py`, `peakguard.py`, `maintenance.py` and `watchdog.py` are absent from this release's diff is **true** — I checked the file list of all four merges.
### Operator text
Read as someone on 0.10.1. Both things the brief cares about are stated in their own bolded sentences, not buried: **TEL-11 is "not a control input and it does not change what the loop does"**, unset means "reads no entity, announces no entity, publishes no topic, shows no status row"; **SAFETY-56 is "TEXT ONLY, BEHAVIOUR BYTE-IDENTICAL"** in the contents table itself. The three stale-wording corrections are all three present and all three maintain the distinction that matters: the original quotations are left standing verbatim as unchanged context, with scoping appended — what the 0.10.1 image prints stays recognisable to someone reading it on 0.10.1.
**11c is satisfied honestly and not by appeasement.** The three withdrawn ids are named inside a block scoped to *"if you are crossing from a version older than 0.10.0"* which states in terms that neither refusal is new in 0.11.0 and neither is news to a site on 0.10.0 or 0.10.1, and points at `## 0.10.0` for the account. That is truthful for a reader on 0.10.1, not merely gate-passing.
### One accuracy note, below the threshold for its own comment
The SAFETY-56 table is captioned **"THE THREE SURFACES"** but its three rows span **four** surfaces — row 1 covers the action log *and* the status page, because `freshness.py`'s `STALE_READING:` line is quoted onto both. The row says so explicitly and names both, so nobody is misdirected and every surface is findable; *"three wordings"* would just be the exact word. Not worth a commit.
### Findings
Two, both non-blocking, posted separately: the `## 0.10.0` forward pointer's inverted direction word, and the placement of the stale-options caveat. Neither touches whether the Supervisor offers 0.11.0 or what an operator is told about SAFETY-56 or TEL-11.
admin
merged commit cccb2fa07b into release/1.02026-09-24 19:20:07 +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.
Cuts 0.11.0 from
release/1.0@94da37f. Two files:config.yamlandCHANGELOG.md. Nothing is deployed, installed, tagged, or pushed tomaster— shipping is a separate pull requestrelease/1.0→master, perdocs/RELEASE.md, which landed in this very range.Why a minor bump and not a patch
TEL-11 adds an option:
pv_entity. 75 keys → 76,options:andschema:in step — the same test 0.8.0, 0.9.0 and 0.10.0 were each cut on.⚠️ And unlike 0.10.0, nothing here can refuse a stored options blob. The key is
str?(optional) with anoptions:default of"",validate.pydoes not mention it at all, no key was removed, no default moved, no enum widened. A blob that starts the add-on on 0.10.1 starts it on 0.11.0. NoSystemExit(78)to plan around on this hop.The contents table has two rows, and the window had four merges
c9aaf7f94da37fCI-9's follow-up (
9f6b9f1) and SEC-2 (239e120) are excluded, under PKG-15's rule: AUDIENCE, NOT IMPORTANCE. Neither file is undergoodwe_controller/, neither is in the built image (theDockerfilecopiesapp/andrun.sh), neither is inpredeploy_check.py'sRUNTIMEtuple, and there is no version of this add-on on which an operator can observe either..gitea/workflows/checks.ymlonly, and is comment-only within it.## 0.10.0excluded CI-9 itself on exactly that reasoning; excluding the follow-up on any other reasoning would make the rule mean nothing.masterrefuse a direct push, and it is why this release reaches anyone as a pull request rather than as a push to the branch the Supervisor clones, which is how 0.10.1 got there. It is nonetheless purely a procedure for whoever cuts a release: both files sit outsidegoodwe_controller/, no install can tell whether that document exists, and an operator reading it learns nothing about 0.11.0 that helps them decide whether to take it. Excluded under the same sentence as CI-9 — importance to this repository is not the test; observability on an installed add-on is.Both are named in the section with pointers to where they are written down (PR #102 + the comment block at the top of
checks.yml; PR #108 +docs/RELEASE.mditself). The section states that a future CI or packaging ticket which changes what the shipped image contains does belong in the table.What the two entries let an operator decide
### SAFETY-56— three messages stop blaming healthy hardware. Behaviour byte-identical, and the section cites the evidence rather than the claim:tools/safety56_behaviour_trace.pyhashes a sentence-free document built from 2390 one-second reads replayed out of the committed 2026-09-21 bus capture, a 66-cell command × register grid stepped 120 times each, and six driven controller worlds —6030175ed2ab9a41…onrelease/1.0and on this tree, while the sentences differ, and the tool refuses to report "behaviour unchanged" when the sentence halves agree. Reproduced in the CI image on this tree:6030175ed2ab9a4143e86e89a71d45c8ab041ac1c00121a3e3e023cd2b08996c,capture_reads 2390,sweep_cells 66,worlds 6.The three surfaces are named in a table with the old and the new wording side by side:
STALE_READING:line fromapp/freshness.py, quoted verbatim onto the status page bystale_note()and into the action log by_read_unit();SAFETY-37:ERROR line in the add-on log, fromapp/main.py;<entity> is missing or not numericand thereby asserted a false fact about a named entity that was present, numeric and updating.It also states what the fix does not give you — which of the two worlds it was — and that SoC is stated in these sentences and never wired into them.
### TEL-11—pv_entity. States plainly, in its own bolded sentence, that it is not a control input and does not change what the loop does: not read by the control law, allocator, arbiter, peak guard, maintenance cycle or any watchdog, not part ofinputs_ok, and its status row is a caution and never abadrow, so a telemetry channel can never put a battery site into NOT READY.Unset behaves exactly as today, and the section prints the trace table rather than asserting it. The three TEL-11 rows re-measured in the CI image on this tree:
Unset is byte-identical in both documents — stronger than "the control path is untouched". Set is byte-identical in the control document while telemetry moves. And absence is published as
unknown, not as 0 and not skipped, so a recorder gap is distinguishable from a genuine zero; the section gives the four-state table and explains why a dropped row would manufacture the "there was no surplus" conclusion the channel exists to test.Three corrections below the new section, without rewriting history
All three quotations are what the 0.10.1 image actually prints, so all three are left standing — a reader on 0.10.1 has to recognise them — each now scoped to 0.10.1 and pointed at
## 0.11.0:## 0.10.1preamble said SAFETY-56 and SAFETY-58 are "both open";### SAFETY-57entry said its accusatory diagnosis half was "deliberately untouched here", and its reading rule still asserted "the register really is not following";## 0.10.0preamble named SAFETY-56 and SAFETY-57 as the two defects the field history found, and gave a forward pointer for SAFETY-57 only.Both SAFETY-56's and TEL-11's reviewers flagged this.
Verified
The version gate, all four parts of part 11, by running it — not by reading it.
⚠️ 11c is satisfied honestly, not by appeasement. The three ids are named inside a block that is explicitly scoped to "if you are crossing from a version older than 0.10.0", says in as many words that neither refusal is new in 0.11.0 and neither is news to a site already on 0.10.0 or 0.10.1, states that they are restated only because this is now the newest section and a reader who opens it and stops would otherwise never meet the one class of thing that can stop the add-on starting, and points at
## 0.10.0for the full account — what to set instead of each id, which tariff a site left on one has really been running, and why all three stay in theprice_providerenum. 11d's 0.8.0-era block is untouched; PKG-08 still owns it.Full suite: 31/31 in the CI image,
checks.yml's exact command, purged tree,:ro, one file at a time, under/home/homelab/. (31, not 30 — TEL-11 addedtest_pv.py.)tools/predeploy_check.py --deployed-version 0.10.1now reports a real version change, with the contrast run to prove the claim:The 2 runtime commits it names are TEL-11 and SAFETY-56 — independent confirmation that CI-9's follow-up and SEC-2 touch no runtime file, which is the exclusion argument above.
⚠️ The validator half was NOT run against a current live options blob, and here is exactly what was and was not done. The live blob is reachable only through the Supervisor API on the box, and this branch contacted no box. Run against the stale local export at
/home/homelab/live-options.json— dated 2026-09-07, i.e. before both the 0.10.0 and the 0.10.1 installs, so it predates the defaults those versions merged in — the tool answerslive options pass this version's validator/CLEARED for deploy/ exit 0. Treat that as a weak positive only. The owner should export the current blob and re-run before pressing Update. Independently, the structural argument stands on its own: the only option-surface change in this release is onestr?key with anoptions:default, andvalidate.pydoes not mention it.The second commit
b11bea7is self-review, text only: the note added to### SAFETY-57said the reading rule it qualifies is "two paragraphs up" when it is in the paragraph directly above. A changelog that misdirects a reader to a paragraph that is not there is the small end of exactly the defect this cut fixes three sentences of elsewhere, so it is corrected rather than left. The branch's diff against94da37fis still the same two files.Everything under Verified above was re-run on
b11bea7: version gate all four parts of 11, 31/31, and thepredeploy_check.pypair.Not done, deliberately
No deploy, no install, no
hacommand, no write to/addons/, no contact with 192.168.2.158 or 192.168.2.221.masteruntouched. Not merged and not tagged.🤖 Generated with Claude Code
https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
## 0.10.0's new forward pointer sends the reader the wrong way down the fileNon-blocking. One word, and I am not amending the head for it — see the last paragraph.
CHANGELOG.mdline 712-713, in the sentence added by this PR at the end of## 0.10.0's no-op paragraph:Section order in the shipped file is newest-first:
## 0.11.0at line 3,## 0.10.1at line 305,## 0.10.0at line 489. So## 0.10.1is above## 0.10.0, not below it.The antecedent of "it" is ambiguous. Read as "below
## 0.11.0" the sentence is true; read as "below this section" — the natural reading of "X, and Y below it", and the one a reader lands on because "this section" is the subject being enumerated — it is inverted. What a reader does wrongly: someone in## 0.10.0who wants the other section that still describes the defective image scrolls down into## 0.9.0,## Unreleasedand## 0.8.0and does not find## 0.10.1, which is 184 lines up. The same paragraph two lines earlier gets it right — "## 0.10.1above" — so the file contradicts itself inside one paragraph.This is the same defect class as the one
b11bea7exists to fix ("the note said the reading rule it qualifies is 'two paragraphs up' when it is in the paragraph directly above"), which is why it is worth naming rather than leaving.I am merging without amending it. Changing the head would discard the
checks / unitgreen I traced to this PR (run 827) and cost another 8m13s for a one-word locator in a superseded section — while SAFETY-56's fix, which the reference install wants tonight, waits. Suggested wording for a later touch of this file:this section, and \## 0.10.1` above it,`.The stale-options caveat: confirmed adequate in operator-facing text, and here is exactly why
Flagged, not blocking — checking the thing the brief asked me to check rather than taking the PR body's word for it.
tools/predeploy_check.py's validator half was run only against/home/homelab/live-options.json, exported 2026-09-07, i.e. before both the 0.10.0 and the 0.10.1 installs, so it predates the defaults those two versions merged in. It returnedCLEARED. The PR body calls that a weak positive and says the owner should export the current blob first. That caveat is in the PR body — the agent's report — and nowhere inCHANGELOG.md.I checked whether that matters, because a caveat that lives only in a pull request is invisible to whoever presses Update six months from now. It does not, for two reasons:
## 0.11.0section (lines 1-304) forpredeploy,CLEARED,live options,blob,export. The only operative sentence is line 74: "tools/predeploy_check.pychecks both against your live options blob before you press Update, and on this tree it reports a real version change rather than 0.10.1's no-op." That is an instruction in the imperative about the reader's own blob, not a report of a run the agent did. So there is no false statement in operator-facing text to correct, and no reader can mistake a stale CLEARED for their own result — they were never shown one.What a reader would fail to do if this were wrong: press Update believing a validator had already cleared their actual stored configuration. Line 74's phrasing forecloses that, so the failure mode is not reachable from the shipped text.
The structural argument stands independently and I verified it directly: the only option-surface change in this release is one
str?key with anoptions:default of"", andvalidate.pydoes not mentionpv_entityat all. Recording it here so the caveat is attached to the merged pull request and not only to a report.Review record — what I re-measured, and how I established the green is this PR's own
Reviewed at head
b11bea70d3256704f21b840b87793be743cb53c0, confirmed against the remote (git ls-remote→refs/heads/PKG-17), in a clean clone of my own. Merging.The green (CI-10)
Statuses are keyed by head SHA on this forge, and this repo has already produced the failure mode: runs 816 and 817 share head
b0b24e5d41across two different pull requests (109 and 110). So I read the run behind the status rather than the status.checks / unit (pull_request)→/actions/runs/827/jobs/1952. Run 827:head_sha b11bea70…(matches),event pull_request,path checks.yml@refs/pull/114/head,run_attempt 1— an original execution, not a replayed payload. The job log settles it from the run's own expression evaluation and checkout, not from metadata:The payload names PR 114, base
release/1.0, base SHA matching this PR's base. Not a launderedskipped: 31 test files allPASSwith real per-file durations (test_loop.py94s,test_peakguard.py76s,test_watchdog.py66s), 8m13s wall.test_pv.pyandtest_version_gate.pyboth present and passing. I did not rerun anything.Re-measured on this tree, not read off the PR body
Both spot-checks were run in
goodwe-addon-checks:1,-v …:/w:ro,PYTHONDONTWRITEBYTECODE=1, purged tree under/home/homelab/.Version gate, all four parts of part 11 — reproduced independently:
SAFETY-56 behaviour hash — recomputed, matches the claim character for character:
TEL-11 trace rows — all four recomputed, all four match:
These were measured, not copied.
Identity — the thing that actually makes the Supervisor offer the update
config.yamlis a one-line diff:version: "0.10.1"→"0.11.0". Diffed against livemaster(0f14c2a) as well as against the base:slug: goodwe_controllerunchanged, and so arename,ingress_port: 8099,panel_title,panel_icon,arch,init,startup,boot,homeassistant_api,hassio_api,url. No repeat of the September swap — the add-on keeps its options and its/data.Option surface parsed rather than counted by eye: 75 → 76,
options:andschema:in step,pv_entity=""/str?.validate.pydoes not mention the key. A blob that starts 0.10.1 starts 0.11.0.The CI-9 / SEC-2 exclusion — rule checked against the mechanism, not the prose
The rule is audience, not importance, and all three of its tests hold mechanically:
RUNTIMEinpredeploy_check.pyisapp/,translations/,config.yaml,Dockerfile,run.sh,requirements.txt,build.yaml— neither.gitea/workflows/checks.yml,README.mdnordocs/RELEASE.mdis in it.Dockerfilecopies exactlyapp/andrun.sh..gitea/workflows/checks.ymlonly. SEC-2 →README.md+docs/RELEASE.mdonly. Both outsidegoodwe_controller/.Applied consistently:
## 0.10.1already excludes CI-9 and PKG-15 on the identical sentence. Excluding SEC-2 is defensible — it is the more consequential ticket, and the section says so in as many words rather than hiding it, then excludes it on observability and points at PR #108 anddocs/RELEASE.md. A release note is not a repository changelog; the reader is deciding whether to press Update on a regulating battery, and no install can tell whetherdocs/RELEASE.mdexists.The contents table's merge order is correct against history:
c9aaf7f(SAFETY-56) then94da37f(TEL-11), the four merges since the 0.10.1 cutb0b24e5being those two plus9f6b9f1and239e120. The preamble's claim thatcontrol.py,allocator.py,arbiter.py,arbitrage.py,clamp.py,peakguard.py,maintenance.pyandwatchdog.pyare absent from this release's diff is true — I checked the file list of all four merges.Operator text
Read as someone on 0.10.1. Both things the brief cares about are stated in their own bolded sentences, not buried: TEL-11 is "not a control input and it does not change what the loop does", unset means "reads no entity, announces no entity, publishes no topic, shows no status row"; SAFETY-56 is "TEXT ONLY, BEHAVIOUR BYTE-IDENTICAL" in the contents table itself. The three stale-wording corrections are all three present and all three maintain the distinction that matters: the original quotations are left standing verbatim as unchanged context, with scoping appended — what the 0.10.1 image prints stays recognisable to someone reading it on 0.10.1.
11c is satisfied honestly and not by appeasement. The three withdrawn ids are named inside a block scoped to "if you are crossing from a version older than 0.10.0" which states in terms that neither refusal is new in 0.11.0 and neither is news to a site on 0.10.0 or 0.10.1, and points at
## 0.10.0for the account. That is truthful for a reader on 0.10.1, not merely gate-passing.One accuracy note, below the threshold for its own comment
The SAFETY-56 table is captioned "THE THREE SURFACES" but its three rows span four surfaces — row 1 covers the action log and the status page, because
freshness.py'sSTALE_READING:line is quoted onto both. The row says so explicitly and names both, so nobody is misdirected and every surface is findable; "three wordings" would just be the exact word. Not worth a commit.Findings
Two, both non-blocking, posted separately: the
## 0.10.0forward pointer's inverted direction word, and the placement of the stale-options caveat. Neither touches whether the Supervisor offers 0.11.0 or what an operator is told about SAFETY-56 or TEL-11.