MAINT-20 part 2 r2: app/main.py comments, lines 1501-2000 (pack_floor_soc .. _reseed_charge_end)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
6492d179d6
commit
9ec0a84129
@@ -1609,8 +1609,10 @@ class Controller:
|
||||
|
||||
def weather_held(self, now) -> bool:
|
||||
"""TARIFF-24: no forecast now, but a run planned with one is under way:
|
||||
it keeps its gap to its end. The wider gap applies from the next
|
||||
run. (DECISIONS/MAIN-orchestration-archive.md)"""
|
||||
it keeps its gap to its end (OPT-34: a night charge that keeps its
|
||||
level too); the wider gap applies from the next run. Pure; Home asks
|
||||
the same (`homeview.trade_verdict()`).
|
||||
(DECISIONS/MAIN-orchestration-archive.md)"""
|
||||
return self.solar_fc is None and (
|
||||
(self._wx_free_to is not None and now < self._wx_free_to)
|
||||
or (self._charge_latch is not None
|
||||
@@ -1618,8 +1620,9 @@ class Controller:
|
||||
|
||||
def _price_runs(self, now, packs, floor_pct, ceiling_pct):
|
||||
"""The kept run plan, rebuilt at most once per price slot (15 min) or
|
||||
when the prices or a battery's choice change; a run under way is
|
||||
kept to its end (`priceruns.keep_started()`).
|
||||
when the prices or a battery's choice change; a run under way is kept
|
||||
to its end (`priceruns.keep_started()`). None with no fresh firm
|
||||
prices: then no battery holds or trades.
|
||||
(DECISIONS/MAIN-orchestration-archive.md)"""
|
||||
plan = self.fresh_tariff_plan(now)
|
||||
if plan is None or not plan.has_prices:
|
||||
@@ -1672,9 +1675,9 @@ class Controller:
|
||||
required, ref, done_c, e):
|
||||
"""OPT-11 P4: the day plan (`dayplan.plan()`) on the runs just made,
|
||||
its levels held by the hysteresis (`dayplan.hold()`), then the runs
|
||||
made again with each battery's shortfall as the charge, so the cheap
|
||||
run is only as long as the shortfall needs.
|
||||
(DECISIONS/MAIN-orchestration-archive.md)"""
|
||||
made again with each battery's shortfall as the charge. With no plan
|
||||
(no or stale forecast, no house profile yet) the levels are dropped: P3
|
||||
option A. (DECISIONS/MAIN-orchestration-archive.md)"""
|
||||
dp = dayplan.plan(
|
||||
self.solar_fc,
|
||||
None if self.house_profile is None
|
||||
@@ -1710,8 +1713,13 @@ class Controller:
|
||||
return self._plan_targets
|
||||
|
||||
def _latch_charge(self, now, runs, targets, packs=None) -> None:
|
||||
"""OPT-34, once per cycle after `modetrade.decide()`.
|
||||
(DECISIONS/MAIN-orchestration-archive.md)"""
|
||||
"""OPT-34, once per cycle after `modetrade.decide()`: a started night
|
||||
grid charge keeps its planned level when the forecast goes mid-charge
|
||||
(`plan_targets()` gives the latched levels, `weather_held()` the narrow
|
||||
price gap). The latch lasts through every cheap run plus one price slot
|
||||
(`CHARGE_LATCH_GRACE_S`), is dropped when `now` passes its end or the
|
||||
battery leaves its mode, and a charge that never started has none.
|
||||
DECISIONS/OPT-34-the-night-charge-keeps-its-level.md"""
|
||||
old = lt = self._charge_latch
|
||||
tick = self.mode_tick
|
||||
run = None if runs is None else runs.cheap
|
||||
@@ -1752,7 +1760,10 @@ class Controller:
|
||||
"born": lt[2].isoformat()})
|
||||
|
||||
def _mode_roles(self, now, maint_results):
|
||||
"""OPT-11 P3: each battery's role and range this tick.
|
||||
"""OPT-11 P3: each battery's role and range this tick. Grid charging
|
||||
and selling need a fresh plan that `TariffPlan.arbitrage()` allows with
|
||||
the MEASURED peak guard (None refuses: nothing measures the quarter),
|
||||
exactly the gate `arbitrage.decide()` has.
|
||||
(DECISIONS/MAIN-orchestration-archive.md)"""
|
||||
floor_pct, ceiling_pct = arbitrage.soc_bounds(self.o)
|
||||
packs = self._mode_packs(maint_results)
|
||||
@@ -1823,6 +1834,8 @@ class Controller:
|
||||
draining=any(r.phase == DRAIN for r in maint_results or ()))
|
||||
self._latch_charge(now, runs, targets, packs)
|
||||
# A charge the guard or the headroom stopped stays off this period.
|
||||
# OPT-15: only a MEASURED stop latches; the first unmeasured headroom
|
||||
# in a quarter stops one tick, a second latches.
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
if was and self.mode_tick.action != modetrade.CHARGE:
|
||||
why = self.mode_tick.why
|
||||
@@ -1851,15 +1864,19 @@ class Controller:
|
||||
|
||||
def _mode_trade(self, claims, now) -> bool:
|
||||
"""OPT-11 P3: file the site trade the roles ask for, sized inside the
|
||||
modes and the hard limits (the loop bound) and, for a charge, the
|
||||
billed quarter's headroom (SAFETY-45, aim cap - margin, SAFETY-79).
|
||||
modes and the hard limits and, for a charge, the billed quarter's
|
||||
headroom (SAFETY-45, aim cap - margin, SAFETY-79), so no battery that
|
||||
its mode or a refusal holds is asked for watts it may not give.
|
||||
(DECISIONS/MAIN-orchestration-archive.md)"""
|
||||
tick = self.mode_tick
|
||||
if tick is None:
|
||||
return False
|
||||
if tick.action == modetrade.CHARGE:
|
||||
# OPT-20: the ask carries PROBE_W for exactly the probes whose pin
|
||||
# is filed this tick. (DECISIONS/MAIN-orchestration-archive.md)
|
||||
# is filed this tick (`decide()` ran before the pin, so they can
|
||||
# differ by one cycle; left alone the allocator moves the pin's
|
||||
# watts to another battery).
|
||||
# (DECISIONS/MAIN-orchestration-archive.md)
|
||||
pins = _ctl.PROBE_W * sum(
|
||||
1 for c in claims if c.source == "refused"
|
||||
and c.kind == "set" and c.value < 0)
|
||||
@@ -1912,7 +1929,10 @@ class Controller:
|
||||
|
||||
def _reseed_run_edge(self, claims, now) -> None:
|
||||
"""OPT-11 P3: a mode trade just ended, so the loop takes over from a
|
||||
command no loop chose. (DECISIONS/MAIN-orchestration-archive.md)"""
|
||||
command no loop chose: it starts at the house's own point E, not at the
|
||||
trade's watts, and OPT-15 HOLDS it at E (`_edge_grid()`) until the
|
||||
meter shows the step or `EDGE_HOLD_S` passes.
|
||||
DECISIONS/OPT-11-battery-modes.md"""
|
||||
self._edge = None
|
||||
batt = self.site_batt_w
|
||||
if batt is None or self.grid is None:
|
||||
@@ -1929,8 +1949,9 @@ class Controller:
|
||||
|
||||
def _edge_grid(self, end, now):
|
||||
"""OPT-15: the grid reading the loop acts on this tick. While a run
|
||||
edge is held, the target itself (no error, the command stays at E).
|
||||
(DECISIONS/MAIN-orchestration-archive.md)"""
|
||||
edge is held, the target itself (no error, the command stays at E); the
|
||||
hold ends when the meter reaches the target, on `end`, or after
|
||||
`EDGE_HOLD_S`. (DECISIONS/MAIN-orchestration-archive.md)"""
|
||||
e = self._edge
|
||||
if e is None:
|
||||
return self.grid
|
||||
@@ -1951,7 +1972,8 @@ class Controller:
|
||||
def _charge_track(self, claims, maint_results) -> None:
|
||||
"""SAFETY-93, N>=2: a charge ends when its battery leaves CHARGE or can
|
||||
no longer be placed (stale link, refusal). Kept until the loop runs.
|
||||
(DECISIONS/MAIN-orchestration-archive.md)"""
|
||||
SAFETY-95: a drain likewise; its refusal (SAFETY-73) is read from the
|
||||
battery (+1 = will not give). (DECISIONS/MAIN-orchestration-archive.md)"""
|
||||
if len(self.inverters) < 2:
|
||||
return
|
||||
live = set(self.live_units())
|
||||
@@ -1970,8 +1992,9 @@ class Controller:
|
||||
def _reseed_charge_end(self, maint_results) -> None:
|
||||
"""SAFETY-93, N>=2: when a battery's maintenance charge ends, the loop
|
||||
starts from what the others stood at plus that battery's new command
|
||||
(its HOLD pin, or its last discharge if it is free; a charge stops).
|
||||
(DECISIONS/MAIN-orchestration-archive.md)"""
|
||||
(its HOLD pin, or its last discharge if it is free; a charge stops);
|
||||
else the charge's watts land on the others (B 0 -> -1600 W in one
|
||||
cycle). DECISIONS/SAFETY-93-charge-end.md"""
|
||||
ended, self._charge_ended = self._charge_ended, frozenset()
|
||||
if len(self.inverters) < 2 or not ended:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user