Skip to content

WhatIf: baseline tariff for the counterfactual, run deletion, nav fix, current cap rates - #4393

Merged
springfall2008 merged 4 commits into
mainfrom
feat/whatif-baseline-tariff
Jul 31, 2026
Merged

WhatIf: baseline tariff for the counterfactual, run deletion, nav fix, current cap rates#4393
springfall2008 merged 4 commits into
mainfrom
feat/whatif-baseline-tariff

Conversation

@springfall2008

Copy link
Copy Markdown
Owner

Four changes to the WhatIf tab.

A separate tariff for the no-PV/battery counterfactual

A household with no system is not on a battery tariff — the cheap overnight rates those offer only pay off once you have somewhere to put the energy. Pricing the counterfactual on the user's own smart tariff therefore credited it with a saving it could never have had, and understated what the system is worth.

The Configure page gains Tariff without PV or a battery, defaulting to the Ofgem price cap. It applies to no_pvbat only; every other scenario keeps the main tariff.

A subtlety that bit the first version: the rate swap must happen before the Prediction is constructed. Prediction snapshots the rates off predbat at construction (prediction.py:181), so a Prediction built first bills at the main tariff no matter what is installed afterwards — the swap appears to work and changes nothing. The integration test caught it by asserting the counterfactual actually moves.

Known simplification, stated rather than hidden: both tariffs share the main tariff's standing charge, so a difference there is excluded from savings and payback. Recorded in the results caveats and the docs.

Sub-pages no longer highlight Dashboard

/annual_view and /annual_compare matched no menu link, so the highlighting JS fell through to its "first item" default and lit up Dashboard while the user was plainly on WhatIf. A second pass maps a sub-page to its parent entry, and only runs when nothing matched exactly — so /apps_editor keeps its own highlight rather than being captured by /apps.

Delete runs from the Compare page

Each row gets a Delete button that confirms first. Deletion goes through the same discard path eviction uses, so the document and every captured plan go too, leaving no orphaned storage.

Price cap rates brought current

24.86p/kWh and a 60p standing charge were stale. Now 26.11p/kWh and 57.19p/day, the Ofgem cap for 1 July–30 September 2026, as named constants so the next 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.

Testing

Full suite green (297s), including the ~110s annual integration run.

The load-bearing test asserts both halves of the tariff swap: the counterfactual must change, and every other scenario must be bit-for-bit identical. Verified by mutation — removing the restore makes pv_only swing from −297p to −1.2p, which the test catches.

🤖 Generated with Claude Code

…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>
Copilot AI review requested due to automatic review settings July 30, 2026 17:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Not ready to approve

There are a couple of concrete UX/validation issues (baseline dropdown selection and baseline-tariff validation error paths) that can mislead users or produce incorrect/misleading behavior until fixed.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

This PR updates the Annual “WhatIf” web flow to more accurately price the no-PV/no-battery counterfactual using a separate baseline tariff (defaulting to the Ofgem price cap), adds run deletion from the compare table, fixes menu highlighting for WhatIf sub-pages, and refreshes built-in price-cap constants.

Changes:

  • Add baseline_tariff support (config, validation, UI selection, and engine execution) so no_pvbat is priced on a separate tariff while other scenarios remain unchanged.
  • Add /annual_delete route + storage-backed delete_run() and a Delete button on the compare table.
  • Improve navigation highlighting for sub-pages and update price-cap constants to current values.
File summaries
File Description
docs/annual-prediction.md Documents baseline tariff semantics and run deletion behavior.
apps/predbat/web.py Registers a new POST route for deleting annual runs.
apps/predbat/web_helper.py Adds a second-pass menu highlight strategy for sub-pages.
apps/predbat/web_annual.py Adds baseline tariff selection to the form and a Delete action/button in compare view.
apps/predbat/annual.py Implements baseline tariff application for no_pvbat and adds caveats/fallback logic.
apps/predbat/annual_store.py Adds delete_run() to remove a run document + plans + index entry.
apps/predbat/tariff_catalogue.py Centralizes updated price-cap/SEG constants and baseline default ID.
apps/predbat/tests/test_web_annual.py Adds UI/config validation checks for baseline tariff selection.
apps/predbat/tests/test_annual_store.py Adds coverage for deletion removing documents, plans, and index entries.
apps/predbat/tests/test_annual_integration.py Adds regression guard to ensure baseline reprices only no_pvbat.
Review details
  • Files reviewed: 10/10 changed files
  • Comments generated: 2
  • Review effort level: Low

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread apps/predbat/web_annual.py Outdated
Comment thread apps/predbat/annual.py Outdated
springfall2008 and others added 3 commits July 30, 2026 20:05
…ng no export

