OPT-34 round 2: the latch follows the plan, up or down, and drops what left it
checks / unit (pull_request) Successful in 18m37s
infra-pin / fresh (pull_request) Successful in 1s

While a forecast is present and the cheap run holds, the night-charge latch is
rebuilt from the plan: a latched battery takes its current level whatever its
role this tick (a fresh figure may lower it below the SoC), one that left the
plan is dropped, a new one comes in only while it charges from the grid.
Test: a fresh brighter figure lowers A below its SoC, the sensors go quiet, A
does not charge back up. CHANGELOG summary says two fixes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
This commit is contained in:
glenn schrooyen
2026-10-31 19:17:20 +01:00
co-authored by Claude
parent 412e47f111
commit eee7616022
3 changed files with 53 additions and 21 deletions
+23 -17
View File
@@ -2392,9 +2392,11 @@ class Controller:
charge that has started keeps its planned level when the forecast goes
mid-charge (the kept figure ends and the sensors are still silent).
While there is a forecast, fresh or kept, each battery that charges
from the grid in the cheap run takes its planned level into the latch;
a fresh figure moves it up or down, as it moves the plan. While there
While there is a forecast, fresh or kept, the latch is rebuilt each
cycle from the plan: a battery that charges from the grid in the cheap
run comes in with its planned level, and stays in it at whatever level
the plan gives it, up or down as a fresh figure moves it (also when it
has stopped, at a lowered level), until it leaves the plan. While there
is none, `plan_targets()` gives the latched levels, so the charge goes
on to them, and `weather_held()` keeps the narrow price gap. The latch
lasts through every cheap run on and one price slot after the last
@@ -2403,20 +2405,24 @@ class Controller:
tick = self.mode_tick
run = None if runs is None else runs.cheap
in_run = run is not None and run.holds(now)
if lt is not None:
if in_run:
lt = (max(lt[0], run.end + timedelta(
seconds=CHARGE_LATCH_GRACE_S)), lt[1])
elif now >= lt[0]:
lt = None
if (in_run and self.solar_fc is not None and targets
and tick is not None and tick.action == modetrade.CHARGE):
lv = dict(lt[1]) if lt is not None else {}
lv.update({n: float(targets[n]) for n, r in tick.roles.items()
if r == modetrade.CHARGE and n in targets})
if lv:
until = run.end + timedelta(seconds=CHARGE_LATCH_GRACE_S)
lt = (max(until, lt[0]) if lt is not None else until, lv)
until = None if run is None else run.end + timedelta(
seconds=CHARGE_LATCH_GRACE_S)
if lt is not None and not in_run and now >= lt[0]:
lt = None
if in_run and self.solar_fc is not None:
# Rebuilt from the plan: a battery already latched follows its
# level up or down whatever its role this tick (a fresh figure may
# lower it below the SoC), one that left the plan is dropped, and
# a new one comes in only while it charges from the grid.
tg = targets or {}
lv = {n: float(tg[n]) for n in (lt[1] if lt else {}) if n in tg}
if tick is not None and tick.action == modetrade.CHARGE:
lv.update({n: float(tg[n]) for n, r in tick.roles.items()
if r == modetrade.CHARGE and n in tg})
lt = (max(until, lt[0]) if lt is not None else until, lv) \
if lv else None
elif lt is not None and in_run:
lt = (max(lt[0], until), lt[1])
if lt != old:
self._charge_latch = lt
self.store.set(CHARGE_LATCH_KEY, None if lt is None else {