Router T241: the comments commit had put new rationale into MAIN-orchestration.md and cut comment lines that the note at56fc41fdid not hold. - DECISIONS/MAIN-orchestration.md is the56fc41ftext plus the one sentence that the Controller is split into app/main.py and app/ctl_*.py. - Comments whose content is not in a DECISIONS note at56fc41fare back in the code verbatim: MAINT-05 one config, the SAFETY-02, SAFETY-42/34, SAFETY-30 site half, SAFETY-31 and SAFETY-18 latch blocks in __init__, the _grade_store_migration docstring and comment, the trade_power_w docstring, the rows.py docstrings. - Kept: cuts of text already in the note, the SMA-10 / SAFETY-137 / SMA-2 pointers, the SAFETY-32 line fix. No code change: maint21_same_ast OK, maint20_same_code -OO same. Co-Authored-By: Claude <noreply@anthropic.com>
84 lines
4.0 KiB
Python
84 lines
4.0 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): two sources, two clocks, and the row says so. The plan is
|
|
stamped with its own age from `tariff_at` (not moved on a kept plan,
|
|
TARIFF-12), up to TARIFF_MAX_AGE_S (5400 s); `age_s` is the caller's
|
|
measurement. "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: three states (TARIFF-11): off (not armed),
|
|
armed with no decision this cycle (`why` is the loop's reason), or a
|
|
decision with its age.
|
|
`on` is read from the options, never inferred from `decision is None`:
|
|
`cycle()` returns at the input hold before `self.arb = None`, so the old
|
|
row named an option that was ON. The age is always printed, even 0.
|
|
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()
|