The tariff dropdown offered a fixed list of import/export pairs, so only the
combinations someone had thought to enumerate could be modelled. Import and
export are now separate dropdowns, and a household with no export agreement can
say so: "No export payment" prices export at 0p rather than leaving it unpriced.

Splitting the dropdowns exposed a latent engine bug that the pairs had been
hiding. rates_for gated BOTH sides on one `self.import_url or self.export_url`
test, so a tariff mixing a downloaded URL on one side with basic rates on the
other took the URL branch and found nothing to stamp for the basic side. No
built-in pair was mixed, so it never fired - but "price cap import with Octopus
Outgoing Prime export" is now an ordinary selection, and it would have priced
the entire year's import at zero. Each side resolves its own source now.

Matching a saved tariff back to the dropdown was also import-URL-only, which
made "Agile / Prime" and "Agile / Fixed" indistinguishable: a saved config
reloaded as whichever came first in the list. Matching each side against its own
list removes that by construction.

The no-PV/battery baseline keeps an import selector only - that scenario has
nothing to export, so an export control there would change no figure.

Tests: mixed URL/basic tariffs in both directions, asserting on values rather
than presence (verified by mutation - restoring the old gate fails both); a
deliberate zero export stamps every minute and raises no unpaid-export caveat;
free combinations round-trip through the form; two exports sharing an import stay
distinguishable. Full suite green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…pare table

The compare table had one Tariff column, fed by a URL-substring match on the
import side alone. Once import and export became independent choices and the
no-PV/battery scenario gained its own baseline, that column could no longer
describe a run: two runs differing only in their export deal looked identical,
and the baseline every saving is measured against was invisible everywhere.

Replace it with Baseline, Import and Export columns - baseline first, since it
is what the other two are judged against. Names come from the tariff catalogue,
including the user's own compare_list entries, via a new shared
tariff_catalogue.match_entry() that the form's dropdowns now use too, so the two
cannot describe the same tariff differently. build_summary carries the tariff
dicts rather than a pre-rendered name, because naming needs compare_list and
only the web layer can read it; backfill_summaries now also refreshes summaries
that predate the tariff fields. "What this run used" gains a Baseline row.

nowrap moves from the whole compare table onto its data cells. Applied to the
headers it held every column open at its heading set on one line - "PV +
battery pays back in" propping open a column reading "4.2 years" - which is
what made the table far wider than its figures needed.

Also: reset-to-defaults no longer selects the Octopus load source. That source
reads the import meter, which on a home with solar or a battery already has
that system's self-consumption subtracted from every reading - the form carries
a banner warning against it - yet anyone with credentials in apps.yaml is by
definition such a home, so the prefill picked the one source that was wrong for
them. The credentials still fill the fields; only the radio now defaults to
predicted consumption.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two issues raised on #4393.

Validation error paths. baseline_tariff goes through the same _validate_tariff
as the main tariff, whose four messages hard-coded "annual.tariff" - so every
baseline problem sent the user to fix a block that was perfectly valid. The
block being validated is now a parameter, and the baseline reports as
annual.baseline_tariff.

Baseline dropdown selection. A baseline written by hand in YAML matches no
catalogue entry, so _selected_import_id returns CUSTOM_ID - which is truthy,
so the "or default" fallback never fired, and since this dropdown deliberately
offers no Custom option, nothing was marked selected at all. The form relied on
the browser showing the first option to display anything. CUSTOM_ID now counts
as "no baseline" alongside None, so the price cap is stated rather than implied.

Not changed, and worth being explicit about: saving the form still replaces a
hand-written baseline with a catalogue one. That is not specific to the
baseline - config_from_post builds the config from scratch, so any key the form
does not render is dropped - and the catalogue-only baseline is deliberate.
Preserving hand-written config through the form is a separate question.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@springfall2008
springfall2008 merged commit d858a6c into main Jul 31, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants