API

Allt är statiska JSON-filer som genereras vid varje körning. Ingen nyckel, ingen rate limit, inga anrop mot en server som kan gå ner. Hämta dem med vad du vill — CORS är påslaget av GitHub Pages, och Home Assistant behöver det inte.

Status för senaste körningen

Uppdaterad –

Läser in status…

Endpoints

Bas-URL är <sajtens adress>/api/v1/.

SökvägInnehåll
status.jsonKörstatus per datakälla, degraded, nästa planerade körning
models.jsonInstallerade modeller: id, namn, beskrivning, om de ger kvantiler
zones.jsonAlla fyra elområden med aktuellt pris och länkar
accuracy.jsonTräffsäkerhet per elområde, modell och horisont
longterm.jsonMånadens snittpris 1–3 månader fram per elområde: alla modeller med intervall, terminsmarknadens pris, drivkrafter och backtest
bess.jsonBatterikalkyl & BESS: 6 kommersiella offerter, 15-åriga kassaflöden under 3 scenarier, 11-årigt historiskt backtest och elområdesstatistik (SE1–SE4)
zones/{SE1..SE4}/forecast.jsonTimserie från i går 00:00 till +7 dygn, alla modeller, drivkrafter
zones/{SE1..SE4}/history.json30 dygn utfall plus prognosen vi gav 24, 48 … 168 h innan
zones/{SE1..SE4}/accuracy.jsonTräffsäkerhet för ett elområde

Enheter och konventioner

  • Alla priser är i EUR/MWh, avrundade till tre decimaler. För öre/kWh: multiplicera med växelkursen EUR/SEK och dela med 10. Kursen följer med i fältet fx i varje fil. Att bara dela med 10 ger eurocent, inte öre.
  • Vilken modell sajten visar står i default_model. Välj modell därifrån i stället för att skriva in ett namn — standardmodellen byts när en annan mätt sig bättre.
  • Tidsstämplar är ISO-8601 med offset i Europe/Stockholm, och avser leveranstimmens början.
  • source: "official" betyder publicerat day-ahead-pris. "forecast" är vår modell. "demo" betyder att instansen kör utan ENTSO-E-nyckel och visar syntetiska siffror — använd dem inte till något.
  • resolution är PT60M i v1. Fältet finns för att kvartsvärden ska kunna läggas till senare utan att bryta klienter.
  • Fälten är stabila. Nya fält kan tillkomma; befintliga tas inte bort inom v1.

Uppdatering och cachning

  • Filerna skrivs om vid varje körning: fyra gånger per dygn, 06:30, 10:15, 13:30 och 18:00 svensk tid, plus när koden ändras. generated_at och run_id säger vilken körning en fil kommer från, och status.json → next_expected_update_utc när nästa är planerad. En körning kan starta upp till en halvtimme efter den tiden, ibland mer.
  • Morgondagens officiella priser publiceras av börsen runt 12:45. Den första körningen därefter, normalt 13:30, har dem.
  • GitHub Pages cachar filerna i upp till tio minuter. Att hämta oftare än var femtonde minut ger inget nytt; var trettionde räcker.

Stabilitet

  • Inom v1 tas inga fält bort och inget fält byter betydelse. Nya fält kan tillkomma — ignorera de du inte känner igen. En ändring som bryter klienter kommer som /api/v2/.
  • Nya modeller kan tillkomma och standardmodellen kan bytas. Läs alltid default_model i stället för att skriva in ett modell-id.

Saknade värden och fel

  • actual: null — inget officiellt pris ännu för timmen.
  • En modell kan sakna värde för vissa timmar. Kontrollera att nyckeln finns i models innan du läser den.
  • degraded: true — en väsentlig källa svarade inte, och delar av filen kan komma från en tidigare körning. Orsaken står i status.json → sources.<källa>.error.
  • demo: true — syntetiska priser, instansen saknar ENTSO-E-nyckel. Använd dem inte.
  • fx.stale: true — dagens ECB-kurs kunde inte hämtas och en äldre används.

Fältreferens

Tider är ISO-8601 med offset i Europe/Stockholm om inget annat står. Priser är EUR/MWh.

zones/{SE1..SE4}/forecast.json — prognosen

