Every WARNING and ERROR line is one plain line of at most 25 words, with
no card ids, capitals or semicolons; the reasoning of the ten longest is
on a DEBUG line. The ux08 allowlist is empty and its cap is 0 (43 -> 0).
The status page: the month-peak and peak-guard rows, the stale-reading
row, the top table (Why, Tariff, Arbitrage, Shadow mode, labels), the
Stopped banner and the line under the buttons, which now shows the plain
hint of each event instead of the raw action log. The Supervisor question
at start logs one line with a real reason.
Refusals are counted as shown (labels expanded, long cases driven), seven
were shortened, the card and parse errors lost their internals, a blank
value reads "empty", and the panel no longer files inverters.max_w under
the flat max_w. Option names lose "(JSON)", the NL texts are checked too.
DOCS gets a glossary (control board, isolator, Developer Tools) and card
ids move from headings to a "For installers" line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
- messages.setting() for one inverter in the list reads
“Inverter rating” for inverter-b (inverters.max_w), so the config panel
files per-unit reasons under `inverters`, where the value is changed.
- The tariff card and plausibility reasons name
“Distribution, levies and VAT” (price_taxes_eur_kwh) again.
- The wrong-direction reasons say "safety stop", the sign_disagree_w
option says what it protects, and the roster, price-entity,
integrator_max_w and capacity_kwh texts read more plainly.
- NL: stuurprint -> stuurmodule, "De piekbeperking".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Validator refusals name the setting by its label and key, say what is
wrong and what to change, and keep the rejected value and limit. Option
names and descriptions are short in en and nl; citations moved to DOCS
"For installers" and the test_translations citation check now pins the
instruction on the tab and the record in DOCS. No rule, key, schema,
default or exit code changed. Allowlist 167 -> 43 (log lines only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
The Supervisor questions for the Details link are asked together with a
3 s budget instead of one after the other at 10 s each, so a hung
Supervisor cannot hold control start for 20 s. In shadow mode the
circulating and site-direction latches are pinned to keep their own red
row that says how they clear. Plainer inverter and battery wording.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Details link: /config/app/<slug>/documentation on HA 2026.2+ (the old
/hassio/addon path is a 404 from 2026.5), /hassio/addon on 2026.1 and
older. Slug from GET /addons/self/info, version from GET /info, hostname
as fallback.
- Shadow mode: the over-rating alarm and row say what would have been sent,
the peak alarm no longer says the battery is giving all it can.
- "controller" -> "the add-on" / "control". Settings named by their labels;
four labels changed to match (Peak reserve, Battery level entity, Solar
production ..., Reversed-sign solar entities).
- Shadow banner and inverter link notes moved to messages.py and checked.
- DOCS Notifications and Status page rewritten for home owners, with a
"For installers" part each. Allowlist 168 -> 167.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
- During a bound episode an earlier over-rating count stays on the page (amber,
under its own title); test_clamp pins it.
- test_messages pins that every latch row says how it clears.
- pv_entities description no longer claims the page names the entity (en, nl);
allowlist hash updated for that option-desc line.
- Plainer texts: multi-inverter direction banner, "fine now" after an episode,
and the e-stop, peak-forecast and peak-guard-waiting rows move to messages.py.
- TEL-12 DECISIONS points to UX-08 item 3 for the retired row rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every Commissioning row and every page banner is now worded in
app/messages.py (row_*, banner_*): one sentence of at most 15 words, a red
row up to three sentences so a latch still says how it clears, banners at
most 12 words. No row names a Home Assistant entity: sensors by their
setting, solar sources by their place in the list. Reason codes and limits
stay on the log line and event list; rows keep the numbers a reader acts on.
UX-07 item 1: ClampWatchdog counts over-rating and integrator-bound
engagements separately, and after an episode each count shows under its own
alarm's title instead of both under "output clamp".
No row colour, visibility or banner order changed. Rows carry an "id" so the
tests pin meaning rather than wording. The row and banner kinds leave the
allowlist (200 -> 168, cap equal). DOCS.md gains "Status page"; choices are in
DECISIONS/UX-08-short-notifications.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
- not delivering: the body said control keeps running and then "start
control"; clearing it takes stop then start, and it now says so.
- peak reserve: the "reserve not set" branch is really soc_floor None,
i.e. an inverter's cut-out level missing; it now names that. The title
is true on all three branches ("Battery stopped limiting the peak").
- no semicolon run-ons in bodies (STYLE.md + test); loop-bound, circulating,
meter-frozen and multi-unit over-rating bodies rewritten to match.
- allowlist cap is now exact, so a shrink cannot leave room to regrow.
- DOCS names the settings behind "peak reserve" and "cut-out level";
DECISIONS no longer states the HA anchor behaviour as fact.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every HA notification the add-on raises now has a title of at most 5
everyday words and a one-sentence body; a red alarm may use three so no
action is lost. The texts live in app/messages.py, the rule in STYLE.md,
and the detail that moved out in DOCS.md "Notifications", which each body
links to. No notification_id, trigger or cadence changed.
test_messages.py enforces the rule on every notification, fails any
notification worded outside app/messages.py, and ratchets the kinds not yet
rewritten (rows, banners, refusals, logs, options) through a shrinking
allowlist of 200. Six tests re-pinned from prose to meaning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
A later anti-windup notice could replace a write-path BUG SIGNAL notice
under the shared goodwe_output_clamp id (escalation ignores the budget).
The WARNING now uses goodwe_integrator_bound and points to DOCS; the
cadence sentence in DOCS is corrected. Tests pin both ids (red on fd14569).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The clamp alarm gave `i_clamped` (normal anti-windup) the over-rating BUG
SIGNAL text, printed `Decision.i_w` as "the loop asked for", and printed
`self.target` (after slew/freeze) as "was clamped to".
- clamp.loop_clamp_note(): i_clamped says "the loop's integrator reached its
N W anti-windup bound" with this tick's command; clamped keeps BUG SIGNAL
with the requested command and the clamp's own result.
- Decision carries i_bound_w / requested_w / clamped_w, set at the clamp.
- i_clamped still counts and notifies (same id, cadence, SAFETY-41
escalation) but as a WARNING, amber row, plain title. A bug has its own
notification budget and its text is not overwritten within an episode.
- test_clamp.py part 9: red on the old code per defect; 5 mutations killed.
- DOCS.md alarm section (and its stale 30 s cadence), DECISIONS/SAFETY-40.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
A Core-only restart wipes persistent_notification and does NOT restart the
add-on (measured on TEST-23: the add-on's SAFETY-31 card was gone 4 min after
`docker restart test23-ha`, add-on StartedAt unchanged). The latched SAFETY-31
and SAFETY-42 cards were re-raised only on add-on start, so they stayed gone.
They are now re-asserted every REANNOUNCE_S = 300 s while tripped (same
notification_id, so it replaces). test_watchdog part 6 and test_delivery part
8b grade it with the same process across the "restart"; red on the old code.
The docstrings that said the add-on restarts with HA are corrected.
Record: goodwe docs/DECISIONS.md, SAFETY-34 D4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
A missing or comment-only .gitea/infra-pin now ends on the workflow's own
FAIL line instead of git's `fatal: path ... does not exist` (still red). Every
x-access-token remote URL becomes an env-set Authorization header, so no
token lands in .git/config or argv; the pin workflows' temp dir is removed
by an EXIT trap, also on failure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The long-lived token no longer lands in $WS/goodwe/.git/config (mounted into
the tests' container, left behind by a failed job) or in git's argv. A missing
secret (fork PR) fails with its own message. README: addon-pin is not re-run
after a bump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
- checks.yml's infra checkout and infra-pin.yml's admin/goodwe ls-remote use
the INFRA_READ_TOKEN secret; the self fetch keeps GITHUB_TOKEN.
- infra-pin.yml compares against admin/goodwe refs/heads/release/1.0; STALE
message, comments and README follow.
- test_control.py / test_alloc.py: drop the claim that CI cannot reach
admin/goodwe.
- .gitea/infra-pin is DELIBERATELY one release/1.0 merge behind (d84ac01) for
the "infra-pin / fresh goes red" proof; the next commit sets it to HEAD.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- .gitea/infra-pin: the one place the admin/goodwe commit is pinned.
- checks.yml: checks out admin/goodwe at the pin as the sibling test_freeze.py
looks for; any failure fails the job. Tests run with GOODWE_REQUIRE_HWSIM=1,
and `CI: ` lines are echoed on PASS so the log shows which transport ran.
- test_freeze.py: GOODWE_REQUIRE_HWSIM refuses the stub; a local stub run
prints a STUB USED line.
- infra-pin.yml: a separate, non-required check that goes red naming both
SHAs when admin/goodwe master has moved past the pin.
- README: when and how to bump the pin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Some PV entities (kWh meters, ESPHome clamps in the PV circuit) read positive
for consumption and negative for production. `pv_invert_entities` lists the
ones to flip; the flip is applied per member inside pv.total(), before the sum,
so the MQTT `pv` row and the status page carry the corrected figure. Default []
is byte-identical to 0.12.0.
Shape measured in supervisor_shape_probe.py section H (Supervisor 2026.08.0):
a parallel list validates the 0.12.0 blob unchanged and renders as its own
translated repeatable field; dicts on pv_entities refuse 0.12.0's stored
strings; a prefix is invisible syntax; a global bool cannot describe a mixed
roster. Status page marks flipped entities and cautions per entity below
-50 W, saying ADD or REMOVE. Telemetry only: control documents unchanged.
Record: DECISIONS/TEL-14-pv-sign.md (reverses TEL-11 section 2).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
Since SAFETY-61 a dead control loop writes 0 W and exits 70. The Supervisor
restarts a stopped add-on only when the per-install Watchdog toggle is on,
and it defaults to off. One step in DOCS.md "Before you start", with a
pointer to the DECISIONS file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU
Checks and prints that indexed an empty list, exhausted next(), or formatted
None killed the rest of their file (FCST-06). Read them through the new
named_absence.py (nth/first/field/given/present) and conjoin a presence term.
- every site in the brief, plus the family the new scan and a sweep found
- test_named_absence.py: scans every test_*.py for unguarded filtered-list
indexing, next() without a default and numeric formats on maybe-None
names; proven to bite; prints what it cannot see, counted
- test_price.py 16a: the `_T17_CAPS is _T17_CAPS` tautology now grades the
two assemble() calls' arguments
- test_control.py: coverage table re-measured on runs graded to the end
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwRLw7eKYj2byVdwB6kbjU