UX-08 phase 2b: short, plain status rows and page banners
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
This commit is contained in:
@@ -541,11 +541,24 @@ check("...and the count in the row is the counter's, so nothing is lost",
|
||||
# is captured mid-run rather than asserted after the hysteresis has cleared it.
|
||||
check("the status page flags it WHILE it is engaged - amber, since it is the "
|
||||
"integrator bound (SAFETY-40; red is part 9's over-rating case)",
|
||||
any(not c["ok"] and c["warn"] and "LOOP AT INTEGRATOR BOUND" in c["text"]
|
||||
any(not c["ok"] and c["warn"] and c.get("id") == "not_keeping_up"
|
||||
and c["text"].startswith(messages.T_LOOP_BOUND)
|
||||
for c in ctl_r.red_snapshot))
|
||||
check("...and once clear, a warn row still says how many times it engaged",
|
||||
any(c["warn"] and f"{ctl_r.clamp_wd.engagements} time(s)" in c["text"]
|
||||
for c in ctl_r.checks()))
|
||||
check("...and once clear, a warn row still says how many times it engaged, "
|
||||
"under the integrator bound's own title",
|
||||
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()))
|
||||
# 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 "
|
||||
"row calls them 'Power request too high'",
|
||||
ctl_r.clamp_wd.bound == ctl_r.clamp_wd.engagements > 0
|
||||
and ctl_r.clamp_wd.over_rating == 0
|
||||
and not any(c.get("id") == "power_too_high"
|
||||
or messages.T_OUTPUT_CLAMP in c["text"]
|
||||
for c in ctl_r.checks()))
|
||||
|
||||
# ⚠️ 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.
|
||||
@@ -592,7 +605,8 @@ check("AC 2: the clamp counted ZERO engagements", ctl_s.clamp_wd.engagements ==
|
||||
check("AC 2: no OUTPUT_CLAMP row in the action log",
|
||||
not [e for e in ctl_s.events if REASON_CODE in e])
|
||||
check("AC 2: and no clamp row on the status page",
|
||||
not [c for c in ctl_s.checks() if "output clamp" in c["text"].lower()])
|
||||
not [c for c in ctl_s.checks()
|
||||
if c.get("id") in ("power_too_high", "not_keeping_up")])
|
||||
|
||||
print(" ...and the two alarms do not share a notification_id")
|
||||
ids_clamp = {d["notification_id"] for d in clamp_notes(hass_r)}
|
||||
@@ -1991,8 +2005,8 @@ check("...logged as a WARNING, not an ERROR",
|
||||
and not any(x.levelno >= logging.ERROR and "OUTPUT_CLAMP" in x.getMessage()
|
||||
for x in log_i))
|
||||
check("...and the status row is amber, not red",
|
||||
any(c["warn"] and "LOOP AT INTEGRATOR BOUND" in c["text"] for c in red_i)
|
||||
and not any(not c["warn"] and "OUTPUT CLAMP ENGAGED" in c["text"]
|
||||
any(c["warn"] and c.get("id") == "not_keeping_up" for c in red_i)
|
||||
and not any(not c["warn"] and c.get("id") == "power_too_high"
|
||||
for c in red_i))
|
||||
|
||||
# --- 9b. the genuine over-rating: integrator_max_w above max_w ----------------
|
||||
@@ -2023,8 +2037,17 @@ check("...under the over-rating title, quoting a tick's own asked and sent "
|
||||
and any(_asked_then_sent(n["message"], _f(d, 'requested_w'),
|
||||
_f(d, 'clamped_w')) for d, t, r in over)
|
||||
for n in clamp_notes(hass_o))
|
||||
and any(not c["warn"] and "OUTPUT CLAMP ENGAGED" in c["text"]
|
||||
and any(not c["warn"] and c.get("id") == "power_too_high"
|
||||
and c["text"].startswith(messages.T_OUTPUT_CLAMP)
|
||||
for c in red_o))
|
||||
# The red row quotes the same tick's numbers as the card, asked before sent.
|
||||
_row_o = [c["text"] for c in red_o if c.get("id") == "power_too_high"]
|
||||
check("...and the red row quotes asked before sent, from one of those ticks",
|
||||
_row_o and any(
|
||||
f"asked {messages.watts(_f(d, 'requested_w'))}" in _row_o[0]
|
||||
and f"sent {messages.watts(_f(d, 'clamped_w'))}" in _row_o[0]
|
||||
and _row_o[0].index("asked") < _row_o[0].index("sent")
|
||||
for d, t, r in over))
|
||||
|
||||
# --- 9c. the text, directly, and the two budgets -------------------------------
|
||||
from app.clamp import loop_clamp_note # noqa: E402
|
||||
|
||||
Reference in New Issue
Block a user