From 1c9fefd9dd5072c88a2362b76955277890d1145e Mon Sep 17 00:00:00 2001 From: glenn schrooyen Date: Fri, 30 Oct 2026 08:11:42 +0100 Subject: [PATCH] Sync 0.19: the Forecast tab extends a cheap run with control's gap (TARIFF-24) UX-54's Forecast tab (release/1.0) re-plans tomorrow's cheap run as control will, with the gap `required`. It still added the "no weather" x1.5 always, while control (TARIFF-24, from master) drops it when a forecast is read. In the UX-54 scenarios the tab asked 0.225 where control needs 0.15. It now asks `arbitrage.weather_uncertainty(True, c.solar_fc)`, so the two agree again, as they did on release/1.0 alone. Display only; no control change. The tab's blocks in every UX-54 scenario, every 2 h, are identical to release/1.0. test_ux54.py pins it (the old line fails it). The TARIFF-24 DECISIONS note names the tab as the fourth place that reads the rule. Co-Authored-By: Claude --- .../TARIFF-24-modes-need-the-tariff-task.md | 5 ++-- goodwe_controller/app/dayview.py | 7 +++-- goodwe_controller/test_ux54.py | 29 +++++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/goodwe_controller/DECISIONS/TARIFF-24-modes-need-the-tariff-task.md b/goodwe_controller/DECISIONS/TARIFF-24-modes-need-the-tariff-task.md index 3bdec04..f2c3d10 100644 --- a/goodwe_controller/DECISIONS/TARIFF-24-modes-need-the-tariff-task.md +++ b/goodwe_controller/DECISIONS/TARIFF-24-modes-need-the-tariff-task.md @@ -50,9 +50,10 @@ was one input judged twice. **The rule** (`arbitrage.weather_uncertainty()`). With battery modes on and a usable forecast (`solar_fc` not None, the day plan's own test) the doubt is not added: the gap needed is the cycle cost, widened by the price product only. -No forecast, or a stale or unusable one: 1.5 x, as before. It is read in three +No forecast, or a stale or unusable one: 1.5 x, as before. It is read in four places that must agree: the run plan (`_price_runs()`), the charge gate -(`_mode_roles()`) and Home's mirror (`homeview.trade_verdict()`). The forecast +(`_mode_roles()`), Home's mirror (`homeview.trade_verdict()`) and the Forecast +tab's longer cheap run (`dayview.py`, from release/1.0). The forecast is read at most every 60 s and the plan key already holds "forecast or not", so the gap follows in the cycle the read changes, both ways, with one exception: a run planned with a forecast that is under way keeps that gap to its end diff --git a/goodwe_controller/app/dayview.py b/goodwe_controller/app/dayview.py index 0208477..799737b 100644 --- a/goodwe_controller/app/dayview.py +++ b/goodwe_controller/app/dayview.py @@ -23,7 +23,7 @@ from . import arbitrage, dayplan, homeview as hv, modetrade, priceruns from . import solarfc from .daycurve import Batt, curve from .packmode import SOLAR_FIRST -from .price import Uncertainty, brussels, required_spread, uncertainty_of +from .price import brussels, required_spread, uncertainty_of BLOCKS = ("grid", "give", "cycle") # the kinds a battery's blocks carry LEVEL_DP = 1 # the tab shows a level to 0.1 % @@ -204,9 +204,12 @@ def _levels(c, est, fc, trust, now, end, runs, floor, ceil, notes_b, shadow, end_t = None if plan_on and cheap is not None and rp.charge_useful() \ and n0 <= cheap.start < n1 and plan is not None: + # The gap control's run plan needs (TARIFF-24): no x1.5 with a + # forecast read. required = required_spread( arbitrage.threshold_eur_kwh(c.o), - uncertainty_of(plan.read.product) | {Uncertainty.NO_WEATHER}) + uncertainty_of(plan.read.product) + | arbitrage.weather_uncertainty(True, c.solar_fc)) end_t = cheap.end for _ in range(24): ext = dataclasses.replace(rp, cheap=priceruns.Run( diff --git a/goodwe_controller/test_ux54.py b/goodwe_controller/test_ux54.py index a4cdaef..5cf5861 100644 --- a/goodwe_controller/test_ux54.py +++ b/goodwe_controller/test_ux54.py @@ -1456,6 +1456,35 @@ hi_ = tab(c_t, at(10, 7))["days"][0]["batteries"][0]["levels"] check("on a controller the curve is built with the plan's trust " "(`solar_trust_pct`)", hi_ and max(v for _h, v in hi_) > hi_[0][1]) +# TARIFF-24 (sync 0.19): the tab extends tomorrow's cheap run as control +# re-plans it, so it asks control's gap: the cycle cost with a forecast read, +# not 1.5 x it. +from app.price import required_spread, uncertainty_of # noqa: E402 +gaps, _plan_real = [], dayview.priceruns.plan + + +def _spy(*a, **k): + gaps.append(k.get("required_eur_kwh")) + return _plan_real(*a, **k) + + +c_g = S.make("dynamic", at(17, 0)) +dayview.priceruns.plan = _spy +try: + tab(c_g, at(17, 0)) +finally: + dayview.priceruns.plan = _plan_real +_p = c_g.fresh_tariff_plan(at(17, 0)) +want = required_spread(arbitrage.threshold_eur_kwh(c_g.o), + uncertainty_of(_p.read.product) + | arbitrage.weather_uncertainty(True, c_g.solar_fc)) +print(f" dynamic 17:00: the tab's extension asks {gaps}, control needs {want}") +check("the tab extends tomorrow's cheap run with control's gap (with a " + "forecast: the cycle cost, not 1.5 x it)", + c_g.solar_fc is not None and bool(gaps) + and all(abs(g - want) < 1e-9 for g in gaps) + and abs(want - arbitrage.threshold_eur_kwh(c_g.o)) < 1e-9) + print() if fails: print(f"{len(fails)} FAILED")