OPT-36: Leftover solar counts what Cheap hours and Solar first batteries will give before buying
checks / unit (pull_request) Failing after 59m31s
infra-pin / fresh (pull_request) Successful in 2s

Live 0.20.0: A (Leftover solar, 60 %) charged to 95 % while B (Cheap hours,
94 %) already covered the expensive hours. dayplan.plan() now takes off the
shortfall what Cheap hours batteries will hold when the hours start (the
ceiling or what their cheap run reaches under the peak aim) and what Solar
first batteries will still hold after the night's use. Home reads a give
block after the planned charge, plan_cover() is the plan's own figure, and
the Why line says other batteries cover.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-11-02 02:03:04 +01:00
co-authored by Claude
parent 01e88a24f2
commit 05746c0564
11 changed files with 867 additions and 23 deletions
+14 -3
View File
@@ -254,9 +254,16 @@ check("B on Solar first never gets a level (never charges from the grid)",
more = owner_plan(6.0, [pk(A, packmode.LEFTOVER_SOLAR),
pk(B, packmode.CHEAP_HOURS, cap=CAP_B,
rating=4600.0)])
check("B on Cheap hours fills up and is no part of A's share: A's level is "
"the same, B is not in the plan",
more.targets == res[6.0].targets and B not in more.targets
# OPT-36 (on purpose, was: "B on Cheap hours ... A's level is the same"): a
# Cheap hours battery still fills up and is still not in the plan, but what
# its own cheap run will give in the expensive hours is taken off the
# shortfall before Leftover solar shares it. Here B (10 %, 4.6 kW, the run
# 22:00-07:00) reaches the ceiling and covers all of the 6 kWh case.
check("B on Cheap hours fills up and gives first (OPT-36): A buys nothing, "
"B is not in the plan",
more.need_kwh[A] == 0.0 and more.targets[A] == FLOOR
and more.cheap_kwh > more.short_kwh - 1e-6
and more.uncovered_kwh == 0.0 and B not in more.targets
and modetrade.charge_to(pk(B, packmode.CHEAP_HOURS, cap=CAP_B), CEIL,
more.targets) == CEIL)
# The profile is net of the sun the house used, so an hour with sun is read
@@ -439,6 +446,10 @@ print("5. the screens say what control does (EN and NL)")
now = DAY0 + timedelta(minutes=18, seconds=5)
c = S.make("dynamic", now, soc=(20.0, 20.0))
c.solar_fc = S.forecast(now, 3.0, 3.0)
# OPT-36: B (Cheap hours) now gives first, so A buys only what B's cheap run
# cannot reach. A 2.5 kW house leaves B 0.95 kW of peak room in the 3.5 h run:
# B gets part of the way, A buys the rest.
c.house_profile = profile_of({h: 2500.0 for h in range(24)})
c._runs_key = None
c._mode_roles(now, None)
tg = c.plan_targets()