From cfafdffaedf787e9095e7cc927fcc0386cdfc934 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 24 Sep 2026 18:50:13 +0200 Subject: [PATCH 1/2] 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 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 Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU --- goodwe_controller/CHANGELOG.md | 327 ++++++++++++++++++++++++++++++++- goodwe_controller/config.yaml | 2 +- 2 files changed, 325 insertions(+), 4 deletions(-) diff --git a/goodwe_controller/CHANGELOG.md b/goodwe_controller/CHANGELOG.md index ab9d298..1b6c752 100644 --- a/goodwe_controller/CHANGELOG.md +++ b/goodwe_controller/CHANGELOG.md @@ -1,5 +1,307 @@ # Changelog +## 0.11.0 + +**A MINOR BUMP, AND ONE NEW OPTION IS THE WHOLE REASON FOR THE SECOND DIGIT: +`pv_entity` ([[TEL-11]]).** The option surface moves - **75 keys -> 76**, +`options:` and `schema:` in step - which is 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.** No key was removed, no default moved, no enum widened, +and **no rule was added to `validate.py`** - the validator does not mention +`pv_entity` at all, because nothing else reads it and it cannot conflict with +anything. The new key is `str?` - **note the trailing `?`, it is OPTIONAL** - +with an `options:` default of `""`, so a blob written on 0.10.0 or 0.10.1 does +not carry it, the Supervisor's merge supplies the empty string, and the empty +string is *off*. ⚠️ **A stored options blob that starts the add-on on 0.10.1 +starts it on 0.11.0. There is no `SystemExit(78)` to plan around on this hop**, +and a site that presses *Update* and changes nothing else gets an add-on that +behaves exactly as its 0.10.1 did. + +⚠️ **WHAT YOU ARE ACTUALLY DECIDING, IN ONE LINE EACH. TWO TICKETS HERE ARE +OBSERVABLE ON AN INSTALLED ADD-ON, AND NEITHER CHANGES A WATT.** + +* [[SAFETY-56]] - **three messages stop naming your hardware as the fault** in a + state where the add-on cannot tell a dead register from a battery that is + simply empty at its BMS floor. **Text only; the behaviour is measured + byte-identical.** If you have ever opened Developer Tools because the add-on + told you a sensor had failed, and found the sensor working, this is that + defect. On the reference install the state it misdiagnoses arrives on most + nights and on every cloudy morning after a deep discharge. +* [[TEL-11]] - **one new optional key, `pv_entity`, and nothing whatever happens + until you fill it in.** Set it and the add-on records what your solar is + producing beside what the meter and the battery are doing. ⚠️ **It is not a + control input and it does not change what the loop does.** + +⚠️ **THERE IS NO CONTROL-LAW CHANGE IN THIS RELEASE, WHICH IS WORTH SAYING +BECAUSE 0.10.0 HAD ONE AND NOTHING IN ITS PREAMBLE COULD WARN YOU.** +`app/control.py` is not in this release's diff at all, and neither are +`allocator.py`, `arbiter.py`, `arbitrage.py`, `clamp.py`, `peakguard.py`, +`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: battery-power register is not following its own command` | `SAFETY-37: 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: is missing or not numeric` | `battery power: no usable value this cycle from - 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. + +**Full account: `DECISIONS/SAFETY-56-what-the-gate-cannot-tell-apart.md`.** + +### TEL-11 — `pv_entity`: PV production gets recorded, and stays out of the control law + +⚠️ **UNSET, THIS IS NOT A CHANGE AT ALL - AND THAT IS MEASURED RATHER THAN +ASSERTED.** `pv_entity` ships empty, which is what **every** install that +upgrades into this version lands on. Empty means the add-on **reads no entity, +announces no entity, publishes no topic, shows no status row and adds no page +row.** `tools/tel11_pv_trace.py` drives five grid behaviours × eight +PV-sensor behaviours through 24 cycles of the **shipped** `Controller`, on +`release/1.0` and on this tree, and hashes two separate documents - one of +**control** decisions and one of **telemetry**: + +| document | tree | `pv_entity` | sha256 | +|---|---|---|---| +| control | `release/1.0` | unset | `846a10a5b89e3210…` | +| control | TEL-11 | unset | `846a10a5b89e3210…` | +| control | TEL-11 | **set** | `846a10a5b89e3210…` | +| telemetry | `release/1.0` | unset | `a443a6ceaffe072b…` | +| telemetry | TEL-11 | unset | `a443a6ceaffe072b…` | +| telemetry | TEL-11 | **set** | `4e6d1cb289a95722…` | + +**Read the two `unset` rows first.** They are byte-identical in **both** +documents, which is a stronger claim than "the control path is untouched": an +install that does not set the option sees no change of any kind, announcements +and status rows included. **Then read the `set` row of the control document** - +identical again, while telemetry moves. ⚠️ The tool **REFUSES** unless the two +trees' `app/` fingerprints differ and unless exactly one side reacts to the +option, so a matching pair cannot be two runs of the same tree. + +⚠️ **IT IS NOT A CONTROL INPUT, AND IT DOES NOT CHANGE WHAT THE LOOP DOES.** It +is instrumentation and nothing else. It is not read by the control law, the +allocator, the arbiter, the peak guard, the maintenance cycle or any watchdog; it +is **not part of `inputs_ok`**, so a PV integration that breaks cannot stop your +battery being regulated; and its status row is a **caution** (`warn`), never a +`bad` row, so a telemetry channel can never put a battery site into NOT READY. +`test_pv.py` part 6 drives 40 cycles of the real `cycle()` and asserts the +commands are bit-identical across unset / no reading / 0 W / 8 kW, with a +**computed** census of all 33 modules under `app/` showing the material is +mentioned in exactly three of them: `pv.py`, `main.py`, `mqtt.py`. Wiring +production into the control law would be a behaviour change on a system driving a +real battery - and it would destroy the measurement, because the loop would then +be reacting to the thing being measured. + +**WHAT YOU GET IF YOU DO SET IT.** One new MQTT-discovery sensor, +`sensor.goodwe_pv_power`, `device_class: power` and ⚠️ **`state_class: +measurement`** - which is the point of the row, because that is what puts the +number into the recorder's long-term statistics, and a week of statistics is what +the attribution needs. **Positive is producing.** The unit is **read off your +entity's own `unit_of_measurement`** - `mW`, `W`, `kW`, `MW`, `GW`, `TW`, Home +Assistant's own power units, every factor an exact power of ten. A sensor with +**no** unit, or with a unit that is not power, is **refused by name, and the +status page says which entity and which unit.** There is deliberately no +magnitude heuristic: nothing can separate 30 W of dusk from 30 kW of noon, and a +silent factor of 1000 in the one channel that exists to be believed a week later +is worse than a refusal you can read. + +⚠️ **ABSENCE IS PUBLISHED AS `unknown`, NOT AS 0 AND NOT SKIPPED. THIS IS THE +DESIGN DECISION WORTH YOUR ATTENTION.** + +| what happened | `sensor.goodwe_pv_power` | the status page | +|---|---|---| +| `pv_entity` not set | the entity does not exist | no row at all | +| set, sensor answered 4100 W | `4100.0` | `PV 4 100 W` | +| set, sensor answered 0 W | `0.0` | `PV 0 W` | +| set, sensor unavailable / missing / bad unit | `unknown` | `PV —`, plus a caution naming the reason | + +The last two rows are why the key is shaped this way. `publish()` normally +**drops** a `None`, which is right for `circulation` - there the last figure +standing beats a zero that reads as health - and **exactly wrong here**: a +dropped row leaves Home Assistant holding the last number it was told, for ever, +and the recorder then draws a **flat line through the outage**, which is +indistinguishable from a measurement. Anyone reading that week back would +conclude the sun was not shining, ⚠️ **which is precisely the conclusion this +channel exists to TEST rather than to manufacture.** So the absence is published, +using Home Assistant's own no-value payload (`mqtt/const.py` declares +`PAYLOAD_NONE = "None"` and `mqtt/sensor.py` tests it *before* the numeric +branch), the entity's state becomes **`unknown`**, and the recorder stores a +**gap**. A failed read also **clears** the previous good one: the value never +outlives its own sensor. + +⚠️ **WHY IT IS A HOME ASSISTANT ENTITY AND NOT A REGISTER.** The RS485 bus this +add-on speaks carries the **battery** inverter it drives. On nearly every site the +PV inverter is a different box from a different vendor on its own connection, +reporting into Home Assistant through its own integration - so this takes +whatever `sensor.*` your PV integration already provides, **any brand Home +Assistant can read**, and nothing anywhere assumes it is on the bus. + +**THERE IS NO `pv_invert`, AND THAT IS NOT AN OVERSIGHT.** `meter_invert` and +`batt_invert` exist because those two quantities are **bipolar and are read by +the control path**, where a reversed sensor hands the site a plausible number it +acts on. Production is unipolar and is read by **nothing**, so a reversed sensor +cannot mislead a decision, and it announces itself as **−4 kW at noon** in the +very history this key exists to create. A flag would only let an installer hide +it. There is no negative-value alarm either: a PV inverter's own standby draw +makes a small negative reading at night ordinary, so any threshold would be a +number nobody has measured. + +⚠️ **WHAT THIS RELEASE DOES NOT DO, AND IT IS THE REASON THE CARD EXISTS.** It +does not answer [[OPT-10]]'s question. That card measured this site over sixteen +days: the pack reached 90 % SoC on **two** of them, and **20.5 kWh of surplus +left the site while the pack still had room** - 12.6 kWh of it with the battery +not charging at all - and it could not say **why**, because PV production was +recorded **nowhere**. Three causes need three different fixes (no irradiance: +nothing to fix; a charge-power limit; the optimiser declining to charge, which is +the only software defect), and guessing between them is how an optimiser gets +tuned to solve a cloud problem. **0.11.0 adds the channel. The attribution needs +a week of history through it, and is OPT-10's own card.** So the reason to set +`pv_entity` on the day you take this update is that the clock on that week starts +when you do. + +**Full account: `DECISIONS/TEL-11-pv-production.md`, with the operator's version +under "PV production" in `DOCS.md`.** + ## 0.10.1 **A PATCH RELEASE, AND ONE TICKET IS THE WHOLE OF IT: [[SAFETY-57]].** The last @@ -50,8 +352,13 @@ operator can observe on an installed add-on - and are excluded for the reason field history found are NOT in this release**: [[SAFETY-56]] (the stale-reading message says *"is not following its own command"* when the true cause is an empty pack, and the NOT READY row that goes with it) and [[SAFETY-58]] (a trip -on the first read after a rest). Both are open, both are wording or bound -changes rather than deadlocks, and neither was allowed to delay this one. +on the first read after a rest). Both were open when this section was cut, both +are wording or bound changes rather than deadlocks, and neither was allowed to +delay this one. ⚠️ **[[SAFETY-56]] HAS SINCE BEEN FIXED AND IS IN `## 0.11.0` +ABOVE.** The wording quoted in this paragraph is what **0.10.1 actually ships** +and is left standing for that reason - if the box in front of you reports +`0.10.1`, that is the sentence it will print at you. It is not what 0.11.0 says. +[[SAFETY-58]] is still open. ### SAFETY-57 — the stale-reading verdict gets an exit @@ -168,6 +475,17 @@ a clearance the state could not produce. ⚠️ Its **diagnosis** half still say pack is merely empty; that wording is [[SAFETY-56]]'s and is deliberately untouched here. +⚠️ **AND THAT IS NOW A STATEMENT ABOUT 0.10.1 ONLY — [[SAFETY-56]] REWROTE THE +DIAGNOSIS HALF, AND `## 0.11.0` ABOVE CARRIES THE NEW WORDING.** The quotation +above is left exactly as it stands because it is what the 0.10.1 image prints, +and a reader on 0.10.1 needs to recognise it. The same applies to this entry's +own reading rule two paragraphs up: on 0.11.0 the add-on no longer asserts that +*"the register really is not following"* — it names both explanations and says +it cannot tell them apart — so *"the pair repeats → look at the unit, it happens +once and stops → the pack was empty"* stops being a correction of the message +and becomes the message's own advice. Nothing about **what** the gate does moved +in either card. + ## 0.10.0 ⚠️ **A CONFIG THAT IS LEGAL ON 0.9.0 IS REFUSED ON 0.10.0, AND THAT - NOT THE @@ -389,7 +707,10 @@ out of it that no test in this repository had reached**: [[SAFETY-56]] and `tools/predeploy_check.py` enforces that by itself: run against a box already reporting `0.10.0` it refuses with `THE DEPLOY WOULD BE A NO-OP` and names the runtime commits since the bump. **SAFETY-57's fix is in `## 0.10.1` above**, -which is what a patch number is for. +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. Contents of this release, in merge order since 0.9.0 (`86bdf49`): diff --git a/goodwe_controller/config.yaml b/goodwe_controller/config.yaml index da627ef..5ad4e0c 100644 --- a/goodwe_controller/config.yaml +++ b/goodwe_controller/config.yaml @@ -1,5 +1,5 @@ name: GoodWe RS485 Controller -version: "0.10.1" +version: "0.11.0" slug: goodwe_controller description: >- Drives a GoodWe ES/BP battery inverter over RS485 by emulating its smart -- 2.54.0 From b11bea70d3256704f21b840b87793be743cb53c0 Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 24 Sep 2026 19:01:35 +0200 Subject: [PATCH 2/2] PKG-17: the SAFETY-57 correction pointed at the wrong paragraph 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 Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU --- goodwe_controller/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/goodwe_controller/CHANGELOG.md b/goodwe_controller/CHANGELOG.md index 1b6c752..02a046e 100644 --- a/goodwe_controller/CHANGELOG.md +++ b/goodwe_controller/CHANGELOG.md @@ -478,8 +478,8 @@ untouched here. ⚠️ **AND THAT IS NOW A STATEMENT ABOUT 0.10.1 ONLY — [[SAFETY-56]] REWROTE THE DIAGNOSIS HALF, AND `## 0.11.0` ABOVE CARRIES THE NEW WORDING.** The quotation above is left exactly as it stands because it is what the 0.10.1 image prints, -and a reader on 0.10.1 needs to recognise it. The same applies to this entry's -own reading rule two paragraphs up: on 0.11.0 the add-on no longer asserts that +and a reader on 0.10.1 needs to recognise it. The same applies to the reading +rule in the paragraph above: on 0.11.0 the add-on no longer asserts that *"the register really is not following"* — it names both explanations and says it cannot tell them apart — so *"the pair repeats → look at the unit, it happens once and stops → the pack was empty"* stops being a correction of the message -- 2.54.0