UX-10: the last plain-language leftovers: log lines, two table rows, 'Why: Other', red at 0 s
- test_messages.py part 6: every dynamic part of a WARNING+ line is classified as data or as a built text, and every built text is produced by the real code and checked. This is the gap the 2d scan had: it counted `%s` as one word, so reasons built by loop_clamp_note() and friends were never seen. - Quoted reasons rewritten to STYLE.md (red-alarm shape, codes kept): OUTPUT_CLAMP, METER_FROZEN, CIRCULATING_POWER, STALE_READING, the peak guard (codes now PEAK_GUARD*), the migration report, price feed, price history and gate reasons. Plurals via messages.times()/count(). - Top table: Price history "96 prices over 24 hours for 26 Sep", Tariff "one every 15 minutes or every hour", "not final yet" dropped. - Levels and noise: per-cycle shadow line DEBUG, shadow on/off INFO, aiohttp access log only at log level debug, settings warnings once per start, one plain line per Home Assistant outage (hass.Outage). - INFO jargon and error texts: no "§8.3", ISO times, "banked", "keys ARE", "NOT_OBSERVED", "did NOT land", "AND". - Display bugs: WHY_WINNER covers every winner and safety reason (main.why_key(), part 7 finds them in the code); the meter row is amber "waiting for the first reading" inside its limit after a start; the missing-reading age is counted when the page asks. - STYLE.md and DECISIONS/UX-08-short-notifications.md (UX-10 section). No change to safety behaviour, alarm levels, notification ids, triggers, cadence or the banner cascade. One row colour changes (meter at start), justified in DECISIONS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
125b0702a2
commit
23a3a3f5b5
@@ -402,27 +402,27 @@ from .tariff import QUARTER_S
|
||||
|
||||
# Why the projection may not be graded. Reasons, not booleans: the status page
|
||||
# and the action log both have to say WHICH, and "blind" alone is unactionable.
|
||||
BLIND_NO_BLOCK = "no open quarter - the meter has produced no sample yet"
|
||||
BLIND_NO_BLOCK = "no open 15-minute period, the meter has given no reading yet"
|
||||
BLIND_COVERAGE = "meter gap"
|
||||
BLIND_FROZEN = "SAFETY-18: the meter is answering without changing"
|
||||
BLIND_FROZEN = "the meter is answering without changing"
|
||||
|
||||
# Where the ceiling came from. Only the second is a relaxation, and only an
|
||||
# OBSERVED reading may produce it.
|
||||
CEILING_CAP = "peak_cap_w"
|
||||
CEILING_MONTH_PEAK = "the month's banked peak"
|
||||
CEILING_MONTH_PEAK = "this month's peak"
|
||||
|
||||
# SAFETY-24. Why the forced discharge may not be sized. Reasons, not booleans,
|
||||
# for BLIND_*'s reason: the status page, the action log and the operator
|
||||
# notification all have to say WHICH, and "floor-limited" alone is unactionable.
|
||||
FLOOR_SOC_UNREADABLE = ("the battery state of charge was not measured this "
|
||||
"cycle, and an unreadable SoC is not a full battery")
|
||||
# ⚠️ SAFETY-25. "the peak guard's SoC floor", not "the SoC floor", and the
|
||||
# possessive is load-bearing: this string is what the status page, the action log
|
||||
# and the operator notification all print, and `peak_soc_floor` bounds THIS GUARD
|
||||
# ONLY. Widening it to "the SoC floor" would make the one sentence an operator
|
||||
# reads at the moment the battery stops shaving into a claim about the pack that
|
||||
# the grid-following loop is at that instant disproving.
|
||||
FLOOR_AT_SOC_FLOOR = "state of charge is at or below the peak guard's SoC floor"
|
||||
FLOOR_SOC_UNREADABLE = ("the battery level was not read this cycle, and an "
|
||||
"unread level is not a full battery")
|
||||
# ⚠️ SAFETY-25. "the peak reserve", not "the SoC floor", and the name is
|
||||
# load-bearing: this string is what the action log and the add-on log print,
|
||||
# and `peak_soc_floor` (“Peak reserve”) bounds THIS GUARD ONLY. Widening it to
|
||||
# "the SoC floor" would make the one sentence an operator reads at the moment
|
||||
# the battery stops shaving into a claim about the pack that the
|
||||
# grid-following loop is at that instant disproving.
|
||||
FLOOR_AT_SOC_FLOOR = "the battery level is at or below the peak reserve"
|
||||
|
||||
SOURCE = "peak"
|
||||
|
||||
@@ -566,8 +566,9 @@ def blindness(projection, max_hold_s, frozen: bool) -> str | None:
|
||||
return BLIND_NO_BLOCK
|
||||
gap = uncovered_s(projection)
|
||||
if max_hold_s is not None and gap > float(max_hold_s):
|
||||
return (f"{BLIND_COVERAGE}: {gap:.0f} s of this quarter uncovered, past "
|
||||
f"the {float(max_hold_s):.0f} s the averager will hold a sample")
|
||||
return (f"{BLIND_COVERAGE}, {gap:.0f} s of this 15-minute period not "
|
||||
f"covered, past the {float(max_hold_s):.0f} s a reading is "
|
||||
f"held")
|
||||
return None
|
||||
|
||||
|
||||
@@ -743,9 +744,9 @@ def decide(projection, *, batt_w, cap_w, reading, achievable_w, soc, soc_floor,
|
||||
return PeakDecision(
|
||||
engaged=True, floor_w=0.0, ceiling_w=ceiling, ceiling_from=from_where,
|
||||
blind=blind, projected_w=None,
|
||||
reason=(f"PEAK GUARD BLIND ({blind}) - the battery may not import "
|
||||
f"while the billed quarter cannot be measured; commanding no "
|
||||
f"charging (floor 0 W)"))
|
||||
reason=(f"PEAK_GUARD_BLIND: {blind}. The battery may not charge "
|
||||
f"while this 15-minute period cannot be measured, so it "
|
||||
f"is asked for no charging, floor 0 W"))
|
||||
|
||||
projected = float(projection.projected_w)
|
||||
if batt_w is None:
|
||||
@@ -807,17 +808,18 @@ def decide(projection, *, batt_w, cap_w, reading, achievable_w, soc, soc_floor,
|
||||
ceiling_from=from_where, projected_w=projected, excess_w=ex,
|
||||
wanted_w=wanted, floor_limited=True, soc=soc, soc_floor=soc_floor,
|
||||
soc_floor_reason=denied,
|
||||
# UX-10: plain sentences, every figure kept (test_messages.py
|
||||
# part 6). The code is one token, so it is not read as shouting.
|
||||
reason=(
|
||||
f"PEAK GUARD FLOOR-LIMITED: the quarter projects "
|
||||
f"{projected:.0f} W against a {ceiling:.0f} W ceiling "
|
||||
f"({from_where}) and {wanted:+.0f} W of discharge would hold "
|
||||
f"it, but {denied}"
|
||||
+ ("" if soc is None else f" ({float(soc):.0f} %")
|
||||
f"PEAK_GUARD_FLOOR_LIMITED: this 15-minute period heads for "
|
||||
f"{projected:.0f} W against a {ceiling:.0f} W limit "
|
||||
f"({from_where}), and {wanted:+.0f} W from the battery would "
|
||||
f"hold it. But {denied}"
|
||||
+ ("" if soc is None else f", at {float(soc):.0f} %")
|
||||
+ ("" if soc is None or soc_floor is None
|
||||
else f" vs {float(soc_floor):.0f} %")
|
||||
+ ("" if soc is None else ")")
|
||||
+ " - commanding no forced discharge, so THIS QUARTER WILL GO "
|
||||
"OVER THE CEILING. Charging stays blocked (floor 0 W)"))
|
||||
else f" against {float(soc_floor):.0f} %")
|
||||
+ ". So there is no forced discharge and no charging, floor "
|
||||
"0 W, and this period will go over the limit"))
|
||||
|
||||
floor = min(wanted, ceil_top)
|
||||
saturated = floor < wanted
|
||||
@@ -826,13 +828,13 @@ def decide(projection, *, batt_w, cap_w, reading, achievable_w, soc, soc_floor,
|
||||
blind=None, projected_w=projected, excess_w=ex, wanted_w=wanted,
|
||||
saturated=saturated, soc=soc, soc_floor=soc_floor,
|
||||
reason=(
|
||||
f"PEAK GUARD: quarter projects {projected:.0f} W against a "
|
||||
f"{ceiling:.0f} W ceiling ({from_where}) with "
|
||||
f"PEAK_GUARD: this 15-minute period heads for {projected:.0f} W "
|
||||
f"against a {ceiling:.0f} W limit ({from_where}), with "
|
||||
f"{QUARTER_S - projection.elapsed_s:.0f} s left, so {ex:.0f} W must "
|
||||
f"come off the meter; battery measured {float(batt_w):+.0f} W, "
|
||||
f"floor {floor:+.0f} W"
|
||||
+ (f" - SATURATED, {wanted:+.0f} W was needed and the inverter can "
|
||||
f"only give {ceil_top:.0f} W" if saturated else "")))
|
||||
f"come off the meter. The battery measured {float(batt_w):+.0f} W "
|
||||
f"and is asked for {floor:+.0f} W"
|
||||
+ (f". {wanted:+.0f} W was needed, but the inverters give at most "
|
||||
f"{ceil_top:.0f} W" if saturated else "")))
|
||||
|
||||
|
||||
def charge_headroom_w(decision) -> float | None:
|
||||
|
||||
Reference in New Issue
Block a user