Cuts 0.12.0 on release/1.0 so TEL-12 (#116) can reach an installed 0.11.0. Diff: config.yaml (version line) and CHANGELOG.md (new ## 0.12.0 section). Nothing else.
One runtime merge since 0.11.0 (cccb2fa): f863eca TEL-12, a new optional list option pv_entities beside pv_entity. Option surface 76 -> 77, so a minor bump. No validator rule added; an 0.11.0 options blob validates untouched (TEL-12's §G probe against Supervisor 2026.08.0).
The entry tells the operator:
pv_entities is for sites with more than one PV inverter;
it replacespv_entity when set (it is not added to it). An operator upgrading with pv_entity set must include that inverter in the list, where it is counted once. The status page caution names which key won;
if any configured PV entity is absent, the published total is unknown (a recorder gap), never a partial sum;
it is instrumentation only, not a control input. Unset, and with a single entity, it behaves exactly as 0.11.0 (control hash 846a10a5… unmoved);
why it is a second key: with pv_entity declared as a list, Supervisor 2026.08.0 refuses an 0.11.0 install's stored string (Invalid list for option 'pv_entity');
the pre-0.10.0 crossing note (slew rule 10, the three withdrawn price_provider ids) points at ## 0.10.0, as ## 0.11.0 does;
the predeploy check must be given the bare data.options dict, not the wrapped ha apps info --raw-json export (SAFETY-60, open).
Verification
Version gate 11a–11d: pass. Exactly one contents table, ## 0.12.0 == config.yaml0.12.0, all 3 RETIRED_PROVIDERS named, exactly 1 ## Unreleased below.
Full suite: ran in goodwe-addon-checks:1 with checks.yml's exact command, on a purged tree mounted :ro, one file at a time. 31/31 PASS.
predeploy_check.py --deployed-version 0.11.0: reports 0.11.0 -> 0.12.0, which is a real version change and no longer the NO-OP refusal it gave at f863eca. On a valid bare options blob the check clears.
SAFETY-60, reproduced locally so the changelog warning is based on a test: an options blob with slew_w: 5000 was REFUSED when passed bare, and printed CLEARED, exit 0 when passed wrapped as {"data":{"options":…}}.
Not deployed, not tagged. master was not touched. Shipping needs a separate release/1.0 -> master PR.
Cuts **0.12.0** on `release/1.0` so TEL-12 (#116) can reach an installed 0.11.0. **Diff: `config.yaml` (version line) and `CHANGELOG.md` (new `## 0.12.0` section). Nothing else.**
One runtime merge since 0.11.0 (`cccb2fa`): `f863eca` TEL-12, a new optional list option `pv_entities` beside `pv_entity`. Option surface 76 -> 77, so a **minor** bump. No validator rule added; an 0.11.0 options blob validates untouched (TEL-12's §G probe against Supervisor 2026.08.0).
The entry tells the operator:
- `pv_entities` is for sites with more than one PV inverter;
- it **replaces** `pv_entity` when set (it is not added to it). An operator upgrading with `pv_entity` set must include that inverter in the list, where it is counted once. The status page caution names which key won;
- if any configured PV entity is absent, the published total is `unknown` (a recorder gap), **never a partial sum**;
- it is instrumentation only, not a control input. Unset, and with a single entity, it behaves exactly as 0.11.0 (control hash `846a10a5…` unmoved);
- why it is a second key: with `pv_entity` declared as a list, Supervisor 2026.08.0 refuses an 0.11.0 install's stored string (`Invalid list for option 'pv_entity'`);
- the pre-0.10.0 crossing note (slew rule 10, the three withdrawn `price_provider` ids) points at `## 0.10.0`, as `## 0.11.0` does;
- the predeploy check must be given the **bare `data.options` dict**, not the wrapped `ha apps info --raw-json` export (SAFETY-60, open).
## Verification
- **Version gate 11a–11d:** pass. Exactly one contents table, `## 0.12.0` == `config.yaml` `0.12.0`, all 3 `RETIRED_PROVIDERS` named, exactly 1 `## Unreleased` below.
- **Full suite:** ran in `goodwe-addon-checks:1` with `checks.yml`'s exact command, on a purged tree mounted `:ro`, one file at a time. **31/31 PASS.**
- **`predeploy_check.py --deployed-version 0.11.0`:** reports `0.11.0 -> 0.12.0`, which is a real version change and no longer the NO-OP refusal it gave at `f863eca`. On a valid bare options blob the check clears.
- **SAFETY-60, reproduced locally so the changelog warning is based on a test:** an options blob with `slew_w: 5000` was **REFUSED** when passed bare, and printed **CLEARED, exit 0** when passed wrapped as `{"data":{"options":…}}`.
Not deployed, not tagged. `master` was not touched. Shipping needs a separate `release/1.0` -> `master` PR.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
Review of PKG-18 at head 1ad4d3a against base f863eca: sound, no blocking findings.
Identity.slug: goodwe_controller is unchanged, so this is the same add-on and its options and /data carry over. version moves "0.11.0" -> "0.12.0". The diff has two files: config.yaml (the version line only) and CHANGELOG.md (+132).
Gate 11a–11d. I ran test_version_gate.py in goodwe-addon-checks:1 on a fresh clone at 1ad4d3a, mounted :ro, and it printed all checks passed (exit 0). There is one contents table, ## 0.12.0 equals config.yaml, all three RETIRED_PROVIDERS (eneco_eddy, engie_dynamic, totalenergies_dynamic) are named, and one ## Unreleased remains below. 11c's text reads correctly for a 0.11.0 reader: it says the ids are "not new here" and that a site on 0.10.0 or later "has already met both". The 555.6 W figure matches validate.py:1137 (SLEW_RAIL_FRAC * 5000).
Operator claims, checked against the code at f863eca and DECISIONS/TEL-12-several-pv-inverters.md:
Replaces, not adds.pv.entities() returns the list when it is non-empty, and pv_entity is read only when the list is empty. The ids are de-duplicated in order with dict.fromkeys, so an inverter listed in both keys is counted once. The quoted caution text matches main.py:5113-5117 word for word. It is warn=True and never bad, so it cannot cause NOT READY.
An absent member makes the total unknown.pv.total() returns None whenever any member returns watts is None. main.py:4513 publishes None, and the status tile's w(None) renders —. A reading of 0 W still counts because the test is is None, not a falsiness check. The table in the changelog matches this, including 3 750 W (f"{v:,.0f} W".replace(",", " ")).
Instrumentation only.git diff cccb2fa..f863eca does not touch validate.py, control.py, allocator.py, arbiter.py or peakguard.py. The runtime files that change are exactly the five the entry lists. pv_w is read only by the MQTT value and by the status rows and tile. The control hash 846a10a5b89e… is the one recorded in DECISIONS/TEL-11-pv-production.md:185.
Why a second key. The Invalid list for option 'pv_entity' refusal, the deepmerge override detail, match(.*)? accepting the integer 417, and the rollback dropping the key with a warning all match §G of the decision doc. validate.py does not mention either key.
SAFETY-60 warning: correct, reproduced. I built an options blob from the shipped defaults with setpoint_entity set and slew_w: 5000 and ran predeploy_check.py --deployed-version 0.11.0 in the checks image. Passed as the bare dict, it was REFUSED, exit 1. Passed wrapped as {"result":"ok","data":{"options":…}}, it printed CLEARED for deploy., exit 0. The same bare blob with slew_w: 0 cleared, exit 0. So the warning is accurate, and jq '.data.options' is the right extraction. There is one placement note in the next comment, and it does not block.
CI. I traced the green to its run instead of trusting the status. Details are posted with the merge.
**Review of PKG-18 at head `1ad4d3a` against base `f863eca`: sound, no blocking findings.**
**Identity.** `slug: goodwe_controller` is unchanged, so this is the same add-on and its options and /data carry over. `version` moves `"0.11.0"` -> `"0.12.0"`. The diff has two files: `config.yaml` (the version line only) and `CHANGELOG.md` (+132).
**Gate 11a–11d.** I ran `test_version_gate.py` in `goodwe-addon-checks:1` on a fresh clone at `1ad4d3a`, mounted `:ro`, and it printed `all checks passed` (exit 0). There is one contents table, `## 0.12.0` equals `config.yaml`, all three `RETIRED_PROVIDERS` (`eneco_eddy`, `engie_dynamic`, `totalenergies_dynamic`) are named, and one `## Unreleased` remains below. 11c's text reads correctly for a 0.11.0 reader: it says the ids are "not new here" and that a site on 0.10.0 or later "has already met both". The `555.6 W` figure matches `validate.py:1137` (`SLEW_RAIL_FRAC * 5000`).
**Operator claims, checked against the code at `f863eca` and `DECISIONS/TEL-12-several-pv-inverters.md`:**
- *Replaces, not adds.* `pv.entities()` returns the list when it is non-empty, and `pv_entity` is read only when the list is empty. The ids are de-duplicated in order with `dict.fromkeys`, so an inverter listed in both keys is counted once. The quoted caution text matches `main.py:5113-5117` word for word. It is `warn=True` and never `bad`, so it cannot cause NOT READY.
- *An absent member makes the total `unknown`.* `pv.total()` returns `None` whenever any member returns `watts is None`. `main.py:4513` publishes `None`, and the status tile's `w(None)` renders `—`. A reading of `0 W` still counts because the test is `is None`, not a falsiness check. The table in the changelog matches this, including `3 750 W` (`f"{v:,.0f} W".replace(",", " ")`).
- *Instrumentation only.* `git diff cccb2fa..f863eca` does not touch `validate.py`, `control.py`, `allocator.py`, `arbiter.py` or `peakguard.py`. The runtime files that change are exactly the five the entry lists. `pv_w` is read only by the MQTT value and by the status rows and tile. The control hash `846a10a5b89e…` is the one recorded in `DECISIONS/TEL-11-pv-production.md:185`.
- *Why a second key.* The `Invalid list for option 'pv_entity'` refusal, the deepmerge `override` detail, `match(.*)?` accepting the integer 417, and the rollback dropping the key with a warning all match §G of the decision doc. `validate.py` does not mention either key.
**SAFETY-60 warning: correct, reproduced.** I built an options blob from the shipped defaults with `setpoint_entity` set and `slew_w: 5000` and ran `predeploy_check.py --deployed-version 0.11.0` in the checks image. Passed as the **bare** dict, it was `REFUSED`, exit 1. Passed **wrapped** as `{"result":"ok","data":{"options":…}}`, it printed `CLEARED for deploy.`, exit 0. The same bare blob with `slew_w: 0` cleared, exit 0. So the warning is accurate, and `jq '.data.options'` is the right extraction. There is one placement note in the next comment, and it does not block.
**CI.** I traced the green to its run instead of trusting the status. Details are posted with the merge.
Non-blocking: length and one placement issue. Suggested for the next release entry. This PR does not need to change for them.
1. The SAFETY-60 warning is under a heading that tells the actual reader to skip it. It is the last paragraph under ### If you are crossing from a version older than 0.10.0. The one operator waiting for this release is on 0.11.0, so that heading tells them the section does not apply to them. But the warning applies to every operator who runs the predeploy check, whatever version they come from. Why this does not block this hop: TEL-12 adds no validator rule (validate.py is not in cccb2fa..f863eca). An options blob that starts 0.11.0 therefore also passes 0.12.0's validator, so a false CLEARED cannot hide a new refusal on this upgrade. For the next cut, give it its own ### Before you press Update heading directly under the contents table, or put it in the top warning block.
2. Length. The entry is 132 lines for one optional telemetry key that is empty on every existing install. The owner has asked for shorter docs. An operator deciding whether to press Update needs about 35–45 lines:
Unset, nothing changes. No new validator rule, so no exit 78. Rolling back is safe. (~5 lines)
Instrumentation only, not a control input. One sentence plus the hash. (~3 lines)
What to do with several inverters: the YAML example, and the rule that it replacespv_entity, so the inverter already in pv_entity goes in the list too and is counted once. (~12 lines)
An absent member makes the total unknown, never a partial sum. Keep the 4-row table. (~10 lines)
Why a second key: one sentence plus a pointer to DECISIONS/TEL-12… §1. (~3 lines)
The crossing note and the SAFETY-60 line. (~8 lines)
Candidates to cut or move to the DECISIONS doc: the match(.*)? / deepmerge argument, the template-sensor paragraph, the list of runtime files, the explanation of why replacement beats union, and "Why it matters now" (that is project context, not information for the operator).
Target for the next release entry: ≤ 50 lines per runtime feature.
**Non-blocking: length and one placement issue. Suggested for the next release entry. This PR does not need to change for them.**
**1. The SAFETY-60 warning is under a heading that tells the actual reader to skip it.** It is the last paragraph under `### If you are crossing from a version older than 0.10.0`. The one operator waiting for this release is on 0.11.0, so that heading tells them the section does not apply to them. But the warning applies to **every** operator who runs the predeploy check, whatever version they come from. Why this does not block this hop: TEL-12 adds no validator rule (`validate.py` is not in `cccb2fa..f863eca`). An options blob that starts 0.11.0 therefore also passes 0.12.0's validator, so a false CLEARED cannot hide a new refusal on this upgrade. For the next cut, give it its own `### Before you press Update` heading directly under the contents table, or put it in the top warning block.
**2. Length.** The entry is 132 lines for one optional telemetry key that is empty on every existing install. The owner has asked for shorter docs. An operator deciding whether to press Update needs about 35–45 lines:
- Unset, nothing changes. No new validator rule, so no exit 78. Rolling back is safe. (~5 lines)
- Instrumentation only, not a control input. One sentence plus the hash. (~3 lines)
- What to do with several inverters: the YAML example, and the rule that it *replaces* `pv_entity`, so the inverter already in `pv_entity` goes in the list too and is counted once. (~12 lines)
- An absent member makes the total `unknown`, never a partial sum. Keep the 4-row table. (~10 lines)
- Why a second key: one sentence plus a pointer to `DECISIONS/TEL-12…` §1. (~3 lines)
- The crossing note and the SAFETY-60 line. (~8 lines)
Candidates to cut or move to the DECISIONS doc: the `match(.*)?` / deepmerge argument, the template-sensor paragraph, the list of runtime files, the explanation of why replacement beats union, and "Why it matters now" (that is project context, not information for the operator).
Target for the next release entry: **≤ 50 lines per runtime feature.**
admin
merged commit a45ed06c05 into release/1.02026-09-24 23:09:29 +02:00
Merged. release/1.0 is now a45ed06, a merge commit with parents f863eca and 1ad4d3a. I confirmed it with git ls-remote.
How I traced the green. The combined status on 1ad4d3a came from run 830, job 1955. That run has event: pull_request, path: checks.yml@refs/pull/117/head, run_attempt: 1, and head_sha equal to this PR's head. Its pull_requests entry is #117, base release/1.0 @ f863eca. The job log shows PRNUM: 117 and BASEREF: release/1.0, then GRADED: refs/pull/117/head merged into release/1.0, then 31 test file(s) to run and 31 lines reading PASS, then Job succeeded (8m5s). So the run really executed; the green is not a laundered skip. The merge was pinned with head_commit_id to 1ad4d3a.
The merge is not a deploy. There is no tag, and master is untouched (78acb55). Shipping still needs a release/1.0 -> master PR and the owner's go.
**Merged. `release/1.0` is now `a45ed06`, a merge commit with parents `f863eca` and `1ad4d3a`. I confirmed it with `git ls-remote`.**
How I traced the green. The combined status on `1ad4d3a` came from run **830**, job 1955. That run has `event: pull_request`, `path: checks.yml@refs/pull/117/head`, `run_attempt: 1`, and `head_sha` equal to this PR's head. Its `pull_requests` entry is #117, base `release/1.0` @ `f863eca`. The job log shows `PRNUM: 117` and `BASEREF: release/1.0`, then `GRADED: refs/pull/117/head merged into release/1.0`, then `31 test file(s) to run` and 31 lines reading `PASS`, then `Job succeeded` (8m5s). So the run really executed; the green is not a laundered skip. The merge was pinned with `head_commit_id` to `1ad4d3a`.
The merge is not a deploy. There is no tag, and `master` is untouched (`78acb55`). Shipping still needs a `release/1.0` -> `master` PR and the owner's go.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Cuts 0.12.0 on
release/1.0so TEL-12 (#116) can reach an installed 0.11.0. Diff:config.yaml(version line) andCHANGELOG.md(new## 0.12.0section). Nothing else.One runtime merge since 0.11.0 (
cccb2fa):f863ecaTEL-12, a new optional list optionpv_entitiesbesidepv_entity. Option surface 76 -> 77, so a minor bump. No validator rule added; an 0.11.0 options blob validates untouched (TEL-12's §G probe against Supervisor 2026.08.0).The entry tells the operator:
pv_entitiesis for sites with more than one PV inverter;pv_entitywhen set (it is not added to it). An operator upgrading withpv_entityset must include that inverter in the list, where it is counted once. The status page caution names which key won;unknown(a recorder gap), never a partial sum;846a10a5…unmoved);pv_entitydeclared as a list, Supervisor 2026.08.0 refuses an 0.11.0 install's stored string (Invalid list for option 'pv_entity');price_providerids) points at## 0.10.0, as## 0.11.0does;data.optionsdict, not the wrappedha apps info --raw-jsonexport (SAFETY-60, open).Verification
## 0.12.0==config.yaml0.12.0, all 3RETIRED_PROVIDERSnamed, exactly 1## Unreleasedbelow.goodwe-addon-checks:1withchecks.yml's exact command, on a purged tree mounted:ro, one file at a time. 31/31 PASS.predeploy_check.py --deployed-version 0.11.0: reports0.11.0 -> 0.12.0, which is a real version change and no longer the NO-OP refusal it gave atf863eca. On a valid bare options blob the check clears.slew_w: 5000was REFUSED when passed bare, and printed CLEARED, exit 0 when passed wrapped as{"data":{"options":…}}.Not deployed, not tagged.
masterwas not touched. Shipping needs a separaterelease/1.0->masterPR.🤖 Generated with Claude Code
https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
Review of PKG-18 at head
1ad4d3aagainst basef863eca: sound, no blocking findings.Identity.
slug: goodwe_controlleris unchanged, so this is the same add-on and its options and /data carry over.versionmoves"0.11.0"->"0.12.0". The diff has two files:config.yaml(the version line only) andCHANGELOG.md(+132).Gate 11a–11d. I ran
test_version_gate.pyingoodwe-addon-checks:1on a fresh clone at1ad4d3a, mounted:ro, and it printedall checks passed(exit 0). There is one contents table,## 0.12.0equalsconfig.yaml, all threeRETIRED_PROVIDERS(eneco_eddy,engie_dynamic,totalenergies_dynamic) are named, and one## Unreleasedremains below. 11c's text reads correctly for a 0.11.0 reader: it says the ids are "not new here" and that a site on 0.10.0 or later "has already met both". The555.6 Wfigure matchesvalidate.py:1137(SLEW_RAIL_FRAC * 5000).Operator claims, checked against the code at
f863ecaandDECISIONS/TEL-12-several-pv-inverters.md:pv.entities()returns the list when it is non-empty, andpv_entityis read only when the list is empty. The ids are de-duplicated in order withdict.fromkeys, so an inverter listed in both keys is counted once. The quoted caution text matchesmain.py:5113-5117word for word. It iswarn=Trueand neverbad, so it cannot cause NOT READY.unknown.pv.total()returnsNonewhenever any member returnswatts is None.main.py:4513publishesNone, and the status tile'sw(None)renders—. A reading of0 Wstill counts because the test isis None, not a falsiness check. The table in the changelog matches this, including3 750 W(f"{v:,.0f} W".replace(",", " ")).git diff cccb2fa..f863ecadoes not touchvalidate.py,control.py,allocator.py,arbiter.pyorpeakguard.py. The runtime files that change are exactly the five the entry lists.pv_wis read only by the MQTT value and by the status rows and tile. The control hash846a10a5b89e…is the one recorded inDECISIONS/TEL-11-pv-production.md:185.Invalid list for option 'pv_entity'refusal, the deepmergeoverridedetail,match(.*)?accepting the integer 417, and the rollback dropping the key with a warning all match §G of the decision doc.validate.pydoes not mention either key.SAFETY-60 warning: correct, reproduced. I built an options blob from the shipped defaults with
setpoint_entityset andslew_w: 5000and ranpredeploy_check.py --deployed-version 0.11.0in the checks image. Passed as the bare dict, it wasREFUSED, exit 1. Passed wrapped as{"result":"ok","data":{"options":…}}, it printedCLEARED for deploy., exit 0. The same bare blob withslew_w: 0cleared, exit 0. So the warning is accurate, andjq '.data.options'is the right extraction. There is one placement note in the next comment, and it does not block.CI. I traced the green to its run instead of trusting the status. Details are posted with the merge.
Non-blocking: length and one placement issue. Suggested for the next release entry. This PR does not need to change for them.
1. The SAFETY-60 warning is under a heading that tells the actual reader to skip it. It is the last paragraph under
### If you are crossing from a version older than 0.10.0. The one operator waiting for this release is on 0.11.0, so that heading tells them the section does not apply to them. But the warning applies to every operator who runs the predeploy check, whatever version they come from. Why this does not block this hop: TEL-12 adds no validator rule (validate.pyis not incccb2fa..f863eca). An options blob that starts 0.11.0 therefore also passes 0.12.0's validator, so a false CLEARED cannot hide a new refusal on this upgrade. For the next cut, give it its own### Before you press Updateheading directly under the contents table, or put it in the top warning block.2. Length. The entry is 132 lines for one optional telemetry key that is empty on every existing install. The owner has asked for shorter docs. An operator deciding whether to press Update needs about 35–45 lines:
pv_entity, so the inverter already inpv_entitygoes in the list too and is counted once. (~12 lines)unknown, never a partial sum. Keep the 4-row table. (~10 lines)DECISIONS/TEL-12…§1. (~3 lines)Candidates to cut or move to the DECISIONS doc: the
match(.*)?/ deepmerge argument, the template-sensor paragraph, the list of runtime files, the explanation of why replacement beats union, and "Why it matters now" (that is project context, not information for the operator).Target for the next release entry: ≤ 50 lines per runtime feature.
Merged.
release/1.0is nowa45ed06, a merge commit with parentsf863ecaand1ad4d3a. I confirmed it withgit ls-remote.How I traced the green. The combined status on
1ad4d3acame from run 830, job 1955. That run hasevent: pull_request,path: checks.yml@refs/pull/117/head,run_attempt: 1, andhead_shaequal to this PR's head. Itspull_requestsentry is #117, baserelease/1.0@f863eca. The job log showsPRNUM: 117andBASEREF: release/1.0, thenGRADED: refs/pull/117/head merged into release/1.0, then31 test file(s) to runand 31 lines readingPASS, thenJob succeeded(8m5s). So the run really executed; the green is not a laundered skip. The merge was pinned withhead_commit_idto1ad4d3a.The merge is not a deploy. There is no tag, and
masteris untouched (78acb55). Shipping still needs arelease/1.0->masterPR and the owner's go.