UX-77: Home draws today's grid charge for the hours control will really charge
checks / unit (pull_request) Successful in 27m7s
infra-pin / fresh (pull_request) Successful in 1s

The cheap run is sized at the full rating, so under the peak limit a battery is
still short when it ends, and control plans the next run then. Home drew only
the run under way. `cheaproll` rolls the run on the way control does
(`modetrade.energy()` + `priceruns.plan()` at each run end, lifted from
`dayview._levels()`), reading the levels at the charge power the peak limit
leaves (peak aim less the house, else the rating). `homeview.mode_runs()` and
`price_windows()` use it for today: the grid block, the Today line and the
chart's cheap bars run to where control stops, each battery's block to its
own stop. Display only: no control change.

test_ux54: today's grid block of the night-before scenario now follows the roll.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-11-05 21:48:56 +01:00
co-authored by Claude
parent af2d0d2f61
commit 45535e19d7
7 changed files with 541 additions and 39 deletions
+24 -11
View File
@@ -39,7 +39,8 @@ sys.path.insert(0, os.path.join(HERE, "tools"))
logging.disable(logging.WARNING)
import ux54_scenarios as S # noqa: E402
from app import arbitrage, dayplan, dayview, homeview, modetrade # noqa: E402
from app import arbitrage, cheaproll, dayplan, dayview, homeview # noqa: E402
from app import modetrade # noqa: E402
from app import packmode, packshare, peakguard, solarfc, uitext # noqa: E402
from app.main import pack_energy_of # noqa: E402
from app.price import Uncertainty # noqa: E402
@@ -232,11 +233,19 @@ END_S = S.brussels(END).strftime("%H:%M")
check(f"the cheap run is 01:00 to {END_S} (02:30 for this house)",
c_n.mode_runs.cheap.start == at(1, 0, 0) and END == at(2, 30, 0))
nb = tab(c_n, at(0, 18))["days"][0]["batteries"]
check("in the night before the cheap run both charge from the grid in it "
f"(01:00 to {END_S}), then give at 17:00",
all([r["kind"] for r in b["blocks"]] == ["grid", "give"] for b in nb)
and all(b["blocks"][0]["from"] == "01:00"
and b["blocks"][0]["to"] == END_S for b in nb))
# UX-77: today's block runs to where control stops charging: the 1.5 h run is
# sized at the full rating, the peak limit leaves 3.1 kW, so control plans the
# next run when it ends (`cheaproll`), and each battery's block ends at its stop.
RL = cheaproll.for_today(c_n, c_n.mode_runs, at(0, 18))
ENDS = [S.brussels(RL.end_for(inv.name, END)).strftime("%H:%M")
for inv in c_n.inverters]
check("in the night before the cheap run both charge from the grid from "
f"01:00 to where control stops ({ENDS}, the run rolls on after "
f"{END_S}), then give at 17:00",
RL is not None and RL.end > END and ENDS[0] < ENDS[1]
and all([r["kind"] for r in b["blocks"]] == ["grid", "give"] for b in nb)
and all(b["blocks"][0]["from"] == "01:00" for b in nb)
and [b["blocks"][0]["to"] for b in nb] == ENDS)
# UX-53: no grid block while the gate refuses.
for why in ("peak", "not_worth"):
c_n.mode_gate = why
@@ -255,12 +264,16 @@ def at_h(b, h):
LVL_A = c_n.plan_targets()[c_n.inverters[0].name]
END_HS = [(S.brussels(RL.end_for(inv.name, END)).replace(tzinfo=None)
- S.brussels(DAY0).replace(tzinfo=None)).total_seconds() / 3600.0
for inv in c_n.inverters]
check(f"the curve charges in a grid block: A (planned level {LVL_A:.0f} %) "
"and B (Cheap hours, to the end of its run) are well above where they "
"were at 01:00, and stay put after the run",
all(at_h(b, END_H) - at_h(b, 1.0) > 8.0
and abs(at_h(b, END_H + 0.5) - at_h(b, END_H)) < 1e-6
for b in nb) and nb[0]["levels"][0][1] == 30.0)
"and B (Cheap hours, to the end of its block) are well above where "
"they were at 01:00, and stay put after the block",
all(at_h(b, eh) - at_h(b, 1.0) > 8.0
and all(abs(v - at_h(b, eh)) < 1e-6 for x, v in b["levels"]
if eh + 1e-6 < x < eh + 3.0)
for b, eh in zip(nb, END_HS)) and nb[0]["levels"][0][1] == 30.0)
# Modes off: the verdict's legs.
c_f = S.make("off", at(10, 7))
fb = tab(c_f, at(10, 7))