Files
goodwe-addon/goodwe_controller/DECISIONS/UX-08-short-notifications.md
T
goodwe-review c84d5fd14f
checks / unit (pull_request) Successful in 8m50s
infra-pin / fresh (pull_request) Successful in 2s
UX-08 2c review: unit settings anchor to inverters, plainer texts
- 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>
2026-09-27 06:51:57 +02:00

11 KiB

UX-08 phase 2a: short notifications, and what moved out of them

The owner decided on 2026-09-27: a title of at most about 5 everyday words and a one-sentence body. A red safety alarm may use up to 3 sentences, so no action-relevant information is lost. Titles are neutral, with no "GoodWe:" prefix. HA entity ids appear only in refusals. The rule is STYLE.md at the repository root, the texts are in app/messages.py, and test_messages.py enforces both. The detail each notification used to carry is in DOCS.md under "Notifications", and the body links to it.

Unchanged: every notification_id, every trigger and cadence (edges, SAFETY-34 re-announce, per-unit ids), every log line and every status row. The status banner and rows are phase 2b, refusals and option texts 2c, logs 2d; ux08_allowlist.txt lists them until then.

Where a choice was made

  1. Severity of "not delivering" (SAFETY-42). The notification is an alarm, so it may use 3 sentences. Its log line is ERROR and the alarm is latched. The page banner stays amber, as SAFETY-42 decided, because control keeps running; the two answer different questions (is control running / must a person act) and HA shows no colour on a notification. The three checks stay in their original order. Clearing it takes stop then start, and the body says so.
  2. Figures left the bodies of the frozen-meter card (the rail and the command), the circulating-power card (the watts and the time window) and the peak cards (the projection). None of them is needed to act, and all of them are still in the reason text that the log line and the status row carry. test_freeze.py and test_watchdog.py now pin that. The frozen-meter card can therefore no longer quote a wrong rail (SAFETY-20), because it quotes none.
  3. The over-rating alarm keeps its numbers (asked, sent, and which limit cut it), because they are what a fault report needs. ClampWatchdog.facts carries them under the same keep-the-bug rule as reason, so the words and the numbers always describe the same tick, with asked before sent (SAFETY-40, test_clamp.py part 6).
  4. SAFETY-25 stays in the peak-reserve alarm as its last sentence ("the reserve only limits peak shaving … down to its cut-out level"). The alarm now gives each branch its own action: at the reserve, lower it; battery level unread, check the sensor; floor unknown, set the cut-out level. That last branch is soc_floor is None, which happens when an inverter's cut-out level is missing, not the peak reserve (that option always has a value). Before, every branch said "lower peak_soc_floor". The title, "Battery stopped limiting the peak", is true on all three branches.
  5. SAFETY-32 at a roster of one. The body no longer ends in a separate "WHICH PACK" paragraph. The pack's name is part of the first sentence ("The battery of inverter-b …"), and at a roster of one that is the only difference.
  6. Dropped as background, now in DOCS: "charging stays blocked" (peak reserve), "why no other guard fired" (circulating power, not delivering), and the saturation-freeze distinction (over-rating). "The meter cannot see this" stays in the circulating-power card, as the owner asked.
  7. The Details link opens the add-on's documentation page (/hassio/addon/<slug>/documentation, with the slug taken from the container hostname) and names the section. A #heading anchor was not used because nobody has checked that Home Assistant's documentation tab renders heading ids; if it does, adding one is a one-line change. Outside an add-on container it names the section without a link.

Phase 2b: status rows and page banners

Every row of the Commissioning list and every banner is now in app/messages.py (row_*, banner_*), and test_messages.py checks a rendering of each. The row and banner kinds are gone from ux08_allowlist.txt (200 → 168 entries). DOCS.md "Status page" explains the rows. Unchanged: every row's colour (ok / warn / bad), which rows show when, and the banner cascade order.

Where a choice was made

  1. A red row may use 3 sentences (30 words). STYLE.md gave rows 15 words; the owner allows red safety states 3 sentences. A red row needs room for what happened, the state (0 W) and how it clears. STYLE.md now says so, and the test applies it by colour.
  2. Figures left the rows, not the product. The reason texts (freeze, sign, circulating, delivery, peak guard) carry reason codes and limits; they stay on the ERROR/WARNING log line and the event list, as before. Rows keep the numbers a reader acts on: the stall age and the 0 W deadline, the meter age and limit, asked and sent for an over-rating, the peak limit, and "opposite for 12 of 60 seconds" while a check counts. So 2a item 2 ("also on the status row") no longer holds, and DOCS.md says "in the add-on log".
  3. No entity id on any row, even the green ones that used to print sensor.x = 54. The sensor rows name the setting (Battery power for inverter-b). Solar sources are numbered in the order of the settings list (source 2 of 3, entry 1 of the reversed-sign list). This retires TEL-12's rule that a one-entity site reads 0.11.0's sentence byte for byte. A tools/tel11_pv_trace.py --compare against a pre-UX-08 tree therefore shows the PV rows as changed telemetry, and the control documents stay equal. The per-source watts and units left the multi-source row. The total, the count and which source is missing remain.
  4. UX-07 item 1. ClampWatchdog now also counts over_rating and bound separately. After an episode, each count shows under its own alarm's title ("Power request too high: 2 times since start, not now", "Battery is not keeping up: …"). A site with both gets two amber rows where it had one.
  5. The "Not ready" banner is the first red row's own banner, or else its first sentence. The peak-reserve and over-rating rows set banner to their notification title. The three trips keep their own banner branches, reworded (e.g. "Inverters worked against each other. Every inverter is set to 0 W.").
  6. How a latch clears stays on the row, shortened: "Only starting control clears this", "A restart does not clear this", "only stopping and starting control clears this" (not delivering). SAFETY-25's sentence stays on the peak-reserve row.
  7. Rows got an id (meter_frozen, direction, pv, …), so tests find a row by what it is, not by its words. The page ignores it.
  8. Left for later: texts that rows embed from other modules and that were not flagged as rows (the month-peak line, peakguard.explain(), the liveness and stale notes, shadow.banner()). The month-peak row only lost its semicolons.

