UX-08 phase 2a: short, plain notifications, and a ratchet for the rest
checks / unit (pull_request) Successful in 8m54s
infra-pin / fresh (pull_request) Successful in 1s

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:
glenn schrooyen
2026-09-27 01:00:54 +02:00
co-authored by Claude Opus 5.5
parent 2503de124e
commit 50ccea3dd7
13 changed files with 1170 additions and 185 deletions
+12 -6
View File
@@ -85,6 +85,7 @@ from pathlib import Path
import aiohttp
from app import messages
from app.control import Tuning, compute
from app.freeze import (
RAIL_FRAC, FreezeWatchdog, FreezeWatchdogConfig, looks_frozen,
@@ -773,7 +774,7 @@ def _is_freeze_note(d):
calls itself frozen, which would be a confusable alarm on its own account.
"""
return (d.get("notification_id") == FREEZE_NOTE_ID
or "frozen" in str(d.get("title", "")).lower())
or d.get("title") == messages.T_METER_FROZEN)
def notes_of(hass):
@@ -788,7 +789,7 @@ def notes_by_text_of(hass):
both would be true by construction.
"""
return [d for d in all_notes_of(hass)
if "frozen" in str(d.get("title", "")).lower()]
if d.get("title") == messages.T_METER_FROZEN]
def wired(unchanged_s, grid, cycles=60, dev=(-5000.0, 5000.0), **over):
@@ -858,7 +859,7 @@ check("...with a fixed notification_id so repeats replace, not stack",
[d.get("notification_id") for d in notes_by_text_of(hass_f)]
== [FREEZE_NOTE_ID])
check("...and it says the controller is still running",
bool(notes) and "STILL RUNNING" in notes[0]["message"])
bool(notes) and "Control is still running" in notes[0]["message"])
check("the status page carries a failed row saying so",
len(frozen_rows) == 1 and not frozen_rows[0]["ok"] and not frozen_rows[0]["warn"])
check("the event log names it", any("METER FROZEN" in e for e in ctl_f.events))
@@ -1180,11 +1181,16 @@ check("...and at or above the device rail, which is why it must",
check("THE ALARM FIRES through the real cycle() - the rating reaches the "
"predicate from the controller, not just from a unit call",
len(_n20) == 1 and _c20.freeze_wd.active)
check("...and the notification the operator gets quotes the capped rail, "
"not the max_w one",
# UX-08: the notification no longer quotes a rail at all (the reader's action
# is "check the meter"), so it cannot quote the wrong one; the capped rail is
# still what the reason - the log line and the status row - carries.
check("...and the notification the operator gets names no max_w rail, and the "
"reason behind it quotes the capped rail",
bool(_n20)
and f"{rail_w(LIVE_INSTALL_MAX_W):.0f} W"
not in str(_n20[0].get("message", ""))
and f"{rail_w(LIVE_INSTALL_MAX_W, dev_min=-4700.0, dev_max=4700.0):.0f} W"
in str(_n20[0].get("message", "")))
in _c20.freeze_wd.reason)
# p1_enabled is what a homewizard_local install has - set by hand here for the
# same reason part 4 sets it, see freeze_rows().
_c20.p1_enabled = True