Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .changeset/european-price-zones.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
"ftw": minor
---

Prices: pick any of the 46 European bidding zones the price API publishes,
and be billed in the currency of the country you picked.

The zone picker offered twelve Nordic codes typed into two `<select>`
elements and the Go side knew twelve EIC codes, so a household in Belgium,
the Netherlands or Spain could not choose its own zone even though the
Sourceful API has served every ENTSO-E area all along. Both lists now come
from one table, `go/internal/prices/zones.go`, generated from that API's
`/areas` endpoint and served to the UI at `GET /api/prices/zones`. The
picker asks for a country first and a zone second, because everyone knows
they live in Italy and nobody knows their area code is `IT-CENTRE-NORTH`.

Currency stops being Swedish by assumption. It defaults to the currency of
the chosen zone, and the price API — which converts to EUR and SEK and
quietly answers anything else with EUR — is only asked for those two;
every other currency is converted here from EUR with the ECB rates the
service already caches. Where no rate is available the fetch fails rather
than storing a number that is wrong by an exchange rate, which is a number
the planner would spend money on. The old 11.5 SEK/EUR fallback is gone for
the same reason.

Two related faults fixed on the way. The direct ENTSO-E provider assumed
every day-ahead document is priced in EUR; Poland and Hungary among others
publish in their own currency, so it now reads `currency_Unit.name` and
converts from that. Its EIC code for Germany was the country code rather
than the DE-LU bidding zone, and NO5 was missing outright.

Prices are stored as minor units per kWh with no currency attached, so
changing the currency clears the price cache — otherwise cost history would
add öre to cent. The next fetch refills today and tomorrow. Every price
label in the UI now follows the configured currency: öre, cent, øre, grosz,
Rappen, or the major unit where the minor one is out of circulation
(4.30 Kč/kWh, not 430 haléř).

Existing installs are untouched: no zone means SE3, no currency means SEK,
and a Swedish install still asks the API for SEK exactly as before.
29 changes: 26 additions & 3 deletions go/internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ func (s *Server) routes() {
s.handle("GET /api/energy/history.csv", s.handleEnergyHistoryCSV)
s.handle("GET /api/savings/daily", s.handleSavingsDaily)
s.handle("GET /api/prices", s.handlePrices)
s.handle("GET /api/prices/zones", s.handlePriceZones)
s.handle("GET /api/forecast", s.handleForecast)
s.handle("GET /api/mpc/plan", s.handleMPCPlan)
s.handle("POST /api/mpc/replan", s.handleMPCReplan)
Expand Down Expand Up @@ -1989,12 +1990,34 @@ func (s *Server) handlePrices(w http.ResponseWriter, r *http.Request) {
return
}
writeJSON(w, 200, map[string]any{
"zone": s.deps.Prices.Zone,
"items": rows,
"enabled": true,
"zone": s.deps.Prices.Zone,
"currency": s.deps.Prices.Currency,
"items": rows,
"enabled": true,
})
}

// ---- /api/prices/zones ----
//
// The bidding zones a household can pick, with the currency each one is
// billed in. Served from the same table the fetchers use, so the picker
// can't offer a zone the providers don't know.
//
// Response: {"zones": [{code, country, currency, name}]}
func (s *Server) handlePriceZones(w http.ResponseWriter, _ *http.Request) {
all := prices.Zones()
items := make([]map[string]string, 0, len(all))
for _, z := range all {
items = append(items, map[string]string{
"code": z.Code,
"country": z.Country,
"currency": z.Currency,
"name": z.Name(),
})
}
writeJSON(w, 200, map[string]any{"zones": items})
}

// ---- /api/forecast ----
//
// Query params: range=24h|48h|3d (default 48h lookahead).
Expand Down
Loading