UX-09: fixes after the phase 2a test

  1. The Details link follows the Home Assistant version. HA 2026.2 added the built-in app panel (core #157554, frontend #28245), whose add-on pages are /config/app/<slug>/documentation. 2026.5 stopped registering the hassio panel, so /hassio/addon/<slug>/documentation is a 404 there (TEST-23, HA 2026.9.1). 2026.1 and older have only the old path. The one path valid on both, /_my_redirect/supervisor_addon?addon=<slug>, opens the Info tab, not Documentation. So the add-on reads its slug (GET /addons/self/info) and the HA version (GET /info) at start, and picks the path. An unknown version gets the current path. The hostname stays as a fallback for the slug. Phase 2a item 7 is replaced by this.
  2. Shadow mode says "would". The over-rating alarm and row said "sent X W" while 0 W was on the wire, and the peak alarm said "the battery is giving all it can". Both now take shadow=, and in shadow mode say what would have been sent and that the inverters stay at 0 W. Severity, id and cadence do not change with the mode.
  3. "Controller" is gone from texts. "The add-on" names who decides, and "control" names what it does, as in the existing "start control". The add-on is what the reader sees in Home Assistant, and "controller" was easy to confuse with the control board.
  4. One term per setting, matching its label. Four labels changed instead of the texts, because the texts' words were plainer: “Peak reserve (%)” (was "Peak guard will not discharge below"), “Battery level entity (%)” (was "Battery charge level"), and “Solar production entity / entities, per inverter / Reversed-sign solar entities” (was "PV …"). The Dutch labels already said "Zonneproductie". Option descriptions are phase 2c and were not changed, so some still say "PV".
  5. The shadow banner and the inverter notes are now messages.banner_shadow() and row_no_setpoint / row_not_controllable / row_commands_refused, so test_messages.py checks them. The red shadow banner no longer lists the latches, because 12 words cannot carry them. It says a safety stop stays on, and each latch keeps its own red row, which names it and says how it clears. The colour of a liveness row now comes from is_live(), not from matching "NOT COMMANDABLE" in the text.
  6. DOCS "Notifications" and "Status page" are written for a home owner, and the card ids, keys and log detail moved to a "For installers" part of each.

UX-08 phase 2c: refusals, error texts and option texts

Validator refusals, the parse errors they carry, and every option name and description are now short. No rule, key, schema, default or exit code changed. Over 36 spoiled configurations the old and new validators refuse the same number of times. The allowlist went from 167 entries to 43, and only log lines (phase 2d) remain on it.

  1. A refusal names the setting by its label and then its key, as messages.setting() writes it: “Inverter rating” (max_w). The key has to stay, because configui.problems_by_field() shows a reason beside every key its text names. A test checks each label against en.yaml. A few reasons no longer mention keys they only named in passing, such as price_markup_eur_kwh in the withdrawn-provider reason, so the panel shows those reasons beside fewer settings. Each reason still names the setting that fixes it. A setting of one inverter in the list is written “Inverter rating” for inverter-b (inverters.max_w), so the panel shows it beside inverters, where that value is changed, and not only beside the flat max_w that a list does not read.
  2. Citations moved to DOCS. Option descriptions say "Measured value, leave it unchanged" or similar, and the record behind the value is under "For installers: where the numbers come from". The UX-01 citation check in test_translations.py now checks the tab for that instruction and DOCS for the record.
  3. What left the refusals: the shipped default in the peak-reserve reason, the superseded-default note in the levy-table reason (DOCS already covers it), the endpoints of the withdrawn providers (DOCS "Energy price providers"), the plant measurement behind slew_w (DOCS, beside the setting), and the leg-specific paragraphs of the tariff-formula errors. Each reason keeps the rejected value, the limit and what to change.
  4. Not rewritten: errors that only a code defect can raise, such as plugin-contract and argument-type checks in price.py, price_forecast.py, static_tariff.py, arbitrage.py and main.py, which a home owner never reads. INFO lines such as the tariff-formula preview were also left.