FältTypInnehåll
zone, zone_nametextElområde, t.ex. SE3 och Stockholm.
generated_at, run_idtid, textKörningen som skrev filen.
unit, resolutiontextEUR/MWh och PT60M (en punkt per timme).
degraded, demoboolSe Saknade värden och fel.
default_modeltextModellen sajten visar.
modelslistaAlla modell-id:n som kan förekomma i serien.
fxobjektpair, rate (SEK per EUR), date, source, fetched_at, stale.
series[]listaEn post per timme från i går 00:00 till sju dygn fram, cirka 200 timmar.
  tstidLeveranstimmens början.
  actualtal | nullOfficiellt pris, eller null om det inte är publicerat.
  sourcetextofficial, forecast eller demo.
  models.<id>objektp10, p50, p90. p50 är prognosen; åtta utfall av tio ska hamna mellan p10 och p90.
driversobjektFörklaringen på förstasidan:
  regime, regime_label_svtextLäget som kod och på svenska, t.ex. outage_tight / Bortfall i systemet.
  headline_sv, bullets_sv[]textRubrik och punkter, färdiga att visa.
  outages.items[]listaPågående och kommande avbrott: kind (production/transmission), unit, zone, from_area, to_area, fuel, nuclear, unavailable_mw, installed_mw, from, to, reason, messages (antal meddelanden bakom posten), local (false = kärnkraft i angränsande elområde).
  reservoirobjekt | nullVattenmagasin: ts, stored_mwh, fill_ratio (0–1), week_anomaly (mot normalt), change_4w.
  featuresobjektSnitt kommande 48 h: wind_index_local, wind_index_north, wind_index_south (1,00 = normal vind), temp_anomaly_<zon>_c, temp_local_c, solar_index_daytime (0–1), spread_proxy_se4_se2 (prognosens pris i SE4 minus SE2).

zones.json — alla elområden

Samma huvudfält som ovan (generated_at, run_id, unit, resolution, degraded, default_model, fx) och zones[] med zone, name, lat, lon, eic (ENTSO-E:s områdeskod), current och relativa sökvägar forecast_url, history_url, accuracy_url. current är timmen då filen skrevs: ts, eur_mwh, ore_kwh, source. Den blir inaktuell mellan körningarna, så en klient som vill ha timmen just nu bör läsa den ur series.

zones/{SE1..SE4}/history.json — vad vi sa då

days (30), model_id (modellen historiken gäller), lead_times_h ([24, 48, … 168]), default_lead_time_h och points[] med ts, actual och forecast — ett objekt från ledtid till p50, t.ex. {"24": 5.8, "48": 12.1}: prognosen som gavs ungefär så många timmar före leveranstimmen (±12 h). En ledtid saknas om ingen prognos gavs just då.

accuracy.json och zones/{…}/accuracy.json — träffsäkerhet

window_days är ett tak; scored_from och scored_to är de leveranstimmar som faktiskt mätts. buckets är avstånden (0-24h … 144-168h), reference_model referensen. Måtten ligger i zones.<zon>.<modell>.<avstånd> (per zon-fil: metrics.<modell>.<avstånd>) och overall för hela landet:

FältInnehåll
nAntal jämförda timmar.
maeMedelfel, EUR/MWh: snittet av avståndet mellan prognos och utfall.
rmseMedelfel som straffar stora missar hårdare.
biasSnittfel med tecken: positivt = för högt.
coverage80Andel utfall inom p10–p90 (0–1), bör ligga nära 0,8.
mapeMedelfel i procent av priset, bara timmar över 5 EUR/MWh. Kan vara null.
p50_corrKorrelation mellan prognos och utfall. Kan vara null.
enough_datafalse om n är under min_samples; visa då inte talen.
skill_vs_naiveAndel mindre fel än referensen på samma timmar: 0,054 = 5,4 % mindre fel. Negativt = sämre.

table är samma MAE kort: table.<ALL|zon>.<avstånd>.<modell>, null där underlaget är för litet.

longterm.json — långtidsprognosen

default_model, reference_model, models[] och zones.<zon> med recent_30d (snitt senaste 30 dagarna), year_mean, drivers_sv[] och months[], en post per kalendermånad:

FältInnehåll
month, label, horizon2026-11, november 2026, antal månader fram (1–3).
models.<id>.p50Prognos för månadens snittpris. Standardmodellen har även p10/p90.
models.lt_marketTerminsmarknaden: p50, tenor (month/quarter/year), delivery, contracts, system, epad, trade_date. Saknas om inget kontrakt täcker månaden.
last_yearSamma månad förra året, faktiskt snittpris.
nuclear_out_mwPlanerat kärnkraftsbortfall i snitt under månaden.
outlookSäsongsprognos för vädret: temp_anomaly_c, precip_anomaly_mm. Kan vara null.

