Files
goodwe-addon/goodwe_controller/app/rows.py
T
glenn schrooyenandClaude 902da3f723
checks / unit (pull_request) Successful in 25m17s
infra-pin / fresh (pull_request) Successful in 1s
MAINT-21: comments only - trim restated notes, add DECISIONS pointers
No code change: tools/maint21_same_ast.py and tools/maint20_same_code.py HEAD~
both print OK, and the -OO code objects are the same.

- Trims 56 comment and docstring lines (2088 -> 2032) in app/main.py and the
  new files that only restated their MAIN-orchestration.md bullet, each cut to
  its first sentence and a pointer. What the cut comments said and the bullet
  did not is now in the bullet: why a SAFETY-02 latch is restored, why the
  SAFETY-30 site check is not armed at one inverter, the SAFETY-18 re-derive,
  the MAINT-05 one-config rule, the OPT-09 sizing rules, the "no plan" row,
  the 5 400 s age bound, and the guard being the only latch repair.
- Adds the missing DECISIONS pointers on the SMA-10, SAFETY-137 and SMA-2
  comments (FALLBACK_BUDGET_S, write_setpoint, _await_inflight,
  _release_last, the retry wait, the failed-release branch, release_zero_sent,
  the in-flight write, the stop_event hand-back, the SMA-2 read and driver).
- DECISIONS/SAFETY-32: the stale `main.py:932-936` now names
  Controller.pack_floor_soc.
- main.py's docstring and import block say where the Controller's parts are.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-11-09 00:41:34 +01:00

78 lines
3.6 KiB
Python

"""Module functions for the status rows and the Why line.
Moved unchanged from app/main.py (MAINT-21). DECISIONS/MAIN-orchestration.md."""
from .arbiter import ACTUATOR_SITE, SET
from . import control as _ctl
from . import messages
def why_raised(resolution, claims) -> bool:
"""F2 (OPT-11 P1 test): did the limit that bound the site target push it
ABOVE what the winner asked (the peak floor asking for more power)? Then
the Why line must not say "held back". False when nothing bound it."""
if not resolution.bound_by or resolution.contradiction:
return False
asked = next((c.value for c in sorted(
(c for c in claims if c.kind == SET
and c.actuator == ACTUATOR_SITE and c.source == resolution.winner),
key=lambda c: c.priority, reverse=True)), None)
return asked is not None and resolution.target_w > float(asked)
def why_key(winner: str, claims) -> str:
"""The key of `messages.WHY_WINNER` for a resolution's winner. "safety" and
"failsafe" stand for several causes, so their own claim reason is the
key ("stopped", "inputs missing", "sign disagreement", or "unsplit" when
this module refuses to split the power)."""
if winner == "loop":
# SAFETY-52: a loop holding a refused direction says so.
return next((c.reason for c in claims if c.source == "loop"
and c.reason in (_ctl.REFUSED, _ctl.PROBING)), winner)
if winner in ("safety", "failsafe"):
return next((c.reason for c in claims
if c.source == winner and c.kind == "set"),
"no claim was made" if not any(
c.kind == "set" for c in claims) else "unsplit")
return winner
def tariff_plan_row(plan, tick_words: str, *, age_s, horizon_h: float,
provider: bool, max_age_s: float) -> str:
"""The *Tariff* status row: the 30 s tick's sentence AND the provider plan
(TARIFF-11), each with its own clock. "No plan" is said aloud on a site
that configured a provider. DECISIONS/MAIN-orchestration.md."""
# UX-08: plain words, every figure kept. The tick's sentence is "now"; the
# plan carries its own age.
if plan is None:
if not provider:
return tick_words
return (f"{tick_words}. No price plan: none yet, or the last one is "
f"over {max_age_s:.0f} s old")
return (f"{tick_words}. Price plan from {age_s:.0f} s ago, for "
f"{horizon_h:.0f} hours ahead: {plan.words}")
def arbitrage_row(*, on: bool, decision, decided_at, cycle_at, now,
why: str) -> str:
"""The *Arbitrage* status row: off, armed with no decision this cycle
(`why` is the loop's reason), or a decision with its age (TARIFF-11).
`on` is read from the options, never inferred from `decision is None`.
DECISIONS/MAIN-orchestration.md."""
# UX-08: plain words; the setting is named by its label.
if not on:
return (f"Off, “{messages.LABELS['arbitrage_enabled']}” is not "
f"switched on")
if decided_at is None:
return (f"On, but no decision yet, because control has not got that "
f"far. Control says: {why or 'no reason recorded'}")
age = (now - decided_at).total_seconds()
stale = cycle_at is not None and decided_at < cycle_at
head = (f"On, no decision this cycle ({why or 'no reason recorded'}), "
f"the last one {age:.0f} s ago: " if stale else
f"On, decided {age:.0f} s ago: ")
if decision is None:
return (f"On, no decision this cycle ({why or 'no reason recorded'}) "
f"and none before")
return head + decision.explain()