SAFETY-135: a young 15-minute period does not cost the night charge the whole period
With battery modes on, a grid charge the peak guard (or a measured headroom) stops in the first 60 s of a period no longer latches off for the period: it may start again once the period is 60 s old, if the guard is clear and the headroom is over START_W. A stop after 60 s latches as before. In a young period no charge starts while the batteries' give is still falling (over START_W within 15 s), since the meter may not show that drop yet. The guard itself is unchanged, so a real over-aim load is cut on the same cycle. DECISIONS/SAFETY-135-young-period.md, test_safety135.py. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
a8ecf171d6
commit
66d8777b39
@@ -36,6 +36,9 @@ Rules that keep this safe (tests: test_modes_p3.py, test_price_run_fixes.py):
|
||||
makes the other batteries stop giving power, so a few watts of charge
|
||||
flipping on and off would swing the house between them and the grid
|
||||
(measured on the 2026-09-09 replay: 13 flips in 4 minutes at 3.5 kW);
|
||||
SAFETY-135: a stop in a period's first YOUNG_S s holds only until then,
|
||||
not to the period's end, and no charge starts then while the batteries'
|
||||
give is still falling (`young`);
|
||||
* selling needs selling on for that battery, the sell run, a price >= 0
|
||||
now, and the battery above `arbitrage_soc_floor`; it stops there. The
|
||||
peak guard never stops a sell (section 9.1: discharge is never capped);
|
||||
@@ -69,6 +72,12 @@ PRICE_MODES = (LEFTOVER_SOLAR, CHEAP_HOURS)
|
||||
# would stop covering the house for less than they give.
|
||||
START_W = 500.0
|
||||
|
||||
# SAFETY-135: seconds a 15-minute period is young, and how far back a
|
||||
# battery's give counts as maybe not yet on the meter.
|
||||
# DECISIONS/SAFETY-135-young-period.md
|
||||
YOUNG_S = 60.0
|
||||
GAVE_S = 15.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Pack:
|
||||
@@ -164,7 +173,7 @@ def energy(packs, floor_pct, ceiling_pct, done_charge=(), done_sell=(),
|
||||
def decide(runs, now, packs, *, charge_ok, sell_ok, trade_why, price_now,
|
||||
headroom_w, floor_pct, ceiling_pct, house_w, charging_now=False,
|
||||
blocked=False, done_charge=(), done_sell=(),
|
||||
targets=None, draining=False) -> Tick:
|
||||
targets=None, draining=False, young=False) -> Tick:
|
||||
"""This tick's ranges, roles and site ask.
|
||||
|
||||
`runs` is the kept `priceruns.RunPlan` or None (no fresh prices).
|
||||
@@ -177,7 +186,8 @@ def decide(runs, now, packs, *, charge_ok, sell_ok, trade_why, price_now,
|
||||
it going); `blocked` that the guard or the headroom stopped a charge in
|
||||
this 15-minute period. `targets` is the day plan (`charge_to()`).
|
||||
`draining` says a battery drains for its monthly cycle, so none may
|
||||
charge from the grid (why "cycle").
|
||||
charge from the grid (why "cycle"). `young` says no charge may start
|
||||
yet in this young period (SAFETY-135, why "young").
|
||||
"""
|
||||
plan_on = runs is not None and runs.on
|
||||
in_cheap = plan_on and runs.cheap.holds(now) and runs.charge_useful()
|
||||
@@ -198,6 +208,8 @@ def decide(runs, now, packs, *, charge_ok, sell_ok, trade_why, price_now,
|
||||
why = trade_why
|
||||
elif headroom_w is None or not headroom_w > need:
|
||||
why = "headroom"
|
||||
elif young and not charging_now:
|
||||
why = "young"
|
||||
else:
|
||||
charging = True
|
||||
se = sellers(packs, floor_pct, done_sell) if in_sell and not charging \
|
||||
|
||||
Reference in New Issue
Block a user