Merge release/1.0 (SAFETY-78, #154) into opt11-p4-solar-forecast
checks / unit (pull_request) Successful in 23m54s
infra-pin / fresh (pull_request) Successful in 1s

Conflicts: modetrade.decide() takes both the day plan (targets) and
SAFETY-78's draining flag; the Why line keeps SAFETY-78's other_cycle
ahead of the plan's word (test added).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-09-29 22:37:09 +02:00
co-authored by Claude Opus 5.5
18 changed files with 743 additions and 93 deletions
+8 -2
View File
@@ -43,7 +43,9 @@ Rules that keep this safe (tests: test_modes_p3.py):
* a battery out of a run (full, or at the floor) stays out until the run
ends, so the ceiling and the floor do not re-arm within one run;
* a battery that refuses a direction (SAFETY-52/73) takes no part in a
run in that direction: the refusal wins over the run.
run in that direction: the refusal wins over the run;
* no grid charge while another battery drains for its monthly cycle
(`draining`, SAFETY-78): it would charge against the drain.
"""
from dataclasses import dataclass
@@ -149,7 +151,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) -> Tick:
targets=None, draining=False) -> Tick:
"""This tick's ranges, roles and site ask.
`runs` is the kept `priceruns.RunPlan` or None (no fresh prices).
@@ -161,6 +163,8 @@ def decide(runs, now, packs, *, charge_ok, sell_ok, trade_why, price_now,
`charging_now` says last tick charged (then any headroom above 0 keeps
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").
"""
plan_on = runs is not None and runs.on
in_cheap = plan_on and runs.cheap.holds(now) and runs.charge_useful()
@@ -170,6 +174,8 @@ def decide(runs, now, packs, *, charge_ok, sell_ok, trade_why, price_now,
why = None
ch = chargers(packs, ceiling_pct, done_charge, targets) if in_cheap \
else []
if ch and draining:
ch, why = [], "cycle"
charging = False
if ch:
need = 0.0 if charging_now else START_W