Skip to content

feat(history): show EV charging energy and cost for 7 and 30 days - #1546

Draft
frahlg wants to merge 1 commit into
masterfrom
cursor/ev-energy-cost-1d35
Draft

frahlg wants to merge 1 commit into
masterfrom
cursor/ev-energy-cost-1d35

Conversation

@frahlg

@frahlg frahlg commented Oct 10, 2026

Copy link
Copy Markdown
Member

Problem and result

A household with a charger can see how much energy the car used, but not what that charging cost over a week or a month. This adds that, from history that already survives a restart.

Do not merge this until after the next stable release. It is self-contained (new read API, new card, no change to dispatch or pricing) and should wait so it does not ride into the release that is about to be cut. The changeset is minor because the History and Energy views gain a visible capability; that bump needs the owner's approval in this PR.

Last 7 days and last 30 days are rolling local days, including today, the same windows as GET /api/energy/daily?days=N. They are not a Monday–Sunday week and not calendar month-to-date. The existing history tiles use month-to-date for their Month toggle; this feature uses 30 days because "a month of charging" on the 3rd of the month would otherwise be three days.

History page with the EV charging card under the daily energy story

Same card after switching the daily chart to 30 days

Compact EV energy and cost under Week and month comparison

Scope and safety

Read-only. No new table and no change to what is sent to a charger. Energy is the persisted vehicle_charge ledger, one asset per charger, joined to a loadpoint when the asset label is that loadpoint's driver name. Prices are prices.total_ore_kwh for the configured zone: spot plus grid tariff, including VAT, the same import price savings and daily cost already use.

How cost is attributed. The ledger does not meter which source fed the car. In each interval that also has site import and household use, grid import is shared between home use and EV charging in proportion to their energy. Only the car's grid share is priced. Solar and home-battery energy in that interval has no further cash cost, because grid energy was priced when it crossed the meter. This path does not trace whether the battery was filled from solar or from an earlier import, and it does not charge the car for export it displaced.

An interval with charger energy but no site import, export, solar, or household use is priced entirely as grid import. Grid-attributed energy with no covering price slot still counts in energy_wh; its cost is omitted and cost_partial is true.

Stale or missing prices do not drop the energy. A restart reads the same ledger and price rows; covered by TestEVEnergySurvivesRestart.

Overlapping open PRs: #1337 also edits go/internal/api/api.go (one new route line here), and #1003 also edits web/index.html. Those branches have right of way if they land first; this should rebase rather than rewrite them. #1177 changes history-card polling; this card copies the hidden-tab pause in a new file and does not edit that PR.

API

GET /api/ev/energy (read tier)

{
  "tz": "Europe/Stockholm",
  "currency": "SEK",
  "window": "rolling_local_days",
  "attribution_note": "...",
  "windows": [
    {
      "id": "7d",
      "label": "Last 7 days",
      "attribution": "site_mix",
      "energy_wh": 8000,
      "grid_wh": 3200,
      "onsite_wh": 4800,
      "cost_ore": 640,
      "unpriced_wh": 0,
      "cost_partial": false,
      "chargers": [{ "asset_id": "...", "label": "easee", "loadpoint_id": "garage", "energy_wh": 6000, "cost_ore": 480, "cost_partial": false }],
      "daily": [{ "day": "2026-06-02", "energy_wh": 8000, "cost_ore": 640, "cost_partial": false, "chargers": [] }]
    },
    { "id": "30d" }
  ]
}

attribution is site_mix, import_price (no site meter in those intervals), or mixed. currency comes from the price config, defaulting to SEK. Cost is minor units (öre, cent), matching the rest of the API. daily is the chart series.

Where it shows

  • History: under the daily energy story. Both periods, per charger when there is more than one, and a 7-day or 30-day bar chart. A partial cost is labeled.
  • Energy → Week and month comparison: the same two totals, without the chart. The card stays hidden until some charging has been recorded.

Screenshots above are Chrome against the real page with a mocked /api/ev/energy, not a production site.

Verification

go test -count=1 ./internal/state/ -run 'TestAggregateEVCharging|TestLocalDaysUseCivilMidnightsAcrossDST|TestEVEnergySurvivesRestart'
go test -count=1 ./internal/api/ -run 'TestEVEnergyAPI'
node --test web/components/ev-energy-format.test.mjs web/energy-history-integration.test.mjs web/javascript-syntax.test.mjs

All of those passed. The state tests cover two loadpoints sharing one interval, a missing price hour, a price that covers only half a bucket, solar surplus at zero cash cost, the 23-hour and 25-hour Europe/Stockholm days (including both occurrences of the repeated 02:30), and a close/reopen of the database.

No hardware run. The ledger rows in tests are the same shape the charger path already writes.

Open questions

  • Should displaced export (solar the car used instead of selling) be a cost? It is not today. The number is cash paid, not opportunity cost.
  • Battery energy is treated as already paid when it was imported. A car charged from the battery shows no extra cost for that energy.
  • The grid/solar split is proportional in the interval. It is not a dedicated EV import meter.
  • Is the card wanted on both History and the Energy week/month disclosure, or only one of them?
  • Confirm the minor changeset, and merge only after the next stable release.

Checklist

  • The change follows VISION.md and one selected scope.
  • I checked overlapping PRs and coordinated shared files/contracts.
  • Relevant checks cover the changed behaviour and failure paths.
  • A human reviewed changed UI in a browser, or no UI changed.
  • A Changeset is included, or the change is exempt.
  • Every commit has a DCO sign-off.

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

Week and month totals come from the persisted charger ledger and the
site import price. Hours without a price still count as energy and
mark the cost partial.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Fredrik Ahlgren <fredrik@sourceful-labs.com>
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