UX-08 phase 2b review: keep over-rating evidence, pin latch clearing, plainer rows
checks / unit (pull_request) Successful in 8m48s
infra-pin / fresh (pull_request) Successful in 1s

- 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>
This commit is contained in:
glenn schrooyen
2026-09-27 03:10:25 +02:00
co-authored by Claude Opus 5.5
parent 696142bccc
commit 571b884315
9 changed files with 61 additions and 12 deletions
@@ -357,3 +357,5 @@ the bump is PKG's call. The material an entry needs is this file.
The option surface moves **76 keys → 77**, one new optional key, `pv_entities`,
empty on every install that does not set it.
**Superseded in part (UX-08, 2026-09-27):** the PV status rows no longer read 0.11.0's sentences at one entity. The owner ruled out entity ids on status rows, so 2/5 now holds for the control document only. See `UX-08-short-notifications.md`, phase 2b item 3.
+1 -1
View File
@@ -2954,7 +2954,7 @@ ERROR or WARNING line of that alarm. The texts are in `app/messages.py`.
| Peak guard, Load too high, Battery at its peak reserve | What the capacity-peak guard is doing. *Peak guard: off* means `meter_source` is `off`, so there is no 15-minute average to guard. |
| Battery level, Battery power, Grid power | The reading of that sensor. *No usable reading* means the sensor is missing **or** its reading is held back as too old (a separate stale-reading row then says so). Check it in Developer Tools before replacing it. |
| Inverter control | The setpoint entity, and the power range the inverter accepts. |
| Power request too high, Battery is not keeping up | The two loop alarms, each with its own count since start. After an episode clears, the count stays, ending in *not now*. See *The winding-loop alarm*. |
| Power request too high, Battery is not keeping up | The two loop alarms, each with its own count since start. After an episode clears, the count stays, ending in *fine now*. See *The winding-loop alarm*. |
| Direction check, Site direction check | The sign checks (SAFETY-02/30). *Opposite for 12 of 60 seconds* counts toward a stop. When it trips, the inverter, or every inverter for the site check, is set to 0 W until you start control. |
| Circulating power check, Inverters worked against each other | SAFETY-31, only with several inverters. |
| Delivery check, *name* is not delivering | SAFETY-42, only with several inverters. |
+11 -5
View File
@@ -4669,8 +4669,7 @@ class Controller:
_pg = self.peak_guard
if _pg is None:
out.append({"ok": True, "warn": True, "id": "peak_guard",
"text": "peak guard: no decision yet - the first "
"control cycle has not run"})
"text": messages.row_peak_guard_waiting()})
elif _pg.saturated:
out.append({"ok": False, "warn": False, "id": "peak_guard",
"text": messages.row_peak_saturated(_pg.ceiling_w)})
@@ -4821,6 +4820,12 @@ class Controller:
# SAFETY-40: the integrator bound alone is amber, not red.
out.append({"ok": False, "warn": True, "id": "not_keeping_up",
"text": messages.row_not_keeping_up(_cw.bound)})
# No over-rating in THIS episode (`bug` would be set), so any count
# is from an earlier one: keep that software-fault evidence shown.
if _cw.over_rating:
out.append({"ok": False, "warn": True, "id": "power_too_high",
"text": messages.row_before(
messages.T_OUTPUT_CLAMP, _cw.over_rating)})
else:
if _cw.over_rating:
out.append({"ok": False, "warn": True, "id": "power_too_high",
@@ -4940,13 +4945,14 @@ class Controller:
out.append({"ok": False, "warn": True, "id": "estop",
"text": messages.row_no_estop()})
else:
out.append({"ok": True, "warn": False, "text": "e-stop fitted"})
out.append({"ok": True, "warn": False, "id": "estop",
"text": messages.row_estop_fitted()})
if not o.get("peak_forecast_entity"):
# ok=False + warn=True renders as a caution, not a pass. A check that
# is both is a check nobody reads.
out.append({"ok": False, "warn": True,
"text": "no peak forecast: maintenance charging is not capacity-capped"})
out.append({"ok": False, "warn": True, "id": "peak_forecast",
"text": messages.row_no_peak_forecast()})
# --- TEL-11 ----------------------------------------------------------
# ⚠️ NO ROW AT ALL WHEN THE ROSTER IS EMPTY, AND THAT IS A DELIBERATE
# DIFFERENCE FROM THE PEAK-FORECAST CAUTION DIRECTLY ABOVE. That one
+18 -2
View File
@@ -281,7 +281,7 @@ def banner_site_direction() -> str:
def banner_direction(several: bool) -> str:
if several:
return "Inverter and own power point opposite ways. It is at 0 W."
return "An inverter's own power opposes its command. It is at 0 W."
return "Command and meter point opposite ways. Inverter set to 0 W."
@@ -427,7 +427,7 @@ def row_not_keeping_up(count) -> str:
def row_before(title, count) -> str:
"""After an episode clears: how often it happened, so the evidence stays."""
return f"{title}: {times(count)} since start, not now"
return f"{title}: {times(count)} since start, fine now"
def row_write_refused(unit=None) -> str:
@@ -494,6 +494,19 @@ def row_delivery_check(unit, short_s, hold_s) -> str:
return f"Delivery check for {unit}: on"
def row_estop_fitted() -> str:
return "Emergency stop: fitted"
def row_no_peak_forecast() -> str:
return ("No peak forecast set, so maintenance charging is not kept under "
"the peak limit")
def row_peak_guard_waiting() -> str:
return "Peak guard: no decision yet, control has not run"
def row_no_estop() -> str:
return ("No emergency stop fitted: if this computer fails, the battery "
"keeps its last command")
@@ -625,6 +638,9 @@ def status_samples() -> list:
("row_delivery_check", GREEN, row_delivery_check("inverter-b", 30, 120)),
("row_delivery_check", GREEN, row_delivery_check("inverter-b", 0, 120)),
("row_no_estop", AMBER, row_no_estop()),
("row_estop_fitted", GREEN, row_estop_fitted()),
("row_no_peak_forecast", AMBER, row_no_peak_forecast()),
("row_peak_guard_waiting", AMBER, row_peak_guard_waiting()),
("row_pv", GREEN, row_pv(3200, 2)),
("row_pv", GREEN, row_pv(1800, 1, True)),
("row_pv_none", AMBER, row_pv_none("no unit_of_measurement on the "
+15 -1
View File
@@ -549,7 +549,7 @@ check("...and once clear, a warn row still says how many times it engaged, "
any(c["warn"] and c.get("id") == "not_keeping_up"
and c["text"].startswith(messages.T_LOOP_BOUND)
and messages.times(ctl_r.clamp_wd.bound) in c["text"]
and "not now" in c["text"] for c in ctl_r.checks()))
and "fine now" in c["text"] for c in ctl_r.checks()))
# UX-07 item 1: the after-episode row used to say "output clamp ... engaged N
# time(s)" for integrator-bound cycles. Each count now sits under its own name.
check("UX-07 item 1: bound-only engagements are counted as the bound, and no "
@@ -559,6 +559,20 @@ check("UX-07 item 1: bound-only engagements are counted as the bound, and no "
and not any(c.get("id") == "power_too_high"
or messages.T_OUTPUT_CLAMP in c["text"]
for c in ctl_r.checks()))
# ...and an earlier over-rating count stays on the page while a LATER bound
# episode runs: it is the software-fault evidence, and that episode (bug False)
# cannot have added to it, so "fine now" is true of it.
_w = ctl_r.clamp_wd
_saved = (_w.active, _w.bug, _w.over_rating)
_w.active, _w.bug, _w.over_rating = True, False, 2
_rows = ctl_r.checks()
_w.active, _w.bug, _w.over_rating = _saved
check("UX-07 item 1: during a bound episode an earlier over-rating count is "
"still shown, amber, under its own title",
any(c.get("id") == "not_keeping_up" for c in _rows)
and any(c.get("id") == "power_too_high" and c["warn"] and not c["ok"]
and c["text"] == messages.row_before(messages.T_OUTPUT_CLAMP, 2)
for c in _rows))
# ⚠️ NOT A TRIP. 11.2's action is "clamp and alarm"; the loop keeps running and
# keeps writing the clamped value. If a later cut changes that it must come here.
+11
View File
@@ -373,6 +373,17 @@ _long = [t for fn, c, t in STATUS if c == messages.RED and fn not in _own_banner
check(f"every other red row makes a 'Not ready' banner of at most 12 words "
f"(too long: {_long})", not _long)
# A latch row must say how it clears: that is the one action it exists for.
_clears = {
"row_direction_trip": "A restart does not clear this",
"row_site_direction_trip": "Only starting control clears this",
"row_circulating_trip": "Only starting control clears this",
"row_not_delivering": "only stopping and starting control clears this",
}
_unclear = [fn for fn, _c, t in STATUS if fn in _clears and _clears[fn] not in t]
check(f"every latch row says how it clears (missing: {_unclear})",
not _unclear and {fn for fn, _c, _t in STATUS} >= set(_clears))
allow = read_allowlist(ALLOW_FILE)
by_kind = {}
for kind, *_ in found:
+1 -1
View File
@@ -89,7 +89,7 @@ configuration:
description: "Optional, and MEASUREMENT ONLY - it changes no control behaviour whatsoever. Point it at your solar inverter's own power sensor (any brand Home Assistant reads; it does not have to be on the RS485 bus) and the add-on records production beside the grid and battery figures, so a day of unstored surplus can be told apart from a day with no sun. POSITIVE MUST MEAN PRODUCING - if yours reads negative while producing, list it under 'sign is reversed' below. The unit is read from the entity itself - W, kW and the rest of Home Assistant's power units - so a sensor with no unit is refused rather than guessed at, and the status page says so. Leave it empty and nothing is read, nothing is published and nothing is shown."
pv_entities:
name: "Hardware · PV production entities, one per inverter (W)"
description: "Optional, and MEASUREMENT ONLY - it changes no control behaviour whatsoever. Use this instead of the single entity above when your roof has SEVERAL solar inverters: add one row per inverter, pointing at each one's own power sensor, and the add-on records the SITE total. Each entity's own unit is read, so a kW inverter beside a W one adds up correctly. ⚠️ SETTING THIS IGNORES THE SINGLE ENTITY ABOVE - clear that one when you fill this in, or you will see a caution on the status page saying so. ⚠️ AND IF ANY ONE OF THESE STOPS REPORTING THERE IS NO SITE TOTAL: the sensor goes to 'unknown' and the history records a gap, rather than publishing the sum of the ones that did answer. Two inverters out of three add up to a smaller number that looks exactly like a real one, and a week later that reads as 'there was less sun than there was' - which is the one conclusion this recording exists to test. The status page names which entity is missing, why, and what the others reported. Leave it empty and nothing is read, nothing is published and nothing is shown."
description: "Optional, and MEASUREMENT ONLY - it changes no control behaviour whatsoever. Use this instead of the single entity above when your roof has SEVERAL solar inverters: add one row per inverter, pointing at each one's own power sensor, and the add-on records the SITE total. Each entity's own unit is read, so a kW inverter beside a W one adds up correctly. ⚠️ SETTING THIS IGNORES THE SINGLE ENTITY ABOVE - clear that one when you fill this in, or you will see a caution on the status page saying so. ⚠️ AND IF ANY ONE OF THESE STOPS REPORTING THERE IS NO SITE TOTAL: the sensor goes to 'unknown' and the history records a gap, rather than publishing the sum of the ones that did answer. Two inverters out of three add up to a smaller number that looks exactly like a real one, and a week later that reads as 'there was less sun than there was' - which is the one conclusion this recording exists to test. The status page says which one is missing, by its place in this list. Leave it empty and nothing is read, nothing is published and nothing is shown."
pv_invert_entities:
name: "Hardware · PV entities whose sign is reversed"
description: "Optional, MEASUREMENT ONLY. Some sensors - typically a kWh meter or a power clamp in the PV circuit - read NEGATIVE while producing and a few watts POSITIVE at night. Add each such entity here, spelled exactly as it is above, and its sign is flipped before it is added up; leave the others out. The status page marks every flipped entity. History recorded before you set this keeps the old sign."
+1 -1
View File
@@ -69,7 +69,7 @@ configuration:
description: "Optioneel, en UITSLUITEND EEN METING - het verandert niets aan de regeling. Verwijs naar de vermogenssensor van je omvormer (elk merk dat Home Assistant kan lezen; hij hoeft niet op de RS485-bus te zitten) en de add-on registreert de productie naast de meter- en batterijwaarden, zodat een dag met niet-opgeslagen overschot te onderscheiden is van een dag zonder zon. POSITIEF MOET PRODUCEREN BETEKENEN - leest de uwe negatief tijdens productie, zet hem dan ook bij 'teken omgekeerd' hieronder. De eenheid wordt van de entiteit zelf gelezen - W, kW en de overige vermogenseenheden van Home Assistant - dus een sensor zonder eenheid wordt geweigerd in plaats van geraden, en de statuspagina zegt waarom. Laat leeg en er wordt niets gelezen, niets gepubliceerd en niets getoond."
pv_entities:
name: "Hardware · Zonneproductie-entiteiten, één per omvormer (W)"
description: "Optioneel, en UITSLUITEND EEN METING - het verandert niets aan de regeling. Gebruik dit in plaats van de losse entiteit hierboven wanneer uw dak MEERDERE zonne-omvormers heeft: voeg één rij per omvormer toe, elk verwijzend naar haar eigen vermogenssensor, en de add-on registreert het totaal van de installatie. De eenheid van elke entiteit wordt afzonderlijk gelezen, dus een omvormer in kW naast een in W telt correct op. ⚠️ ALS U DIT INVULT WORDT DE LOSSE ENTITEIT HIERBOVEN GENEGEERD - maak die leeg, anders verschijnt er een waarschuwing op de statuspagina die dat meldt. ⚠️ EN ALS ÉÉN VAN DEZE ENTITEITEN NIETS MEER DOORGEEFT, IS ER GEEN TOTAAL: de sensor gaat naar 'unknown' en de geschiedenis bewaart een gat, in plaats van de som van de entiteiten die wél antwoordden te publiceren. Twee omvormers van de drie tellen op tot een kleiner getal dat precies op een echt getal lijkt, en een week later leest dat als 'er was minder zon dan er was' - net de conclusie die deze registratie moet kunnen testen. De statuspagina noemt welke entiteit ontbreekt, waarom, en wat de andere doorgaven. Laat leeg en er wordt niets gelezen, niets gepubliceerd en niets getoond."
description: "Optioneel, en UITSLUITEND EEN METING - het verandert niets aan de regeling. Gebruik dit in plaats van de losse entiteit hierboven wanneer uw dak MEERDERE zonne-omvormers heeft: voeg één rij per omvormer toe, elk verwijzend naar haar eigen vermogenssensor, en de add-on registreert het totaal van de installatie. De eenheid van elke entiteit wordt afzonderlijk gelezen, dus een omvormer in kW naast een in W telt correct op. ⚠️ ALS U DIT INVULT WORDT DE LOSSE ENTITEIT HIERBOVEN GENEGEERD - maak die leeg, anders verschijnt er een waarschuwing op de statuspagina die dat meldt. ⚠️ EN ALS ÉÉN VAN DEZE ENTITEITEN NIETS MEER DOORGEEFT, IS ER GEEN TOTAAL: de sensor gaat naar 'unknown' en de geschiedenis bewaart een gat, in plaats van de som van de entiteiten die wél antwoordden te publiceren. Twee omvormers van de drie tellen op tot een kleiner getal dat precies op een echt getal lijkt, en een week later leest dat als 'er was minder zon dan er was' - net de conclusie die deze registratie moet kunnen testen. De statuspagina zegt welke ontbreekt, met zijn plaats in deze lijst. Laat leeg en er wordt niets gelezen, niets gepubliceerd en niets getoond."
pv_invert_entities:
name: "Hardware · Zonneproductie-entiteiten met omgekeerd teken"
description: "Optioneel, UITSLUITEND EEN METING. Sommige sensoren - meestal een kWh-meter of een stroomtang in het PV-circuit - lezen NEGATIEF tijdens productie en 's nachts enkele watt POSITIEF. Zet elke zulke entiteit hier, exact gespeld zoals hierboven, en haar teken wordt omgedraaid vóór het optellen; laat de andere weg. De statuspagina markeert elke omgedraaide entiteit. Geschiedenis van vóór deze instelling behoudt het oude teken."
+1 -1
View File
@@ -93,7 +93,7 @@ option-desc d34ef16ba99a # en.yaml over 30 words (63) :: 'The older way of gett
option-desc c70929091e71 # en.yaml over 30 words (252); capitals: SWITCHING THIS ON IS WHAT STARTS THE PRICE FETCH :: "Where day-ahead or real-time prices come from. 'off' is what ships, an"
option-desc a62e17ccf445 # en.yaml over 30 words (230); banned: TARIFF-18; capitals: THIS NUMBER IS NOT :: 'Everything on your bill that is not the energy itself - distribution, '
option-desc 56d56f58c43e # en.yaml over 30 words (206); capitals: THE NUMBERS HERE ARE THE LEVIES ALONE :: 'Leave this empty unless your network tariff really does charge a diffe'
option-desc 313bdd035b0b # en.yaml over 30 words (193); capitals: MEASUREMENT ONLY :: 'Optional, and MEASUREMENT ONLY - it changes no control behaviour whats'
option-desc 0b7ea32cf8d0 # en.yaml over 30 words (193); capitals: MEASUREMENT ONLY :: 'Optional, and MEASUREMENT ONLY - it changes no control behaviour whats'
option-desc 4172d9920d65 # en.yaml over 30 words (128); capitals: MEASUREMENT ONLY :: 'Optional, and MEASUREMENT ONLY - it changes no control behaviour whats'
option-desc 09ceba2c6afb # en.yaml over 30 words (70); banned: clamp; capitals: MEASUREMENT ONLY :: 'Optional, MEASUREMENT ONLY. Some sensors - typically a kWh meter or a '
option-desc 42a7eaac6ff1 # en.yaml banned: saturated :: 'How many consecutive saturated cycles before the loop stops winding up'