UX-09: Details link for current HA, "would" in shadow mode, plain terms
checks / unit (pull_request) Successful in 8m50s
infra-pin / fresh (pull_request) Successful in 2s

- Details link: /config/app/<slug>/documentation on HA 2026.2+ (the old
  /hassio/addon path is a 404 from 2026.5), /hassio/addon on 2026.1 and
  older. Slug from GET /addons/self/info, version from GET /info, hostname
  as fallback.
- Shadow mode: the over-rating alarm and row say what would have been sent,
  the peak alarm no longer says the battery is giving all it can.
- "controller" -> "the add-on" / "control". Settings named by their labels;
  four labels changed to match (Peak reserve, Battery level entity, Solar
  production ..., Reversed-sign solar entities).
- Shadow banner and inverter link notes moved to messages.py and checked.
- DOCS Notifications and Status page rewritten for home owners, with a
  "For installers" part each. Allowlist 168 -> 167.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
This commit is contained in:
2026-09-27 04:10:39 +02:00
co-authored by Claude Opus 5.5
parent d57767fe38
commit 00e3ee9de8
16 changed files with 666 additions and 264 deletions
+47 -13
View File
@@ -85,7 +85,8 @@ from named_absence import given, nth, present
from app import messages
from app.clamp import (
CLEAR_CYCLES, REASON_CODE, REASSERT_S, SUSTAINED_CYCLES, ClampWatchdog,
CLEAR_CYCLES, REASON_CODE, REASSERT_S, SUSTAINED_CYCLES, ClampResult,
ClampWatchdog,
clamp_to_rating,
is_commandable,
)
@@ -1751,6 +1752,35 @@ check("SAFETY-28's gate still OBSERVES too - it refused nothing on this run, "
"implemented by making every write unauthorised",
not [e for e in _cl_c.events if "WRITE REFUSED" in e])
# UX-09: what the over-rating alarm and its row SAY in each mode. The loop
# bounds the command before the write clamp here, so an over-rating is injected
# as the write path records one, and check_output_clamp() is run on it. Nothing
# reaches the wire in shadow mode, so neither text may claim a command was sent.
def _said(mode):
h, c = _s10(mode, cycles=2)
c.inverters[0].clamp_last = ClampResult(7412.0, 5000.0, 5000.0, True,
"max_w")
asyncio.run(c.check_output_clamp())
notes = [d["message"] for d in clamp_notes(h)
if d["notification_id"] == CLAMP_ID]
rows = [r["text"] for r in c.checks() if r.get("id") == "power_too_high"]
return notes, rows
_ux_live, _ux_shad = _said("off"), _said("on")
print(f" over-rating, live: {_ux_live}\n over-rating, shadow: {_ux_shad}")
check("UX-09: live, the over-rating alarm and row say what was SENT",
_ux_live[0] and all(" and sent 5 000 W" in m for m in _ux_live[0])
and _ux_live[1] and all("sent 5 000 W" in t and "would" not in t
for t in _ux_live[1]))
check("...in shadow mode they say what WOULD have been sent and that the "
"inverter stays at 0 W, never that it was sent",
_ux_shad[0] and all("would have sent 5 000 W" in m
and " and sent " not in m and "stays at 0 W" in m
for m in _ux_shad[0])
and _ux_shad[1] and all("would send 5 000 W" in t and " sent " not in t
for t in _ux_shad[1]))
# ⚠️ AND SAFETY-18 IS GRADED ON WHAT IT WAS HANDED, WHICH IS AN ORDERING
# CLAIM. `check_meter_frozen()` takes `self.target` - the arbiter's RESOLVED
# INTENT - and `_shadow_settle()` re-seeds that to 0 W at the very end of the
@@ -1835,15 +1865,15 @@ check("the status page has a Shadow mode row in BOTH states, so its ABSENCE is "
_ROW in _shad_rows and _ROW in _live_rows)
check("AC 3: the banner states plainly that the battery is not being "
"controlled",
"SHADOW MODE" in _shad_pay["banner"]
and "NOT being controlled" in _shad_pay["banner"])
"Shadow mode" in _shad_pay["banner"]
and "not controlled" in _shad_pay["banner"])
check("...and it is not green. A page that reads `ok` while nothing is being "
"commanded is the 'looks healthy but is inert' failure itself",
_shad_pay["level"] == "warn")
check("...and it is FIRST in the cascade: the same controller has other "
"conditions to report and none of them displaces this sentence",
_live_pay["banner"] != _shad_pay["banner"]
and "SHADOW MODE" not in _live_pay["banner"])
and "Shadow mode" not in _live_pay["banner"])
_slot = re.search(r"WOULD HAVE COMMANDED ([-+][\d ]+) W, inverter held at "
r"([-\d]+) W", _shad_rows[_ROW])
check("the row answers 'what would it have done' with a NUMBER in its own "
@@ -1869,22 +1899,26 @@ check("with shadow mode off the row says so, and says what follows from it - "
and "DOES reach the inverter" in _live_rows[_ROW]
and "WOULD HAVE COMMANDED" not in _live_rows[_ROW])
# ⚠️ A STANDING LATCH IS NAMED RATHER THAN HIDDEN. The shadow banner sits above
# the three `bad` branches, so it has to carry what they would have said - a
# ⚠️ A STANDING LATCH IS SHOWN RATHER THAN HIDDEN. The shadow banner sits above
# the three `bad` branches, so it has to say what they would have said - a
# latch that is up while the controller is inert is up the moment it stops
# being inert.
# being inert. UX-09: the banner (12 words) says a safety stop outlasts shadow
# mode, and the latch's own red row, still on the list, names it.
_shad_c.inverters[0].sign_wd.tripped = True
_latched = _shad_c.status_payload()
print(f" banner with a latch up: {_latched['banner']!r}")
check("a failsafe latch that is standing is NAMED in the shadow banner and "
"turns it red, so nobody turns shadow mode off into a battery that will "
"not move and a page that never mentioned why",
check("a failsafe latch that is standing turns the shadow banner red and the "
"banner says a safety stop stays on, so nobody turns shadow mode off "
"into a battery that will not move and a page that never mentioned why",
_latched["level"] == "bad"
and "LATCH IS STILL UP" in _latched["banner"]
and _shad_c.inverters[0].name in _latched["banner"])
and "safety stop stays on" in _latched["banner"])
check("...and the latch is NAMED on its own red row, which says how it clears",
any(not r["ok"] and not r["warn"] and r.get("id") == "direction"
and "A restart does not clear this" in r["text"]
for r in _shad_c.checks()))
check("...and it still says the battery is not being controlled, so the latch "
"has not displaced the sentence that may never be displaced",
"NOT being controlled" in _latched["banner"])
"not controlled" in _latched["banner"])
_shad_c.inverters[0].sign_wd.tripped = False
# --- 8i. THE RECORD A LATER ANALYSIS READS ----------------------------------