1. Startsida
  2. Skript
  3. Aktuella elpriser

Skript

Aktuella elpriser

Hämta det aktuella svenska elpriset till enheten och visa det i appen.

Uppdaterad: november 2026

Så fungerar det

Enheten hämtar själv dagens kvartspriser via internet direkt från se.elpris.eu — det finns ingen server eller tjänst emellan som du behöver hålla igång. Enheten måste ha internetåtkomst, och om priset ska visas i appen skickar skriptet det vidare till ditt Shelly-konto.

Priset från se.elpris.eu är spotpriset för ditt elområde, utan påslag, avgifter och moms. Skriptet lägger själv på de påslag och avgifter du anger och därefter moms, så det värde som visas är ditt slutliga pris per kWh. Priserna hämtas en gång per dygn och priset uppdateras varje kvart.

Det här behöver du

  • En Shelly-enhet som stöder skript — Gen2 eller nyare
  • Minst en enhet med effektmätning (PM eller EM), om förbrukningen också ska mätas
  • Shelly Smart Control eller control.shelly.cloud. Det går att göra från både telefon och dator — en större skärm gör det enklare att arbeta med kod
  • Gen3 eller nyare, om priset ska visas som en enhet i appen — det kräver virtuella komponenter
1
Skript

Klistra in skriptet

Tryck på enheten, välj skriptikonen och tryck på Skapa nytt skript. Ge skriptet ett namn, klistra in koden nedan och tryck på Spara. Det går också att göra från enhetens eget webbgränssnitt.

Använder du en dator kan du med fördel låta redigeraren vara öppen och öppna nästa steg i en ny webbläsarflik.

Skriptlistan med knappen Skapa nytt skript och redigeraren där skriptet klistras in
Tryck för att förstora.
// =====================================================================
// Shelly script: hämtar dagens elpris (SE1–SE4) en gång per dygn och,
// på exakta klockslag xx:00:01, xx:15:01, xx:30:01, xx:45:01:
//   1) uppdaterar den virtuella komponenten number:200 lokalt (Number.Set)
//   2) postar priset till Shelly Cloud (om CLOUD_URL är satt)
//
// API:et levererar kvartspriser (96 poster/dygn) exkl. påslag, avgifter
// och moms. Skriptet lägger på rörliga påslag (exkl. moms) och därefter
// moms – värdet som skrivs/postas är alltså slutligt konsumentpris.
//
// Förutsättning: skapa en virtuell Number-komponent (id 200) på enheten.
// Kontrollera att dess min/max täcker prisintervallet (t.ex. 0–20),
// annars avvisas värdet av Number.Set.
// =====================================================================

// --- Konfiguration ---
let PRICE_API_BASE = "https://se.elpris.eu/api/v1/prices";
let PRICE_ZONE     = "AUTO";         // "AUTO" = räkna ut från enhetens lat/lon, annars SE1–SE4
let FALLBACK_ZONE  = "SE3";          // används om AUTO inte kan avgöra (position saknas/utanför Sverige)
let CLOUD_URL      = "DIN_API_URL";  // Lämna "" eller "DIN_API_URL" för att hoppa över molnpost
let HTTP_TIMEOUT_S = 20;

let NUMBER_ID      = 200;            // id för virtuell Number-komponent (number:200)
let DECIMALS       = 4;              // antal decimaler i det skrivna/postade priset
let UPDATE_ON_BOOT = true;           // skriv number:200 direkt vid uppstart (ingen molnpost)

// --- Prispåslag och moms (SEK/kWh exkl. moms) ---
// Exempel: 8,5 öre/kWh skrivs 0.085. Sätt 0 för poster som inte gäller.
let ELLEVERANTOR_PASLAG   = 0.0;  // Påslag från elleverantör
let ELCERTIFIKAT          = 0.0;  // Elcertifikat
let OVERFORINGSAVGIFT     = 0.0;  // Nätbolagets överföringsavgift
let ELSKATT               = 0.0;  // Energiskatt
let ELBEREDSKAPSAVGIFT    = 0.0;  // Elberedskapsavgift
let ELSAKERHETSAVGIFT     = 0.0;  // Elsäkerhetsavgift
let NATOVERVAKNINGSAVGIFT = 0.0;  // Nätövervakningsavgift

let VAT_RATE = 0.25;              // 25 % moms

