UX-08 phase 2a: short, plain notifications, and a ratchet for the rest
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
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2503de124e
commit
50ccea3dd7
@@ -114,6 +114,7 @@ from datetime import datetime, timedelta, timezone
|
||||
from math import inf, isinf
|
||||
from pathlib import Path
|
||||
|
||||
from app import messages
|
||||
from app import peakguard
|
||||
from named_absence import first, given, nth, present
|
||||
|
||||
@@ -2522,13 +2523,21 @@ check("...and shortening the banner did NOT shorten the row: the full AC 3 "
|
||||
and "BMS cut-out" in rows25[0]["text"])
|
||||
|
||||
_body25 = hass25.last_message("goodwe_peak_soc_floor")
|
||||
|
||||
|
||||
def _says_scope(body):
|
||||
"""SAFETY-25 in messages.peak_reserve()'s words: the reserve binds the peak
|
||||
guard only, and home use goes on discharging below it to the cut-out."""
|
||||
return ("The reserve only limits peak shaving" in body
|
||||
and "normal home use can still discharge the battery below it" in body
|
||||
and "down to its cut-out level" in body)
|
||||
|
||||
|
||||
print(f" notification body tail: ...{_body25[-180:]}")
|
||||
check("...and so does the persistent notification, which outlives the page and "
|
||||
"is the thing an operator reads on a phone at 19:00. It names what has "
|
||||
"stopped - the guard buying a lower bill - and what has not",
|
||||
"peak_soc_floor bounds THE PEAK GUARD ONLY" in _body25
|
||||
and "not a" in _body25 and "pack-wide reserve" in _body25
|
||||
and "BMS cut-out" in _body25)
|
||||
_says_scope(_body25))
|
||||
|
||||
|
||||
# --- 10f. THE CLAIM-SOURCE SET, PINNED --------------------------------------
|
||||
@@ -4332,8 +4341,8 @@ check("the notification an operator reads at 19:00 NAMES THE PACK that stopped "
|
||||
"the site, and names only that one. 'the battery is at its floor' on a "
|
||||
"site with two batteries is an instruction to go and look at both, and "
|
||||
"the wrong one is the one that is fine",
|
||||
"SAFETY-32" in _body32 and "inverter-b" in _body32
|
||||
and "inverter-a" not in _body32.split("WHICH PACK")[-1])
|
||||
"The battery of inverter-b is at its peak reserve" in _body32
|
||||
and "inverter-a" not in _body32)
|
||||
# The mirror again, on the SoC axis: swap which pack is empty and the name
|
||||
# swaps. A hardwired `self.inverters[1].name` passes the line above.
|
||||
_body32b = asyncio.get_event_loop_policy().new_event_loop().run_until_complete(
|
||||
@@ -4341,8 +4350,8 @@ _body32b = asyncio.get_event_loop_policy().new_event_loop().run_until_complete(
|
||||
check("...and with the SAME two readings swapped between the packs it names "
|
||||
"inverter-a instead - so the sentence follows the measurement rather "
|
||||
"than a position in the roster",
|
||||
"inverter-a" in _body32b
|
||||
and "inverter-b" not in _body32b.split("WHICH PACK")[-1])
|
||||
"The battery of inverter-a is at its peak reserve" in _body32b
|
||||
and "inverter-b" not in _body32b)
|
||||
|
||||
|
||||
# ⚠️ AC 5, THE OTHER HALF: THE SAME METHOD ON A ROSTER OF ONE MUST PRINT WHAT IT
|
||||
@@ -4366,13 +4375,14 @@ async def _notify32_one():
|
||||
|
||||
_body32one = asyncio.get_event_loop_policy().new_event_loop().run_until_complete(
|
||||
_notify32_one())
|
||||
check("⚠️ AC 5. At a roster of ONE the same notification carries NO per-pack "
|
||||
"sentence at all - byte for byte the message SAFETY-25 shipped. There is "
|
||||
check("⚠️ AC 5. At a roster of ONE the same notification names NO pack - "
|
||||
"byte for byte the two-pack message minus the name (UX-08). There is "
|
||||
"one battery, naming it is noise, and this alarm reaches every existing "
|
||||
"install",
|
||||
"SAFETY-32" not in _body32one and "WHICH PACK" not in _body32one
|
||||
and _body32one == _body32.split("\n\nSAFETY-32")[0]
|
||||
and "peak_soc_floor bounds THE PEAK GUARD ONLY" in _body32one)
|
||||
"inverter-" not in _body32one
|
||||
and _body32one == _body32.replace("The battery of inverter-b is",
|
||||
"The battery is")
|
||||
and _says_scope(_body32one))
|
||||
|
||||
|
||||
# --- 12i. THE SIBLING ARGUMENT: `pack_floor_soc`, THE OTHER HALF OF THE PAIR --
|
||||
@@ -5604,8 +5614,7 @@ MUT.append(("rule 8's refusal naming a hardwired inverter",
|
||||
# 21. the notification naming a fixed roster position rather than the pack that
|
||||
# is actually at its floor.
|
||||
MUT.append(("the per-pack notification naming a fixed roster position",
|
||||
"inverter-a" in _body32b
|
||||
and "inverter-b" not in _body32b.split("WHICH PACK")[-1],
|
||||
"inverter-a" in _body32b and "inverter-b" not in _body32b,
|
||||
"part 12h: the same two readings swapped between the packs"))
|
||||
|
||||
# ---- the two PR #48 SURVIVORS, and why they survived -------------------------
|
||||
|
||||
Reference in New Issue
Block a user