Merge master (0.19.2, f1c9733) into sync-0.19: TARIFF-24 and SAFETY-125 reach release/1.0
Brings in 0.19.1 (TARIFF-24) and 0.19.2 (SAFETY-125). Git merged every file without a text conflict. config.yaml's version becomes 0.19.2, the newest CHANGELOG section: test_version_gate.py (PKG-14, 11b) requires the two to be equal, and release/1.0 took 0.19.0 the same way (#207). The 0.19.1 and 0.19.2 CHANGELOG sections come in as history. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -540,6 +540,12 @@ rp.Hass.state = _sensor
|
||||
rp_start = [UTC0]
|
||||
FC_OPTS = {"pv_forecast_entity": "sensor.energy_production_today_remaining",
|
||||
"cycle_cost_threshold_eur": 0.20}
|
||||
# TARIFF-24 round 2: with a forecast read, a price mode needs the cycle cost
|
||||
# itself (0.20 here), no longer 1.5 times it (0.30). These scenarios were
|
||||
# written for a 0.30 gap, and a midday 0.21 gap now plans a grid top-up they
|
||||
# are not about, so they ask for 0.30 with the forecast; the no-forecast leg
|
||||
# (`rows_nf`) keeps 0.20, which is 0.30 widened: the same gate on both sides.
|
||||
GATE_030 = {"cycle_cost_threshold_eur": 0.30}
|
||||
|
||||
|
||||
def seed_profile(c):
|
||||
@@ -597,7 +603,8 @@ def invariants(tag, rows, c):
|
||||
|
||||
|
||||
# Sunny: 40 kWh forecast. B (Leftover solar) at 30 % needs nothing.
|
||||
rows, st_, c = run_day(SF_LS, fc=40.0, soc0={"a": 60.0, "b": 30.0})
|
||||
rows, st_, c = run_day(SF_LS, fc=40.0, soc0={"a": 60.0, "b": 30.0},
|
||||
extra=GATE_030)
|
||||
charge = invariants("sunny", rows, c)
|
||||
_last = nth(rows, -1, "a cycle")
|
||||
print(f" sunny: verdict {present(_last) and _last['plan'] and _last['plan'].verdict}, "
|
||||
@@ -607,6 +614,35 @@ check("sunny: a day plan is made, and says sunny",
|
||||
for r in rows))
|
||||
check("sunny: no grid charge at all", not charge)
|
||||
|
||||
# TARIFF-24 round 2: the same day at the cycle cost 0.20 itself. With the
|
||||
# forecast read the gap needed is 0.20 (not 1.5 x = 0.30), so the midday
|
||||
# quarter 0.339 against the evening 0.551 (gap 0.21, between 1x and 1.5x) is
|
||||
# a planned top-up for B (Leftover solar) up to its planned level.
|
||||
rows_g, st_, c_g = run_day(SF_LS, fc=40.0, soc0={"a": 60.0, "b": 30.0})
|
||||
charge_g = invariants("gap between 1x and 1.5x", rows_g, c_g)
|
||||
_first_g = nth(charge_g, 0, "a charge cycle")
|
||||
_gaps = [min(g.mean_eur_kwh for g in r["runs"].give)
|
||||
- r["runs"].cheap.mean_eur_kwh for r in charge_g
|
||||
if r["runs"] is not None and r["runs"].give]
|
||||
print(f" gap 0.20 <= gap < 0.30: {len(charge_g)} charge cycles, run gap "
|
||||
f"{min(_gaps, default=0):.3f}, B to "
|
||||
f"{max((r['soc']['b'] for r in rows_g), default=0):.1f} %")
|
||||
check("a forecast day whose price gap lies between 1x and 1.5x the cycle "
|
||||
"cost (0.20 < 0.21 < 0.30) plans a cheap run: B charges from the grid "
|
||||
"inside it, only B, and only up to its planned level",
|
||||
present(_first_g) and len(_gaps) == len(charge_g)
|
||||
and all(0.20 <= g < 0.30 for g in _gaps)
|
||||
and all(r["runs"].cheap.holds(r["now"]) for r in charge_g)
|
||||
and all(r["roles"][B] == modetrade.CHARGE for r in charge_g)
|
||||
and all(r["roles"][A] != modetrade.CHARGE for r in charge_g)
|
||||
and max(r["soc"]["b"] for r in rows_g) <= 45.64 + 2.0)
|
||||
check("...and the same day with no forecast read does not (it needs 0.30): "
|
||||
"the old gate, kept for a day without a forecast",
|
||||
not [r for r in run_day(SF_LS, fc=None,
|
||||
soc0={"a": 60.0, "b": 30.0})[0]
|
||||
if r["action"] == arbitrage.CHARGE
|
||||
and r["now"].astimezone(L).hour == 12])
|
||||
|
||||
# Dark: 1 kWh. B buys the evening's shortfall only, in the cheap run.
|
||||
rows, st_, c = run_day(SF_LS, fc=1.0, soc0={"a": 60.0, "b": 30.0})
|
||||
charge = invariants("dark", rows, c)
|
||||
@@ -623,7 +659,8 @@ check("dark: A rests while B charges", all(r["roles"][A] == modetrade.REST
|
||||
for r in charge))
|
||||
|
||||
# Cheap hours on a partly sunny day: ignores the plan, fills as in P3.
|
||||
rows, st_, c = run_day(SF_CH, fc=14.0, soc0={"a": 60.0, "b": 30.0})
|
||||
rows, st_, c = run_day(SF_CH, fc=14.0, soc0={"a": 60.0, "b": 30.0},
|
||||
extra=GATE_030)
|
||||
charge = invariants("cheap hours, partly", rows, c)
|
||||
top = max(r["soc"]["b"] for r in rows)
|
||||
print(f" cheap hours partly: B reached {top:.1f} %")
|
||||
@@ -648,7 +685,8 @@ check("forecast lost mid-run: Leftover solar was charging, and stops within "
|
||||
|
||||
# The forecast is far too high (40 kWh, and there is no sun at all): no grid
|
||||
# charge, so B covers less of the evening - a money risk, never a safety one.
|
||||
rows, st_, c = run_day(SF_LS, fc=40.0, soc0={"a": 30.0, "b": 30.0})
|
||||
rows, st_, c = run_day(SF_LS, fc=40.0, soc0={"a": 30.0, "b": 30.0},
|
||||
extra=GATE_030)
|
||||
invariants("forecast too high", rows, c)
|
||||
check("forecast too high: no grid charge, and B still keeps its power for "
|
||||
"the dear run", not [r for r in rows if r["action"]
|
||||
@@ -658,7 +696,7 @@ check("forecast too high: no grid charge, and B still keeps its power for "
|
||||
|
||||
# A refusal wins over the plan.
|
||||
rows, st_, c = run_day(SF_LS, fc=1.0, soc0={"a": 60.0, "b": 30.0},
|
||||
refuse=("b",))
|
||||
refuse=("b",), extra=GATE_030)
|
||||
invariants("refusal", rows, c)
|
||||
check("refusal: B refuses to charge, so the plan's charge ends and B is "
|
||||
"never a charger after it", rows and not [
|
||||
|
||||
Reference in New Issue
Block a user