// Förberäknas en gång: slutpris = (spot + påslag) * (1 + moms)
let ADDITIONS_EXVAT = ELLEVERANTOR_PASLAG + ELCERTIFIKAT + OVERFORINGSAVGIFT +
                      ELSKATT + ELBEREDSKAPSAVGIFT + ELSAKERHETSAVGIFT +
                      NATOVERVAKNINGSAVGIFT;
let VAT_FACTOR  = 1 + VAT_RATE;
let STEP        = 1 / Math.pow(10, DECIMALS);   // t.ex. 0.0001 för 4 decimaler
let CLOUD_ENABLED = CLOUD_URL !== "" && CLOUD_URL !== "DIN_API_URL";

// --- Cache och status ---
// API-svar: { t0, s: 900, u: "SEK", p: [pris0, pris1, ...] }
let cachedPrices  = null;
let cachedDateStr = null;
let cachedZone    = null;   // elområdet cachen gäller för
let isFetching = false;
let isPosting  = false;

// =====================================================================
// Hjälpfunktioner
// =====================================================================
function pad2(n) { return (n < 10 ? "0" : "") + n; }

// "YYYY-MM-DD" för angivet (eller nuvarande) datum
function dateStr(d) {
  d = d || new Date();
  return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate());
}

// =====================================================================
// Elområde från enhetens position (Sys-konfig: location.lat / location.lon)
// =====================================================================
// Gränserna mellan elområdena följer stamnätets snitt, inte läns- eller
// kommungränser. Nedan är de förenklade till tre brutna linjer [lon, lat]
// genom Sverige. Positionen jämförs med linjens latitud vid samma
// longitud. Träffsäkert för det mesta av landet, men nära en gräns kan
// det bli fel – sätt då PRICE_ZONE manuellt.
let BORDER_SE1_SE2 = [[11.0, 66.1], [16.5, 66.0], [17.6, 65.75], [18.8, 65.35], [20.5, 65.1], [24.5, 64.95]];
let BORDER_SE2_SE3 = [[11.0, 62.0], [14.0, 62.0], [15.0, 61.65], [16.0, 61.3], [16.6, 60.95], [17.5, 60.85], [24.5, 60.85]];
let BORDER_SE3_SE4 = [[11.0, 56.85], [12.9, 56.95], [13.6, 56.95], [14.4, 57.0], [15.5, 56.95], [16.3, 57.1], [24.5, 57.1]];

// Linjens latitud vid given longitud (linjär interpolation mellan punkterna)
function borderLat(line, lon) {
  if (lon <= line[0][0]) return line[0][1];
  for (let i = 1; i < line.length; i++) {
    if (lon <= line[i][0]) {
      let a = line[i - 1], b = line[i];
      return a[1] + (b[1] - a[1]) * (lon - a[0]) / (b[0] - a[0]);
    }
  }
  return line[line.length - 1][1];
}

function zoneFromLatLon(lat, lon) {
  // Grov kontroll att positionen ligger i Sverige
  if (lat < 55.0 || lat > 69.2 || lon < 10.9 || lon > 24.3) return null;
  if (lat >= borderLat(BORDER_SE1_SE2, lon)) return "SE1";
  if (lat >= borderLat(BORDER_SE2_SE3, lon)) return "SE2";
  if (lon >= 17.9 && lat >= 56.8) return "SE3";   // Gotland ligger i SE3 trots sin sydliga del
  if (lat >= borderLat(BORDER_SE3_SE4, lon)) return "SE3";
  return "SE4";
}

let resolvedZone = null;   // sätts av resolveZone()

function resolveZone() {
  if (PRICE_ZONE !== "AUTO") {
    resolvedZone = PRICE_ZONE;
    return resolvedZone;
  }
  let sys = Shelly.getComponentConfig("sys");
  let loc = sys && sys.location;
  let hasPos = loc && typeof loc.lat === "number" && typeof loc.lon === "number";
  let zone = hasPos ? zoneFromLatLon(loc.lat, loc.lon) : null;
  let src  = "position";
  if (!zone) {
    zone = FALLBACK_ZONE;
    src  = hasPos ? "reserv, positionen ligger utanför Sverige" : "reserv, ingen position inställd";
  }
  // Logga bara när området ändras, inte varje kvart
  if (zone !== resolvedZone) {
    print("Elområde: " + zone + " (" + src +
          (hasPos ? ": " + loc.lat + ", " + loc.lon : "") + ")");
  }
  resolvedZone = zone;
  return zone;
}

