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>
78 lines
3.6 KiB
Python
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()
|