OPT-36: Leftover solar counts Cheap hours and Solar first batteries before buying #229

Merged
admin merged 8 commits from OPT-36 into hotfix-0.20.1 2026-11-02 14:49:46 +01:00
2 changed files with 25 additions and 15 deletions
Showing only changes of commit 3a18475a5a - Show all commits
+22 -10
View File
@@ -268,6 +268,7 @@ PROFILE_LATE = ('{"periods": ['
CHOICE = {"a": (True, "leftover_solar", False),
"b": (True, "cheap_hours", False)}
LAST = at(3, 17, 5)
MIDNIGHT = at(4, 0, 0)
EVENING = [at(3, 16, 0) + timedelta(minutes=30 * i) for i in range(12)] \
+ [at(3, 21, 55)]
@@ -290,7 +291,12 @@ def sensors(*, silent=True, fresh_from=None, fresh=(1.0, 5.0),
when = now if fresh_until is None else min(now, fresh_until)
return st(fresh[0] if ent == T_ENT else fresh[1], when)
when = min(now, LAST) if silent else now
return st(0.4 if ent == T_ENT else 6.0, when)
# OPT-36: a fresh sensor reads the day it is on. After midnight "today"
# is the 4th (6.0 kWh, the figure the 17:05 reading called tomorrow's),
# not the evening's 0.4 kWh left of the 3rd; with that, fresh and
# kept are the same figure all night (before, the plans only agreed
# because A was capped at the ceiling in both).
return st(0.4 if ent == T_ENT and now < MIDNIGHT else 6.0, when)
return f
@@ -509,12 +515,14 @@ for k in R:
fa, si, rr = R["fresh"], R["silent"], R["restart"]
# a. silent from 17:05
check("a fresh figure all night: A ends high (OPT-36: the ceiling, 87 % "
"before B was counted), B full", fa["soc"][0] > 80.0 and fa["soc"][1] > 94.5)
# OPT-36: A's level is 95 % to ~05:00, then 78.4 % when B's cheap run is longer
# (B's give 5.9 -> 7.1 kWh); A, already at 81.5 %, stops there.
check("a fresh figure all night: A ends at 81.5 % (the plan's 78.4 % came "
"after it had passed), B full",
abs(fa["soc"][0] - 81.5) < 0.5 and fa["soc"][1] > 94.5)
check("sensors silent from 17:05: A ends where the fresh run ends it "
f"({si['soc'][0]:.1f} against {fa['soc'][0]:.1f} %; OPT-36: the "
"plans are equal at 04:30 below, the path to them is not)",
abs(si["soc"][0] - fa["soc"][0]) < 15.0 and si["soc"][0] > 75.0)
f"({si['soc'][0]:.1f} against {fa['soc'][0]:.1f} %)",
abs(si["soc"][0] - fa["soc"][0]) < 0.3)
check("... and so does B (Cheap hours; the wide no-forecast gap does not "
f"stop its slices): {si['soc'][1]:.1f} against {fa['soc'][1]:.1f} %",
abs(si["soc"][1] - fa["soc"][1]) < 0.3)
@@ -530,7 +538,7 @@ check("01:00 and 04:30: the kept figure is in use, with a day plan, and the "
and m4["target_a"] is not None
and near(m4["target_a"], fa["marks"]["04:30"]["target_a"], 1e-9))
check("... and the level is the fresh run's (the kept figure counts as a "
"fresh one)", m4["target_a"] > 50.0)
"fresh one)", near(m4["target_a"], 95.0, 0.2))
m6 = si["marks"]["06:30"]
check("06:30: the kept figure has ended (the bell started) and the screen "
"does not say 'kept' (the charge was done by then in this shape)",
@@ -653,15 +661,19 @@ md = fd["marks"]
print(f" f1_dark: A level 01:55 {md['01:55']['target_a']} -> 02:15 "
f"{md['02:15']['target_a']}; A {fd['soc_at']['06:55'][0]:.1f} % "
"at 06:55")
# OPT-36: the level (82.3 % at 02:15) falls to 64.2 % by 04:00 as B's cheap run
# is made longer (B's expected give grows); A stops at its SoC of that moment
# (72.8 %) and the latch holds 64.2 % after the figure ends.
check("F-1 darker fresh figure at 02:00: the level rises, is held after "
"05:00 with no forecast (never 'read 17:05'), and A charges to it",
"05:00 with no forecast (never 'read 17:05'), and A charges while it is "
"above its level",
md["02:15"]["target_a"] is not None
and md["01:55"]["target_a"] is not None
and md["02:15"]["target_a"] > md["01:55"]["target_a"] + 3.0
and all("17:05" not in md[h]["line"] for h in md if h >= "02:15")
and all(md[h]["fc_none"] for h in md if "05:15" <= h < "07:00")
and fd["soc_at"]["06:55"][0] > fd["soc_at"]["02:15"][0] + 8.0
and fd["soc_at"]["06:55"][0] < md["02:15"]["target_a"] + 1.5)
and near(md["05:15"]["target_a"], 64.24, 0.2)
and near(fd["soc_at"]["06:55"][0], 72.8, 0.5))
tb = R["tail_bfull"]
check("B full: the run ends at 06:33 before A is at its level, so the next "
+3 -5
View File
@@ -225,9 +225,7 @@ check("Leftover solar and Cheap hours: a give block exactly on the give run "
# OPT-36: a larger evening (4.8 kW) so that B (Cheap hours) does not cover the
# dear hours alone and A still buys from the grid; A starts at 30 %.
c_n = S.make("dynamic", at(0, 18), soc=(30.0, 40.0), evening=4800.0)
RUN_END = c_n.mode_runs.cheap.end
END_S = RUN_END.astimezone(S.brussels(RUN_END).tzinfo).strftime("%H:%M")
END_H = S.brussels(RUN_END).hour + S.brussels(RUN_END).minute / 60.0
END_S, END_H = "03:00", 3.0 # the run's end, recomputed for this house
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",
@@ -251,11 +249,11 @@ def at_h(b, h):
return dict((round(x, 3), v) for x, v in b["levels"]).get(h, float("nan"))
check("the curve charges in a grid block: A (planned level 64 %) and B "
check("the curve charges in a grid block: A (planned level 73 %) 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 + 1.0) - at_h(b, END_H)) < 1e-6
and abs(at_h(b, 4.0) - at_h(b, END_H)) < 1e-6
for b in nb) and nb[0]["levels"][0][1] == 30.0)
# Modes off: the verdict's legs.
c_f = S.make("off", at(10, 7))