master is what the Home Assistant Supervisor clones, and the Supervisor
compares config.yaml version strings - so 0.11.0 could not be offered to an
installed 0.10.1 without this bump reaching master.
Carries SAFETY-56 (#112), TEL-11 (#113), the CI-9 follow-up (#102) and SEC-2's
docs/RELEASE.md (#108), by way of the 0.11.0 cut (#114).
NOT A FAST-FORWARD. master was not an ancestor of release/1.0: 0f14c2a, the
pre-SEC-2 direct push, exists only on master. Its tree is the same object as
the merge base b0b24e5 (c1fef26), so it carries no content delta, and
`git merge-tree --write-tree` over the two branches returns a tree bit-identical
to cccb2fa's own (6670502). This merge commit therefore contributes exactly
release/1.0's tree - master synthesises nothing.
Reviewed as the release decision, not a re-review of content. Verified from the
remote: slug goodwe_controller and every identity key unchanged (cross-checked
against the live box's Supervisor export), version 0.10.1 -> 0.11.0, and the
green is this PR's own - run 828 / job 1953, path checks.yml@refs/pull/115/head,
PRNUM 115, GRADED refs/pull/115/head merged into master, run_attempt 1, 31 test
files all passing in 8m01s of real work.
predeploy_check.py re-run against the owner's current options (exported
2026-09-24 20:02, 75 keys, pv_entity unset): CLEARED, exit 0. That retires the
stale-2026-09-07-export warning in the pull request body. Option surface
75 -> 76 with only pv_entity added, str? defaulting to "", no key removed, no
default moved, no rule added to validate.py - no SystemExit(78) on this hop.
Merging is not installing. No tag is cut here and `ha store reload` /
`ha addons update` have not been run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
Bumps config.yaml 0.10.1 -> 0.11.0 and adds the `## 0.11.0` section, so the Supervisor offers the update that carries SAFETY-56's wording fix and TEL-11's `pv_entity`.
Reviewed at head b11bea7. Green traced to this PR's own run (827, attempt 1, payload names PR 114 base release/1.0, 31/31 real). Version gate 11a-11d, SAFETY-56's behaviour hash and TEL-11's four trace rows all re-measured on this tree and matching. slug unchanged; option surface 75 -> 76 with no validator rule added.
Two non-blocking findings recorded on the PR.
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
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
Reviewed on the PR. Green traced to run 825 (PRNUM 113, BASEREF release/1.0, attempt 1, real 8m6s execution). Control-path identity independently reproduced on both trees and counter-tested with a planted leak (red on 26/41 cells); all three of the trace tool substituted refusals fire. M7 genuinely red. Three publication states distinct on the wire. 31/31 in the CI image.
Not deployable as merged: config.yaml stays at 0.10.1, so predeploy_check.py reports a NO-OP until PKG cuts a version.
master is what Home Assistant clones. SAFETY-57 fixed a deadlock that parked
the reference install's battery for ~13 minutes while the site exported
2.4 kW, and the Supervisor compares config.yaml version strings - so the fix
could not reach an installed 0.10.0 without this bump reaching master.
Brings SAFETY-57 (#104) and the 0.10.1 cut (#105). master was a strict
ancestor; no conflict was possible.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
2026-09-22 15:54:29 +02:00
2 changed files with 325 additions and 4 deletions
`maintenance.py` or `watchdog.py`. The runtime files that move are
`app/freshness.py` and `app/main.py` (SAFETY-56's three sentences), and
`app/pv.py` - new - with `app/main.py`, `app/mqtt.py`, `config.yaml` and
`translations/` (TEL-11). Contrast [[SAFETY-48]] in `## 0.10.0`, which altered
what the inverter is asked to do while touching no option, no default and no
validator rule, so that nothing a config check could see would mention it. This
release has nothing of that shape in it.
⚠️ **IF YOU ARE CROSSING FROM A VERSION OLDER THAN 0.10.0, THIS SECTION IS
STILL NOT THE ONE THAT CAN REFUSE YOU - `## 0.10.0` BELOW IS.** `ha addons
update` does not walk the versions; it installs 0.11.0 in one step, so every
validator rule added in 0.8.0, 0.9.0 and 0.10.0 arrives at the same moment as
this section's nothing. Two of them can refuse a config that nobody has edited:
> [[SAFETY-39]]'s **rule 10**, added in 0.8.0, against 0.7.0's own shipped
> default `slew_w: 1000` - the ceiling is 2/18 of the site rail, which is
> **555.6 W** on a 5000 W rail.
>
> [[TARIFF-07]]'s three withdrawn `price_provider` ids - **`engie_dynamic`,
> `totalenergies_dynamic` and `eneco_eddy`** - each of which is **still refused
> by name at startup on 0.11.0**, with the same message and for the same
> measured reasons: a 404 from a gateway with no route, a TLS handshake against
> a hostname the certificate does not cover, and a 404 with an empty body.
⚠️ **NEITHER OF THOSE IS NEW IN 0.11.0, AND NEITHER IS NEWS TO A SITE ALREADY ON
0.10.0 OR 0.10.1 - IT HAS ALREADY MET THEM.** They are restated here only
because this is now the newest section, and a reader who opens the newest section
and stops would otherwise never meet the one class of thing that can stop the
add-on starting. **The full account of both is in `## 0.10.0` below**: what to
set instead of each withdrawn id, which tariff a site left on one of them has
really been running, why the refusal *is* the migration, and why all three ids
stay in `config.yaml`'s `price_provider` enum even though `PROVIDERS` is down to
three. **That is the section that withdrew them, it is where the evidence is,
and it is not superseded by this one.** ⚠️ The reference install is on
`price_provider: off` and at `slew_w: 400`, so neither applies to it.
`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.
Contents of this release, in merge order since 0.10.1 (`b0b24e5`):
| | |
|---|---|
| `c9aaf7f` | SAFETY-56 - the gate cannot tell a dead register from a pack that cannot comply, so stop saying it can **⚠️ TEXT ONLY, BEHAVIOUR BYTE-IDENTICAL** |
| `94da37f` | TEL-11 - PV production stops being invisible, and stays out of the control law **⚠️ NEW OPTIONAL OPTION, UNSET ON EVERY EXISTING INSTALL** |
⚠️ **TWO MORE MERGES LANDED IN THIS WINDOW AND ARE DELIBERATELY NOT IN THE TABLE
ABOVE: [[CI-9]]'S FOLLOW-UP (`9f6b9f1`) AND [[SEC-2]] (`239e120`). THE RULE IS
[[PKG-15]]'S - AUDIENCE, NOT IMPORTANCE - AND BOTH FAIL IT THE SAME WAY.** This
file is the **add-on's** release note: the text the Supervisor renders beside the
version number, read by somebody deciding whether to press *Update* on a battery
that is currently regulating. A row here has to be something that reader can act
on.
* **CI-9's follow-up changes one file, `.gitea/workflows/checks.yml`**, exactly
as CI-9 itself did, and it is comment-only within that file. It is not under
`goodwe_controller/`, it is not in `predeploy_check.py`'s `RUNTIME` tuple, it
is not in the built image, and **there is no version of this add-on on which an
operator can observe it.** `## 0.10.0` excluded CI-9 on exactly that reasoning;
excluding the follow-up on any other reasoning would make the rule mean
nothing.
* **SEC-2 adds `docs/RELEASE.md` and two lines to `README.md`, and this one is
the closer call, so here is the whole of it.** SEC-2 is by some distance the
more consequential of the two: it is the ticket that made `master` refuse a
direct push, and it is *why this release reaches you as a pull request into
`release/1.0` and then into `master`, rather than as a push straight to the
branch the Supervisor clones* - which is how 0.10.1 got there. It is also
**purely a procedure for whoever cuts a release**; both files sit outside
`goodwe_controller/`, neither is in the image, and no install can tell whether
that document exists or what it says. An operator reading it learns nothing
about 0.11.0 that helps them decide whether to take it. So it is excluded, and
it is excluded under the same sentence as CI-9: ⚠️ **importance to this
repository is not the test. Observability on an installed add-on is.**
⚠️ **AND BOTH ARE WRITTEN DOWN, JUST NOT HERE.** CI-9's follow-up is PR #102 plus
the comment block at the top of `.gitea/workflows/checks.yml`, which carries the
measurements in full; SEC-2 is PR #108 plus `docs/RELEASE.md` itself, which is
now the canonical account of the release path and is the procedure this release
followed. ⚠️ **If a later CI or packaging ticket changes what the shipped image
contains, it belongs in this table.** Neither of these does.
### SAFETY-56 — three messages stop blaming healthy hardware
⚠️ **NOTHING THE ADD-ON DOES CHANGES. THREE THINGS IT SAYS DO.** Same
conjunction, same limits, same `MISSING`, same 0 W command, same NOT READY row,
same [[SAFETY-57]] release at 32 s. `tools/safety56_behaviour_trace.py` is what
says so rather than a commit message: it hashes a document with no sentence
anywhere in it, 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**. That document is
`6030175ed2ab9a41…` on `release/1.0` and on this tree, byte for byte, while the
sentences differ. ⚠️ **And the tool REFUSES to report "behaviour unchanged" when
the sentence halves agree**, so an identical pair is a result rather than one
tree compared with itself.
**THE DEFECT, AND IT IS NOT A TYPO.** [[SAFETY-37]]'s freshness gate fires when a
battery-power register holds one value while the standing command moves. That
conjunction is satisfied by **two disjoint worlds**:
1. the register has stopped reporting reality - which is what the gate was built
to catch; or
2. **reality is not moving**, because the source cannot comply with the command.
A BMS refusing a discharge at its floor holds the pack still under any
setpoint, and a register reporting that honestly publishes the same unchanged
number a dead one does.
**Nothing the gate is handed separates them, and three sentences asserted the
first.** ⚠️ **On the one episode this repository has measured, it was the
second.** On 2026-09-21 the pack read **11 %** for the whole 850 s hold, **every
command inside that hold was a discharge**, and the register **answered the first
CHARGE command three seconds later**. All three numbers are read off the
committed capture by `test_freshness.py` part 11a rather than typed there. The
register was healthy and truthful throughout, and the add-on said it had failed.
**THE THREE SURFACES, so you can recognise what moved:**
| where you read it | what 0.10.1 said | what 0.11.0 says |
|---|---|---|
| the **action log** and the **status page** — both quote `freshness.py`'s `STALE_READING:` line verbatim, through `_read_unit()` and `stale_note()` | `… the reading is not following, so it is reported as MISSING` | `… TWO THINGS LOOK LIKE THIS AND THIS GATE CANNOT TELL THEM APART: the register may have stopped reporting reality, or the source may be unable to comply with the command …` |
| the **add-on log**, at ERROR, from `main.py` | `SAFETY-37: <unit> battery-power register is not following its own command` | `SAFETY-37: <unit> battery-power reading did not move under a command that did, WHICH IS NOT BY ITSELF A VERDICT ON THE REGISTER` |
| the **readiness row** on the diagnostics page | `battery power: <entity> is missing or not numeric` | `battery power: no usable value this cycle from <entity> - and this row cannot tell which of two things that is … Confirm the entity in Developer Tools before replacing it` |
⚠️ **THE THIRD ROW IS THE ONE THAT COST AN OPERATOR AN EVENING, AND IT IS THE
WORST THING A DIAGNOSTIC CAN DO: IT ASSERTED A FALSE FACT ABOUT A NAMED
ENTITY.** It read *"is missing or not numeric"* for **any**`None`, and one of
the ways that value becomes `None` is the freshness gate withholding a reading
that is present, numeric and updating - `Freshness.reading()` returns `None`
there **by design**. So the operator opened Developer Tools on the entity the row
named, found it working, and had every reason to conclude the diagnostics were
broken. **It is fixed by saying less, not by looking harder:** the row does not
branch on the gate's state, because the gate already has a surface of its own -
`stale_note()` rides `liveness_notes()` a few rows below with the whole
`STALE_READING` sentence in it - so your discriminator is *"is that row there?"*,
and this row's only job is to stop pre-empting the answer.
⚠️ **WHAT IT DOES NOT GIVE YOU IS WHICH OF THE TWO IT WAS.** Naming which would
need the SoC and the pack floor, and those are deliberately not `freshness.py`'s
inputs - they are [[TEL-08]]'s and [[SAFETY-32]]'s numbers, on their own rows.
**SoC is STATED in these sentences and never WIRED into them**: part 11b drives
the same two worlds at 50 % and at 11 % and the sentence does not move one byte.
What changed is that the add-on now tells you it cannot tell, instead of guessing
and blaming your hardware. **The reading rule is unchanged from
`### SAFETY-57` below**: if the stale/withdrawn pair keeps repeating, the unit
really does want looking at; if it happens once at a low SoC and stops, the pack
was empty and the site has resumed.
⚠️ **AND THE THREE WORDINGS SHARE NO HELPER, ON PURPOSE.** Two of them are log
lines about a **behaviour** and the third is a banner about an **entity**; they
answer different questions ("what did the register do" against "can I trust this
entity"), and one helper serving both would have to drop whichever half the other
caller does not need - and the half it dropped would be the operator's next step.
Drives a GoodWe ES/BP battery inverter over RS485 by emulating its smart
Reference in New Issue
Block a user
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.