backtest.zones.<zon|ALL>.<modell>.<horisont> har mae, bias och n; accuracy räknar skarpa prognoser när månaderna är slut.

status.json och models.json

status.json: ok, generated_at, run_id, degraded, demo, fx, next_expected_update_utc (UTC) och sources — en post per datakälla med ok, källspecifika räknare (rows, messages …) och error när något gick fel. models.json: default_model och models[] med id, name_sv, description_sv, quantiles, derived, is_default, is_reference.

Räcker det för en Home Assistant-integration?

Ja. Allt en integration behöver finns i zones/<zon>/forecast.json: aktuellt pris och prognos i series, vilken modell som gäller i default_model och växelkursen i fx. Hämta var trettionde minut, välj timmen ur series med din egen klocka, räkna öre med fx.rate, hantera actual: null och visa en varning när degraded är sant. Exemplen nedan gör detta med Home Assistants inbyggda rest- och template-integrationer, utan egen kod.

Exempel

curl -s https://hakannormark.github.io/power-price-oracle/api/v1/zones/SE3/forecast.json | jq '.series[0]'

{
  "ts": "2026-09-04T00:00:00+02:00",
  "actual": 54.1,
  "source": "official",
  "models": {
    "seasonal_naive": { "p10": 40.0, "p50": 52.0, "p90": 80.0 },
    "shrunk_scaled":  { "p10": 17.3, "p50": 55.0, "p90": 142.1 },
    "market_scaled":  { "p10": 21.6, "p50": 59.3, "p90": 146.4 }
  }
}

Förkortat: models har en post per modell i models.json. Bandet är brett och asymmetriskt med avsikt — se Metod.

# Aktuellt pris i alla fyra elområden, i öre/kWh
curl -s https://hakannormark.github.io/power-price-oracle/api/v1/zones.json \
  | jq -r '.zones[] | "\(.zone) \(.current.ore_kwh) öre/kWh (\(.current.source))"'

Home Assistant

Hämta hela serien som attribut på en sensor:

rest:
  - resource: https://hakannormark.github.io/power-price-oracle/api/v1/zones/SE3/forecast.json
    scan_interval: 1800
    sensor:
      - name: Spotprognos SE3
        unique_id: spotprognos_se3
        value_template: "{{ value_json.generated_at }}"
        device_class: timestamp
        json_attributes:
          - series
          - drivers
          - default_model
          - fx
          - unit

Och plocka ut den aktuella timmens pris ur serien med en template-sensor. Den tar det officiella priset när auktionen är klar och faller annars tillbaka på standardmodellens p50. Omräkningen till öre använder växelkursen i fx:

template:
  - sensor:
      - name: Spotpris SE3 nu
        unique_id: spotpris_se3_nu
        unit_of_measurement: "öre/kWh"
        state_class: measurement
        state: >
          {% set hour = now().replace(minute=0, second=0, microsecond=0).isoformat() %}
          {% set series = state_attr('sensor.spotprognos_se3', 'series') or [] %}
          {% set model = state_attr('sensor.spotprognos_se3', 'default_model') %}
          {% set fx = state_attr('sensor.spotprognos_se3', 'fx') %}
          {% set rate = fx.rate if fx else none %}
          {% set match = series | selectattr('ts', 'match', hour[:13]) | list | first %}
          {% if match is none or rate is none %}
            unknown
          {% elif match.actual is not none %}
            {{ (match.actual * rate / 10) | round(1) }}
          {% else %}
            {{ (match.models[model].p50 * rate / 10) | round(1) }}
          {% endif %}
        attributes:
          källa: >
            {% set hour = now().replace(minute=0, second=0, microsecond=0).isoformat() %}
            {% set series = state_attr('sensor.spotprognos_se3', 'series') or [] %}
            {% set match = series | selectattr('ts', 'match', hour[:13]) | list | first %}
            {{ match.source if match is not none else 'unknown' }}

Byt SE3 mot ditt elområde. Vill du styra automationer på prognosen: läs source först. Är den official är priset satt av börsen, är den forecast är det vår gissning och bör hanteras med bandet p10–p90 i åtanke.