// https://se.elpris.eu/api/v1/prices/ÅÅÅÅ/MM-DD_SEx.json
function buildPriceUrl(d, zone) {
  return PRICE_API_BASE + "/" + d.getFullYear() + "/" +
         pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()) + "_" + zone + ".json";
}

// Slutpris som STRÄNG med exakt DECIMALS decimaler, t.ex. "1.2346".
// OBS: Math.round(v*10^n)/10^n räcker inte – skriptmotorn skriver ut
// flyttal med fler siffror (t.ex. 1.23460000000000003). Därför formateras
// värdet som text; molnposten skickar texten direkt.
function finalPriceStr(spot) {
  return ((spot + ADDITIONS_EXVAT) * VAT_FACTOR).toFixed(DECIMALS);
}

// =====================================================================
// Hämta dagens priser och cacha. callback(ok: boolean)
// =====================================================================
function fetchPrices(callback) {
  if (isFetching) {
    print("Fetch redan igång, hoppar över.");
    if (callback) callback(false);
    return;
  }
  isFetching = true;

  let now = new Date();
  let zone = resolveZone();
  let url = buildPriceUrl(now, zone);
  let day = dateStr(now);   // låses vid anropet, så cachen inte får fel datum runt midnatt
  print("Hämtar priser (" + zone + "): " + url);

  Shelly.call("HTTP.GET", { url: url, timeout: HTTP_TIMEOUT_S }, function (res, err, msg) {
    isFetching = false;
    let ok = false;

    if (err !== 0 || !res) {
      print("Hämtningsfel:", msg);
    } else if (res.code !== 200) {
      print("HTTP-fel vid hämtning:", res.code);
    } else {
      let parsed = null;
      try { parsed = JSON.parse(res.body); } catch (e) { print("JSON-parsefel:", e); }

      if (parsed && parsed.p && parsed.p.length > 0) {
        cachedPrices  = parsed;
        cachedDateStr = day;
        cachedZone    = zone;
        ok = true;
        print("Cachade " + parsed.p.length + " priser (" + (parsed.u || "?") +
              ", steg " + (parsed.s || "?") + "s) för " + day + ".");
      } else if (parsed) {
        print("Oväntat svarsformat (förväntade icke-tom .p-array).");
      }
    }
    res = null;  // frigör svarsbufferten direkt
    if (callback) callback(ok);
  });
}

// =====================================================================
// Pris för aktuell kvart: index = floor(sekunder sedan midnatt / steg)
// =====================================================================
function findCurrentPrice() {
  if (!cachedPrices || !cachedPrices.p) return null;
  let now = new Date();
  let sec = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds();
  let idx = Math.floor(sec / (cachedPrices.s || 900));
  if (idx < 0 || idx >= cachedPrices.p.length) return null;
  return cachedPrices.p[idx];
}

// =====================================================================
// Utdata: lokal Number-komponent och molnpost
// =====================================================================
// Number.Set kräver ett objekt med ett numeriskt värde (firmwaren avvisar
// JSON-text med fel -103). Värdet avrundas först via toFixed; antalet
// decimaler som visas i appen styrs av meta.ui.step, se syncNumberStep().
function setNumber(valueStr) {
  Shelly.call("Number.Set", { id: NUMBER_ID, value: parseFloat(valueStr) }, function (res, err, msg) {
    if (err !== 0) print("Number.Set (id " + NUMBER_ID + ") misslyckades:", err, msg);
    else print("number:" + NUMBER_ID + " = " + valueStr);
  });
}

// Sätter visningssteget (meta.ui.step) på number:200 så att appen och
// webbgränssnittet visar samma antal decimaler som DECIMALS. Körs vid uppstart.
function syncNumberStep() {
  Shelly.call("Number.GetConfig", { id: NUMBER_ID }, function (cfg, err, msg) {
    if (err !== 0 || !cfg) {
      print("Kunde inte läsa number:" + NUMBER_ID + " config:", err, msg);
      return;
    }
    let meta = cfg.meta || {};
    meta.ui = meta.ui || {};
    if (meta.ui.step === STEP) return;   // redan rätt
    meta.ui.step = STEP;
    Shelly.call("Number.SetConfig", { id: NUMBER_ID, config: { meta: meta } }, function (r, e, m) {
      if (e !== 0) print("Kunde inte sätta step på number:" + NUMBER_ID + ":", e, m);
      else print("number:" + NUMBER_ID + " visningssteg satt till " + STEP);
    });
  });
}

function postPrice(valueStr) {
  if (!CLOUD_ENABLED) return;
  if (isPosting) {
    print("Post redan igång, hoppar över.");
    return;
  }
  isPosting = true;

  let body = '{"price":"' + valueStr + '"}';
  Shelly.call("HTTP.POST", {
    url: CLOUD_URL,
    body: body,
    headers: { "Content-Type": "application/json" },
    timeout: HTTP_TIMEOUT_S
  }, function (res, err, msg) {
    isPosting = false;
    if (err === 0 && res && res.code >= 200 && res.code < 300) {
      print("POST OK: " + body);
    } else {
      print("POST misslyckades. err=", err, "msg=", msg, "code=", res ? res.code : "n/a");
    }
  });
}

// Räknar ut aktuellt slutpris och skickar det. post=false → bara lokalt.
function publishCurrentQuarter(post) {
  let spot = findCurrentPrice();
  if (typeof spot !== "number") {
    print("Hittade inget pris för aktuell kvart.");
    return;
  }
  let price = finalPriceStr(spot);
  print("Spot " + spot + " -> slutpris " + price);
  setNumber(price);
  if (post) postPrice(price);
}

// =====================================================================
// Tick: säkerställ dagens cache, publicera aktuell kvart
// =====================================================================
function tick() {
  // Hämta om: tom cache, ny dag, eller ändrad position/elområde
  if (!cachedPrices || cachedDateStr !== dateStr() || cachedZone !== resolveZone()) {
    fetchPrices(function (ok) { if (ok) publishCurrentQuarter(true); });
  } else {
    publishCurrentQuarter(true);
  }
}

// =====================================================================
// Schemaläggning på xx:00:01, xx:15:01, xx:30:01, xx:45:01
// =====================================================================
let QUARTER_MS = 15 * 60000;
let OFFSET_MS  = 1000;

function msUntilNextQuarterPlusOne() {
  let now = new Date();
  let inHour = now.getMinutes() * 60000 + now.getSeconds() * 1000 + now.getMilliseconds();
  // Nästa kvartsgräns + 1 s (räknas aritmetiskt i stället för loop över mål)
  let next = Math.floor((inHour - OFFSET_MS) / QUARTER_MS + 1) * QUARTER_MS + OFFSET_MS;
  return next - inHour;
}

function scheduleNextTick() {
  let wait = msUntilNextQuarterPlusOne();
  print("Nästa tick om " + wait + " ms");
  Timer.set(wait, false, function () {
    scheduleNextTick();   // schemalägg först så kedjan överlever ett fel i tick()
    tick();
  });
}

// =====================================================================
// Uppstart: vänta 10 s på nätverk, hämta priser, sätt ev. number:200
// direkt (utan molnpost) och starta schemat.
// =====================================================================
Timer.set(10 * 1000, false, function () {
  syncNumberStep();
  fetchPrices(function (ok) {
    if (ok && UPDATE_ON_BOOT) publishCurrentQuarter(false);
    scheduleNextTick();
  });
});
2
Plats

Ställ in enhetens plats

Gå i appen till Inställningar (kugghjulet) → Geografisk plats och tidszon → Välj från karta. Dra markören till närheten av adressen, tryck på Välj och sedan på Spara.

Skriptet läser platsen direkt från enheten — du behöver inte skriva in några koordinater i skriptet.

Utifrån platsen tar skriptet automatiskt reda på vilket elområde (SE1–SE4) du tillhör. Det räcker att markören ligger i området — bara om du bor nära gränsen mellan två elområden behöver den vara exakt.

I steg 6 kontrollerar du att skriptet har hittat rätt elområde.

Inställningar → Geografisk plats och tidszon med latitud, longitud och dialogen Välj från karta
Tryck för att förstora.
3
Token

Hitta din token och klistra in den

Gå till Energi → Eltariff och välj Live under Taxa. Scrolla ned till API URL, kopiera hela URL:en med kopieringsknappen och klistra in den i CLOUD_URL i stället för platshållaren DIN_API_URL. Citattecknen runt URL:en ska stå kvar.

Ska priset aldrig visas under Energi kan du låta platshållaren stå kvar eller sätta fältet till två citattecken utan något emellan, "".

