MAINT-21: app/rows.py - the status-row and Why-line module functions
Moves why_raised, why_key, tariff_plan_row and arbitrage_row out of app/main.py unchanged. main.py imports them, so app.main still exports them. tools/maint21_same_ast.py: OK. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
0d49027daa
commit
1b6bb05fb4
@@ -103,6 +103,7 @@ from .watchdog import (
|
||||
)
|
||||
from . import setuponly
|
||||
from . import web
|
||||
from .rows import arbitrage_row, tariff_plan_row, why_key, why_raised
|
||||
|
||||
OPTIONS_PATH = "/data/options.json"
|
||||
INGRESS_PORT = 8099
|
||||
@@ -227,19 +228,6 @@ def _night_limit(born):
|
||||
return bell if bell > born else solarfc._bell_start(d1)
|
||||
|
||||
|
||||
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 pack_energy_of(inv):
|
||||
"""OPT-09. ONE roster entry as the view `packshare` consumes.
|
||||
One builder for two callers (`pack_energy` needs the roster, `cycle()` one
|
||||
@@ -278,69 +266,6 @@ def _refused_interval(sign: int) -> tuple:
|
||||
return (-math.inf, 0.0) if sign > 0 else (0.0, math.inf)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
class Controller:
|
||||
def __init__(self, opts: dict, hass: HomeAssistant, store, mqtt_pub, *, stop,
|
||||
peak_db_path: str = "/data/tariff.db",
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user