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
+10 -16
View File
@@ -279,6 +279,8 @@ that write is issued rather than skipped as unchanged.
"""
from __future__ import annotations
from . import messages
# ⚠️ CFG-03's mirror. `test_defaults_pin.py` part 2 discovers every
# module-level name ending in OPTION_DEFAULTS and asserts each value
# reproduces config.yaml EXACTLY, so this table cannot drift from the shipped
@@ -471,25 +473,17 @@ def log_line(*, would_w: float, reason: str, sized_from_meter: bool) -> str:
def banner(opts: dict, *, control_started: bool, latched: tuple = ()) -> tuple:
"""`(level, text)` for the top of the status page, or `None` when live.
⚠️ IT NAMES A STANDING LATCH AND GOES RED FOR ONE. The shadow banner sits
ABOVE every other branch of the cascade, because "the battery is not being
controlled" is the one sentence that may never be displaced - but a
failsafe latch that is up while the controller is inert is up when it
stops being inert, and an operator who turns shadow mode off without
clearing it gets a battery that will not move and a green page that
never mentioned why. So the latch is carried INTO the shadow banner
rather than losing its own.
⚠️ IT GOES RED FOR A STANDING LATCH. The shadow banner sits ABOVE every
other branch of the cascade, because "the battery is not controlled" is
the one sentence that may never be displaced - but a failsafe latch that
is up while the controller is inert is up when it stops being inert. So the
banner says a safety stop outlasts shadow mode, and each latch keeps its
own red row below, which names it and says how it clears. The words are
`messages.banner_shadow()`'s (UX-09); `latched` names them for callers.
"""
if not engaged(opts, control_started=control_started):
return None
text = ("SHADOW MODE - the battery is NOT being controlled. Every "
f"inverter is held at {WIRE_W:.0f} W and nothing this add-on "
"decides reaches the inverter.")
if latched:
return ("bad", text + " ⚠️ A FAILSAFE LATCH IS STILL UP ("
+ ", ".join(latched) + ") and it will still be up when shadow "
"mode is turned off.")
return ("warn", text)
return ("bad" if latched else "warn", messages.banner_shadow(bool(latched)))
def problems(opts: dict) -> list: