You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(annual): baseline tariff, run deletion, nav fix and current cap rates
Four changes to the WhatIf tab.
The no-PV/battery counterfactual can now be priced on its own tariff,
defaulting to the Ofgem price cap. A household with no system is not on a
battery tariff - the cheap overnight rates only pay off once there is somewhere
to put the energy - so pricing the counterfactual on the user's own smart
tariff credited it with a saving it could never have had, and understated the
system. It applies to no_pvbat only; every other scenario keeps the main
tariff. Both still share the main tariff's standing charge, so any difference
there is excluded from savings - stated in the results caveats and the docs
rather than left to be discovered.
The rate swap has to happen BEFORE the Prediction is constructed, because
Prediction snapshots the rates off predbat at construction time - built first,
it bills at the main tariff whatever is installed afterwards, so the swap looks
like it works and changes nothing. That is exactly what the first version did.
Sub-pages now highlight their parent menu entry. /annual_view and
/annual_compare matched no menu link, so the JS fell through to its default of
"first item" and lit up Dashboard while the user was plainly on WhatIf. The
second pass only runs when nothing matched exactly, so /apps_editor keeps its
own highlight rather than being captured by /apps.
Runs can be deleted from the Compare page, with confirmation. Deleting discards
the document and every captured plan through the same path eviction uses, so it
leaves no orphaned storage behind.
Price cap figures brought to the July 2026 cap: 26.11p/kWh and 57.19p/day, from
Ofgem, replacing 24.86p and a 60p default. Named constants now, so the next cap
change is a one-line edit. The 4.1p SEG export rate still sits inside the
typical 3-8p band for fixed export offers and is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
self.caveats.append("Months {} had too few forecast/actual day pairs, so their P10 used the flat {} derate.".format(sorted(self.weather.fallback_months), self.config["pv10_derate_fallback"]))
1424
1459
ifhas_solar:
1425
1460
self.caveats.append("The forecast-versus-ERA5 gap includes systematic model bias as well as forecast error, so measured solar uncertainty is slightly overstated.")
1461
+
self.caveats.append(
1462
+
"The no_pvbat counterfactual is priced on its own baseline tariff, since a household with no PV or battery would not be on a battery tariff. Both scenarios still use ONE standing charge - the main tariff's - so any difference in standing charge between the two tariffs is NOT included in the reported savings or payback."
1463
+
)
1426
1464
self.caveats.append("export_credit_p_estimate is money ALREADY included inside cost_p (which prices every export minute at its real rate); it is informational only - adding it to cost_p double-counts export income.")
1427
1465
self.caveats.append(
1428
1466
"The without_predbat baseline charges in the single cheapest contiguous band of each day, mirroring Predbat's own savings baseline. On a half-hourly tariff such as Agile the cheapest band is often one 30 minute slot, so the baseline is a more pessimistic comparator there than on a banded tariff (Economy 7, Cosy, Flux) where it covers the whole cheap period. Compare predbat_vs_baseline_p across tariffs with that in mind."
exceptExceptionasexc: # noqa: BLE001 - one bad sample must not abort the whole year
1483
1530
self.log("Warn: Annual: {} in month {} failed to plan/cost ({}: {}); excluding it from this month's total".format(day.isoformat(), month, type(exc).__name__, exc))
# Falling back to the main tariff keeps the month rather than losing it, but it
1587
+
# silently changes what no_pvbat means there, so it has to be said out loud.
1588
+
self.caveats.append(
1589
+
"No baseline-tariff rates were available for month(s) {}, so the no-PV/battery counterfactual there was priced on the main tariff instead, which understates what the system is worth in those months.".format(sorted(baseline_fallback_months))
Copy file name to clipboardExpand all lines: apps/predbat/tests/test_annual_integration.py
+24Lines changed: 24 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -329,6 +329,30 @@ def _run_car_config():
329
329
print(" ERROR: {}.{} = {} without capture but {} with capture - save is leaking into the billed numbers".format(key, field, without_capture[key][field], with_capture[key][field]))
330
330
failed=True
331
331
332
+
print("Test: the baseline tariff prices no_pvbat only and does not leak into the other scenarios")
333
+
# A flat price-cap-style baseline against the banded main tariff. Two properties
334
+
# matter and they pull in opposite directions: the counterfactual MUST change (or the
335
+
# feature does nothing), and every other scenario must be bit-for-bit identical (or
336
+
# the swap has leaked and silently repriced the system being evaluated). _apply_rates
337
+
# mutates predbat in place and replicates what it is given, so a leak here is a very
print(" ERROR: a flat baseline tariff should reprice no_pvbat away from the banded main tariff, got {} vs {}".format(without_baseline["no_pvbat"]["cost_p"], with_baseline["no_pvbat"]["cost_p"]))
print(" ERROR: {}.{} changed from {} to {} - the baseline tariff has leaked past no_pvbat".format(key, field, without_baseline[key][field], with_baseline[key][field]))
354
+
failed=True
355
+
332
356
print("Test: capturing plans does not leak predbat.debug_enable on")
333
357
# The annual "debug" flag means "save the plan info", nothing more - it must never be
334
358
# wired to Predbat's own debug_enable, which kernel_supported() requires False to use
0 commit comments