Energi → Eltariff med Taxa Standard/Live och fältet API URL med kopieringsknapp
Tryck för att förstora.
4
Inställningar

Anpassa övriga fält

CLOUD_URL är på plats efter steg 3. Resten av konfigurationen står högst upp i skriptet:

  • PRICE_ZONE — Låt stå på "AUTO" så väljer skriptet elområde utifrån enhetens plats. Hittar det fel område kan du ange det själv: "SE1", "SE2", "SE3" eller "SE4". Är du osäker på vilket område du tillhör, besök elomraden.se
  • FALLBACK_ZONE — Elområdet som används om enheten saknar plats eller platsen ligger utanför Sverige.
  • ELLEVERANTOR_PASLAG, ELCERTIFIKAT, OVERFORINGSAVGIFT, ELSKATT och myndighetsavgifterna — Ange dina rörliga påslag och avgifter från el- och nätfakturan, i SEK/kWh exklusive moms. 8,5 öre/kWh skrivs 0.085. Sätt 0 för det som inte gäller dig.
  • VAT_RATE — Momssats, 0.25 = 25 %.
  • NUMBER_ID och DECIMALS — Används bara om priset ska visas som en enhet i appen, se Extra nedan.

Tryck på Spara när fälten är på plats.

5
Start

Starta skriptet

Slå på autostart och tryck på play. En blå ring runt play-knappen betyder att skriptet körs.

Skriptlistan med Kör vid start påslaget och play-knappen aktiv
Tryck för att förstora.
6
Kontroll

Kontrollera att det fungerar

Priset visas under Energi → Eltariff som Nuvarande taxa när skriptet har skickat det första gången. Det sker vid nästa hela kvart, alltså inom 15 minuter. Uppdatera vyn om det inte har dykt upp än.

Energi → Eltariff med Taxa Live och Nuvarande taxa
Tryck för att förstora.

Öppnar du skriptet från enhetens eget webbgränssnitt visar konsolen vilket elområde skriptet har valt och vilken plats det bygger på, spotpriset just nu, slutpriset inklusive påslag och moms samt när nästa uppdatering sker. Stämmer inte elområdet kan du ange det själv i PRICE_ZONE i skriptet.

Skriptets konsol med valt elområde, spotpris, slutpris och nästa uppdatering
Tryck för att förstora.

Extra: visa priset som en enhet i appen

Det kräver en Gen3-enhet eller nyare. Skapa en virtuell komponent av typen nummer på enheten. När den har skapats står id:t i namnet, till exempel number:200 — skriv just det talet i NUMBER_ID. Det är inte alltid 200: har enheten redan andra komponenter, till exempel från ett annat skript, får den nya ett annat id.

Skriptet ställer själv in hur många decimaler som visas (DECIMALS), så Steg-fältet behöver du inte ändra.

Sätt Minsta värde till ett negativt tal, till exempel -5. Spotpriset kan bli negativt, och då avvisar komponenten värdet om minsta värde är 0.

Virtuella komponenter → Komponenter med knappen Skapa virtuell komponent
Tryck för att förstora.
Dialogen Skapa virtuell komponent med typen nummer vald
Tryck för att förstora.
Inställningar för number:200 med namn Elpris och enhet kr
Tryck för att förstora.

Skapa sedan en grupp, markera komponenten och slå på Extrahera virtuell grupp som enhet.

Virtuella komponenter → Grupper med knappen Skapa grupp
Tryck för att förstora.
Dialogen Skapa virtuell grupp med Elpris markerad och Extrahera virtuell grupp som enhet påslaget
Tryck för att förstora.

Priset visas nu som en egen enhet bredvid den enhet det hör till.

Rumsvyn med den virtuella enheten Elpris bredvid enheten den hör till
Tryck för att förstora.

Priset kan också användas som villkor i scener och åtgärder i appen — till exempel för att slå på en varmvattenberedare när priset är under en gräns som du själv anger.

Fungerar priset inte i appen eller på en Wall Display, öppna skriptets konsol via enhetens IP-adress och leta efter ett fel på Number.Set. Den vanligaste orsaken är att komponenten inte finns eller att id:t i NUMBER_ID inte stämmer.

Tips

  • Låt skriptet sköta tidpunkterna — det hämtar priserna en gång per dygn och uppdaterar varje kvart, så du behöver inte schemalägga något själv.
  • Håll ett öga på konsolen det första dygnet, särskilt vid midnatt när nästa dags priser hämtas.