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.
// =====================================================================
// 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();
});
});









