PKG-17: cut 0.11.0, so SAFETY-56's wording fix and TEL-11's pv_entity can reach an installed 0.10.1 #114

Merged
admin merged 2 commits from PKG-17 into release/1.0 2026-09-24 19:20:07 +02:00
Owner

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.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU

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
admin added 1 commit 2026-09-24 18:52:24 +02:00
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
admin added 1 commit 2026-09-24 19:02:01 +02:00
PKG-17: the SAFETY-57 correction pointed at the wrong paragraph
checks / unit (pull_request) Successful in 8m13s
b11bea70d3
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
Author
Owner

## 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,`.
Author
Owner

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.

## 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.**
Author
Owner

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.

## 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.0 2026-09-24 19:20:07 +02:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/goodwe-addon#114