OPT-34 round 2: the latch follows the plan, up or down, and drops what left it
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:
co-authored by
Claude
parent
412e47f111
commit
eee7616022
@@ -7,7 +7,7 @@
|
||||
- The night grid charge no longer raises "Battery stopped limiting the peak" when the 15-minute period stays on the aim. A steadily rising load now alarms a little later, and with a peak margin of 0 a period can end up to 10 W over the limit without this alarm ([[SAFETY-126]]).
|
||||
- A night grid charge for *Leftover solar* no longer stops at 03:00 when the solar forecast sensors went quiet in the evening. The evening's figure is used until 06:00, and a charge that has started goes on to its level ([[OPT-34]]).
|
||||
|
||||
**A patch bump: one fix for charging from the grid under the peak limit, and no option changes.**
|
||||
**A patch bump: two fixes, one for charging from the grid under the peak limit and one for the night charge of Leftover solar, and no option changes.**
|
||||
No option was added, removed or renamed, and no default changed (86 options).
|
||||
**Existing installs need no option change:** options that start 0.19.1 also
|
||||
start 0.19.2. The change below applies with one inverter or two.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -236,12 +236,14 @@ def opts(late=False, cap=2500):
|
||||
return o
|
||||
|
||||
|
||||
def sensors(*, silent=True, fresh_from=None, fresh=(1.0, 5.0)):
|
||||
def sensors(*, silent=True, fresh_from=None, fresh=(1.0, 5.0),
|
||||
fresh_until=None):
|
||||
"""Sensors that update until 17:05 (or all night) and then show that
|
||||
reading for good; from `fresh_from` a new reading every call."""
|
||||
def f(now, ent):
|
||||
if fresh_from is not None and now >= fresh_from:
|
||||
return st(fresh[0] if ent == T_ENT else fresh[1], now)
|
||||
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)
|
||||
return f
|
||||
@@ -260,7 +262,8 @@ def run_night(spec):
|
||||
store.data[unit_key(f"inverter-{u}", k)] = val
|
||||
sens = sensors(silent=spec.get("silent", True),
|
||||
fresh_from=spec.get("fresh_from"),
|
||||
fresh=spec.get("fresh", (1.0, 5.0)))
|
||||
fresh=spec.get("fresh", (1.0, 5.0)),
|
||||
fresh_until=spec.get("fresh_until"))
|
||||
clock = [EVENING[0]]
|
||||
|
||||
class H(rp.Hass):
|
||||
@@ -399,6 +402,12 @@ SCEN = {
|
||||
"tail": dict(start=at(4, 5, 40), hours=80 / 60.0,
|
||||
soc={"a": 70.0, "b": 70.0}, restarts=[at(4, 6, 30)],
|
||||
marks=[at(4, 5, 50), at(4, 6, 10), at(4, 6, 40)]),
|
||||
# a fresh, brighter figure at 03:10 lowers A's level below its SoC (A
|
||||
# stops, B goes on), then the sensors go quiet: the forecast ends at 06:10
|
||||
"lowered": dict(start=at(4, 2, 30), hours=4.5, house_w=500.0,
|
||||
soc={"a": 30.0, "b": 30.0}, fresh_from=at(4, 3, 10),
|
||||
fresh_until=at(4, 3, 10), fresh=(30.0, 30.0),
|
||||
marks=[at(4, 3, 5), at(4, 3, 20), at(4, 6, 30)]),
|
||||
# the same, B already full and a 1.5 kW limit: the run is sized at the
|
||||
# battery's 2.5 kW rating and ends at 06:33, before A is there, so the
|
||||
# next slice is planned after 06:00 and is only A's
|
||||
@@ -509,6 +518,23 @@ check("06:10 and 06:40 (the second after a restart at 06:30): A still "
|
||||
and t6["why_a"] == "grid_charge" and t7["why_a"] == "grid_charge")
|
||||
check("... and ends the hour higher than it was (+8 % or more from 06:00)",
|
||||
tl["soc_at"]["06:55"][0] > tl["soc_at"]["06:00"][0] + 8.0)
|
||||
lo = R["lowered"]
|
||||
l0, l1, l2 = (lo["marks"][k] for k in ("03:05", "03:20", "06:30"))
|
||||
check("a fresh, brighter figure lowers A's level below its SoC during the "
|
||||
f"charge ({l0['target_a']} -> {l1['target_a']}, SoC "
|
||||
f"{l1['soc_a']:.1f} %), and A stops",
|
||||
l0["target_a"] is not None and l1["target_a"] is not None
|
||||
and l1["target_a"] < l1["soc_a"] - 2.0
|
||||
and l1["target_a"] < l0["target_a"] - 10.0
|
||||
and lo["soc_at"]["03:30"][0] < lo["soc_at"]["03:20"][0] + 0.5)
|
||||
check("the sensors go quiet, the forecast ends (06:30): the latch holds the "
|
||||
"LOWERED level, so A does not charge back up to the old one "
|
||||
f"({lo['soc_at']['03:30'][0]:.1f} -> {lo['soc_at']['06:55'][0]:.1f} %), "
|
||||
"while B goes on",
|
||||
l2["fc_none"] and l2["target_a"] is not None
|
||||
and l2["target_a"] < l2["soc_a"] and l2["p_a"] > -200.0
|
||||
and lo["soc_at"]["06:55"][0] < lo["soc_at"]["03:30"][0] + 1.0
|
||||
and lo["soc"][1] > lo["soc_at"]["03:30"][1] + 15.0)
|
||||
tb = R["tail_bfull"]
|
||||
check("B full: the run ends at 06:33 before A is at its level, so the next "
|
||||
"slice is only A's, planned after the figure has ended with A's "
|
||||
|
||||
Reference in New Issue
Block a user