Test Scripts

Elpriser: Aktuel pris, X billigste timer, Billigste sammenhængende blok

Forbehold:
– Min. Gen3, eller Pro enhed.

– Er teste med Firmware 1.6.2.
– Virker ikke med Firmwre: 1.7 beta

Script 1: Henter el priser hvert døgn og gemmer dem i KVS (Key value storage) på Shelly enheden.

Det eneste der skal ændres er tallene i Længdegrad (lat) og Breddegrad (long), så de passer til din lokation.

// ===== CONFIGURATION =====
let CONFIG = {
  lat: 55.120019,
  long: 12.054538,
  run_seconds_after_midnight: 5 // How many seconds after 00:00 the script should run
};
// ==========================
 
function isDSTInEffect() {
  try {
    let now = new Date();
    let year = now.getFullYear();
    let march31 = new Date(year + "-03-31T00:00:00Z");
    let lastSundayMarch = new Date(march31.getTime() - march31.getDay() * 24 * 3600 * 1000);
    let oct31 = new Date(year + "-10-31T00:00:00Z");
    let lastSundayOctober = new Date(oct31.getTime() - oct31.getDay() * 24 * 3600 * 1000);
    return now >= lastSundayMarch && now < lastSundayOctober;
  } catch (e) {
    print("[FEJL] isDSTInEffect: " + e.message);
    return false;
  }
}
 
let LOCAL_OFFSET_HOURS = isDSTInEffect() ? 2 : 1;
print("[ELPRIS] Dansk sommertid aktiv? " + (LOCAL_OFFSET_HOURS === 2 ? "JA (+2)" : "NEJ (+1)"));
 
let supplierId = null;
let priceArea = null;
let nextRunTimer = null;
let logCountdownTimer = null;
let msToNextRun = 0;
 
function toISOStringNoMs(date) {
  return date.toISOString().split(".")[0] + "Z";
}
 
function sortByHour(array) {
  try {
    for (let i = 0; i < array.length - 1; i++) {
      for (let j = i + 1; j < array.length; j++) {
        if (parseInt(array[i].hour) > parseInt(array[j].hour)) {
          let temp = array[i];
          array[i] = array[j];
          array[j] = temp;
        }
      }
    }
  } catch (e) {
    print("[FEJL] sortByHour: " + e.message);
  }
}
 
function buildUrl() {
  try {
    let now = new Date();
    let localMidnight = new Date(now.getFullYear(), now.getMonth(), now.getDate());
    let fromDate = new Date(localMidnight.getTime() - 2 * 3600 * 1000); // 22:00 yesterday
    let toDate = new Date(localMidnight.getTime() + 23 * 3600 * 1000); // 21:00 today
    let fromStr = fromDate.toISOString().substring(0, 19) + "Z";
    let toStr = toDate.toISOString().substring(0, 13) + ":45:00Z";
 
    let url = "https://stromligning.dk/api/prices?from=" + fromStr +
              "&to=" + toStr +
              "&supplierId=" + supplierId +
              "&priceArea=" + priceArea +
              "&aggregation=1h" +  
              "&lean=true";
 
    print("[ELPRIS] Genereret URL: " + url);
    return url;
  } catch (e) {
    print("[FEJL] buildUrl: " + e.message);
    return "";
  }
}
 
function formatMs(ms) {
  let sec = Math.floor(ms / 1000);
  let h = Math.floor(sec / 3600);
  let m = Math.floor((sec % 3600) / 60);
  let s = sec % 60;
  return h + "t " + m + "m " + s + "s";
}
 
function scheduleNextRun() {
  try {
    if (nextRunTimer) Timer.clear(nextRunTimer);
    if (logCountdownTimer) Timer.clear(logCountdownTimer);
 
    let now = new Date();
    let msSinceMidnight = now.getHours() * 3600000 +
                          now.getMinutes() * 60000 +
                          now.getSeconds() * 1000 +
                          now.getMilliseconds();
    let msToMidnight = 86400000 - msSinceMidnight;
    let msDelay = msToMidnight + CONFIG.run_seconds_after_midnight * 1000;
    msToNextRun = msDelay;
 
    print("[ELPRIS] Næste opdatering planlagt om " + formatMs(msToNextRun));
 
    nextRunTimer = Timer.set(msDelay, false, function () {
      fetchSupplierAndThenLoadPrices();
      scheduleNextRun();
    });
 
    logCountdownTimer = Timer.set(5 * 60 * 1000, true, function () {
      msToNextRun -= 5 * 60 * 1000;
      if (msToNextRun <= 0) {
        Timer.clear(logCountdownTimer);
      } else {
        print("[ELPRIS] Næste opdatering om " + formatMs(msToNextRun));
      }
    });
  } catch (e) {
    print("[FEJL] scheduleNextRun: " + e.message);
  }
}
 
function fetchSupplierAndThenLoadPrices() {
  try {
    let supplierUrl = "https://stromligning.dk/api/suppliers/find?lat=" + CONFIG.lat + "&long=" + CONFIG.long;
    print("[ELPRIS] Henter supplier fra: " + supplierUrl);
 
    Shelly.call("HTTP.GET", { url: supplierUrl }, function (res, errCode, errMsg) {
      try {
        if (errCode !== 0) {
          print("[ELPRIS] Fejl ved supplier-API: " + errMsg);
          return;
        }
 
        let suppliers = JSON.parse(res.body);
        if (!suppliers || suppliers.length === 0) {
          print("[ELPRIS] Ingen supplier-data modtaget.");
          return;
        }
 
        supplierId = suppliers[0].id;
        priceArea = suppliers[0].priceArea;
        print("[ELPRIS] Fundet netselskab: " + supplierId + " (" + priceArea + ")");
 
        loadPrices();
      } catch (e) {
        print("[FEJL] parse supplier response: " + e.message);
      }
    });
  } catch (e) {
    print("[FEJL] fetchSupplierAndThenLoadPrices: " + e.message);
  }
}
 
function loadPrices() {
  try {
    let url = buildUrl();
    if (!url) return;
 
    Shelly.call("HTTP.GET", { url: url }, function(result, error_code, error_msg) {
      try {
        if (error_code !== 0) {
          print("Fejl ved HTTP GET: " + error_msg);
          return;
        }
 
        let data = JSON.parse(result.body);
        print("Antal datapunkter modtaget: " + data.length);
 
        let now = new Date();
        let localNow = new Date(now.getTime() + LOCAL_OFFSET_HOURS * 3600 * 1000);
        let todayLocalStr = localNow.toISOString().substring(0, 10);
 
        let todayPrices = [];
 
        for (let i = 0; i < data.length; i++) {
          let utc = new Date(data[i].date);
          let hourUTC = parseInt(data[i].date.substring(11, 13));
          let hourLocal = (hourUTC + LOCAL_OFFSET_HOURS) % 24;
          let localDate = new Date(utc.getTime() + LOCAL_OFFSET_HOURS * 3600 * 1000);
          let localDateStr = localDate.toISOString().substring(0, 10);
 
          if (localDateStr === todayLocalStr) {
            todayPrices.push({
              hour: ("0" + hourLocal).slice(-2),
              price: parseFloat(data[i].price.toFixed(3))
            });
          }
        }
 
        if (todayPrices.length === 0) {
          print("[ELPRIS] Ingen priser fundet for i dag (" + todayLocalStr + ")");
          return;
        }
 
        sortByHour(todayPrices);
 
        print("[ELPRIS] Priser for i dag (" + todayLocalStr + ") i lokal tid:");
 
        let index = 0;
 
        function showNextLine() {
          if (index >= todayPrices.length) {
            savePricesToKVS(todayPrices);
            return;
          }
          let p = todayPrices[index];
          print("Kl. " + p.hour + ":00 → " + p.price.toFixed(3) + " kr/kWh");
          index++;
          Timer.set(200, false, showNextLine);
        }
 
        showNextLine();
      } catch (e) {
        print("[FEJL] loadPrices: " + e.message);
      }
    });
  } catch (e) {
    print("[FEJL] loadPrices outer: " + e.message);
  }
}
 
function savePricesToKVS(prices) {
  let i = 0;
  function writeNext() {
    if (i >= prices.length) {
      print("[KVS] Alle priser gemt i KVS.");
      return;
    }
    let entry = prices[i];
    let key = "price_" + entry.hour;
    let value = entry.price.toFixed(3);
 
    try {
      Shelly.call("KVS.Set", { key: key, value: value }, function(res, err) {
        if (err) {
          print("[KVS] Fejl ved skrivning af " + key + ": " + JSON.stringify(err));
        } else {
          print("[KVS] Gemte " + key + " = " + value);
        }
        i++;
        Timer.set(100, false, writeNext);
      });
    } catch (e) {
      print("[FEJL] savePricesToKVS: " + e.message);
      i++;
      Timer.set(100, false, writeNext);
    }
  }
  writeNext();
}
 
// 🚀 Start!
try {
  fetchSupplierAndThenLoadPrices();
  scheduleNextRun();
} catch (e) {
  print("[FEJL] Hovedkørsel: " + e.message);
}

Script 2: Henter priser fra KVS og kan styre relæ i 3 forskellige modes

Her er der lidt flere settings der skal ændres på, men de forklare meget godt sig selv 🙂

– mode: 1, // Vælg funktion: 1 = aktuel pris, 2 = billigste timer, 3 = billigste sammenhængende blok
– price_threshold: 1.20, // Bruges i mode 1 – maks pris for at tænde
– cheapest_hours: 13, // Bruges i mode 2 og 3 – antal timer/blok
– relay_id: 0, // ID på relæ der skal styres (brug fx 99 hvis intet relæ skal styres)
– invert: false, // true = vend logikken (TÆND bliver SLUK)
– run_seconds_after_hour: 10, // Forsinkelse i sek. efter timeskift
– debug: false // true = vis alle priser ved opstart

// =====================================================
// ⚡ SIMPEL GUIDE TIL ELPRIS-SCRIPTET
// =====================================================
//
// Dette script tænder/slukker et relæ baseret på elpriser.
// Det opdateres hver time og bruger én af tre funktioner (modes).
//
// ➕ VIGTIGT: Scriptet henter priser fra KVS!
//            Det kræver derfor, at et andet script
//            allerede har gemt priserne i KVS som:
//              "price_00", "price_01", ..., "price_23"
//
// -----------------------------------------------------
// 🔧 KONFIGURATION (øverst i scriptet)
//
// mode: 1,  // Vælg funktion: 1 = aktuel pris, 2 = billigste timer, 3 = billigste sammenhængende blok
// price_threshold: 1.20,    // Bruges i mode 1 – maks pris for at tænde
// cheapest_hours: 13,       // Bruges i mode 2 og 3 – antal timer/blok
// relay_id: 0,              // ID på relæ der skal styres (brug fx 99 hvis intet relæ skal styres)
// invert: false,            // true = vend logikken (TÆND bliver SLUK)
// run_seconds_after_hour: 10, // Forsinkelse i sek. efter timeskift
// debug: false              // true = vis alle priser ved opstart
//
// -----------------------------------------------------
// 🔁 MODES
//
// ▶️ MODE 1 – AKTUEL PRIS
// - Tænder hvis prisen er under price_threshold
//
// ▶️ MODE 2 – X BILLIGSTE TIMER (SPREDT)
// - Tænder i de billigste timer (ikke nødvendigvis i træk)
//
// ▶️ MODE 3 – X BILLIGSTE BLOK (SAMMENHÆNGENDE)
// - Tænder i den billigste sammenhængende periode
//
// -----------------------------------------------------
// 📊 VIRTUELLE KOMPONENTER
//
// Boolean 200:
// - Mode 1: Sand hvis pris < threshold
// - Mode 2: Sand hvis aktuel time er blandt de billigste
// - Mode 3: Sand hvis aktuel time er i billigste blok
//
// Number 200:
// - Viser aktuel pris (afrundet til 2 decimaler)
//   → Bruges i scener: fx "hvis pris < 1.50 → sluk tørretumbler"
//
// Number 201:
// - Viser placering af aktuel time (1 = billigst)
//   → Bruges i scener: fx "hvis værdi < 6 → tænd enhed"
//
// Number 202:
// - Mode 3: Viser gennemsnitspris for billigste blok
//
// Text 200:
// - Mode 2: Viser billigste timer (fx "01, 05, 14")
// - Mode 3: Viser timer i billigste blok (fx "04, 05, 06, ...")
//
// =====================================================
 
 
 
// ====== KONFIGURATION ======
let CONFIG = {
  mode: 2,                  // 1 = aktuel pris, 2 = X billigste timer, 3 = X billigste sammenhængende timer
  price_threshold: 1.20,    // Bruges i mode 1
  cheapest_hours: 13,       // Bruges i mode 2 og 3
  relay_id: 0,              // ID på relæ der skal styres
  invert: false,            // Inverter logik i ALLE modes
  run_seconds_after_hour: 10, // Hvor mange sekunder efter hver hele time checket skal køre
  debug: false              // Vis KVS-priser ved opstart
};
 
function pad(n) {
  return (n < 10 ? "0" + n : n);
}
 
function sortByHour(prices) {
  for (let i = 0; i < prices.length - 1; i++) {
    for (let j = 0; j < prices.length - i - 1; j++) {
      if (prices[j].hour > prices[j + 1].hour) {
        let temp = prices[j];
        prices[j] = prices[j + 1];
        prices[j + 1] = temp;
      }
    }
  }
}
 
function sortPricesAscending(prices) {
  for (let i = 0; i < prices.length - 1; i++) {
    for (let j = 0; j < prices.length - i - 1; j++) {
      if (prices[j].price > prices[j + 1].price) {
        let temp = prices[j];
        prices[j] = prices[j + 1];
        prices[j + 1] = temp;
      }
    }
  }
}
 
function readAllPricesFromKVS() {
  if (!CONFIG.debug) return;
  let hour = 0;
  function readNext() {
    if (hour >= 24) return;
    let key = "price_" + pad(hour);
    Shelly.call("KVS.Get", { key: key }, function (result, error) {
      if (!error && result && result.value !== undefined) {
        print(key + " =", result.value);
      }
    });
    hour++;
    Timer.set(200, false, readNext);
  }
  readNext();
}
 
function fetchPricesAndRunMode(callback) {
  let prices = [];
  let hour = 0;
  function readNext() {
    if (hour >= 24) {
      if (prices.length !== 24) {
        print("❌ FEJL: Forventede 24 priser, men fik", prices.length, ". Afbryder.");
        return;
      }
      sortByHour(prices);
      print("Laver udregning...");
      Timer.set(100, false, function () {
        try {
          callback(prices);
        } catch (e) {
          print("Fejl i callback:", e.message);
        }
      });
      return;
    }
    let key = "price_" + pad(hour);
    Shelly.call("KVS.Get", { key: key }, function (result, error) {
      if (!error && result && result.value !== undefined) {
        prices.push({ hour: hour, price: parseFloat(result.value) });
      }
      hour++;
      Timer.set(200, false, readNext);
    });
  }
  readNext();
}
 
function checkCurrentPriceMode1() {
  let hour = new Date().getHours();
  let key = "price_" + pad(hour);
  Shelly.call("KVS.Get", { key: key }, function (result, error) {
    if (error || !result || result.value === undefined) {
      print("Fejl ved hentning af pris for", key);
      return;
    }
    let price = parseFloat(result.value);
    let roundedPrice = Math.round(price * 100) / 100;
    print("Aktuel pris kl", pad(hour) + ":00 =", roundedPrice, "kr");
    let shouldTurnOn = !CONFIG.invert ? price < CONFIG.price_threshold : price >= CONFIG.price_threshold;
    print("Logik (mode 1, invert=" + CONFIG.invert + "): " + (shouldTurnOn ? "TÆND" : "SLUK"));
    Shelly.call("Switch.Set", { id: CONFIG.relay_id, on: shouldTurnOn });
    Shelly.call("Number.Set", { id: 200, value: roundedPrice });
 
    Shelly.call("Boolean.Set", { id: 200, value: shouldTurnOn });
 
    updatePriceRanking();
  });
}
function checkCheapestHoursMode2(prices) {
  sortPricesAscending(prices);
  let selected = [];
  for (let i = 0; i < CONFIG.cheapest_hours && i < prices.length; i++) {
    selected.push(prices[i]);
  }
  sortByHour(selected);
 
  let currentHour = new Date().getHours();
  let match = false;
  for (let i = 0; i < selected.length; i++) {
    if (selected[i].hour === currentHour) {
      match = true;
      break;
    }
  }
 
  let shouldTurnOn = CONFIG.invert ? !match : match;
  let hourList = selected.map(function (item) {
    return item ? pad(item.hour) : "?";
  }).join(", ");
 
  let currentPrice = null;
  for (let i = 0; i < prices.length; i++) {
    if (prices[i].hour === currentHour) {
      currentPrice = prices[i].price;
      break;
    }
  }
 
  print("Billigste timer:", hourList);
  print("Aktuel time:", pad(currentHour), "→", shouldTurnOn ? "TÆND" : "SLUK");
  if (currentPrice !== null) {
    let roundedPrice = Math.round(currentPrice * 100) / 100;
    print("Aktuel pris:", roundedPrice, "kr");
}
 
 
  Shelly.call("Switch.Set", { id: CONFIG.relay_id, on: shouldTurnOn });
  Timer.set(100, false, function () {
    Shelly.call("Text.Set", { id: 200, value: hourList });
    Timer.set(100, false, function () {
 
        Shelly.call("Boolean.Set", { id: 200, value: shouldTurnOn });
        Timer.set(100, false, function () {
        if (currentPrice !== null) {
          let roundedPrice = Math.round(currentPrice * 100) / 100;
          Shelly.call("Number.Set", { id: 200, value: roundedPrice });
        }
 
          Timer.set(100, false, updatePriceRanking);
        });
      });
 
  });
}
 
function checkCheapestSequenceMode3(prices) {
  let minSum = 99999;
  let startIndex = 0;
  for (let i = 0; i <= 24 - CONFIG.cheapest_hours; i++) {
    let sum = 0;
    for (let j = 0; j < CONFIG.cheapest_hours; j++) {
      sum += prices[i + j].price;
    }
    if (sum < minSum) {
      minSum = sum;
      startIndex = i;
    }
  }
 
  let selected = [];
  for (let i = 0; i < CONFIG.cheapest_hours; i++) {
    selected.push(prices[startIndex + i]);
  }
 
  let currentHour = new Date().getHours();
  let match = false;
  let totalPrice = 0;
  let hourList = "";
  for (let i = 0; i < selected.length; i++) {
    let h = selected[i].hour;
    hourList += pad(h) + (i < selected.length - 1 ? ", " : "");
    totalPrice += selected[i].price;
    if (h === currentHour) match = true;
  }
 
  let averagePrice = Math.round(totalPrice * 100 / CONFIG.cheapest_hours) / 100;
  let shouldTurnOn = CONFIG.invert ? !match : match;
 
  let currentPrice = null;
  for (let i = 0; i < prices.length; i++) {
    if (prices[i].hour === currentHour) {
      currentPrice = prices[i].price;
      break;
    }
  }
 
  print("Sammenhængende billigste blok:", hourList);
  print("Gennemsnitlig pris for blokken:", averagePrice, "kr");
  print("Aktuel time:", pad(currentHour), "→", shouldTurnOn ? "TÆND" : "SLUK");
  if (currentPrice !== null) {
    let roundedPrice = Math.round(currentPrice * 100) / 100;
    print("Aktuel pris:", roundedPrice, "kr");
}
 
 
  Shelly.call("Switch.Set", { id: CONFIG.relay_id, on: shouldTurnOn });
  Timer.set(100, false, function () {
    Shelly.call("Text.Set", { id: 200, value: hourList });
    Timer.set(100, false, function () {
      Shelly.call("Number.Set", { id: 202, value: averagePrice });
      Timer.set(100, false, function () {
        Shelly.call("Boolean.Set", { id: 200, value: shouldTurnOn });
        Timer.set(100, false, function () {
        if (currentPrice !== null) {
          let roundedPrice = Math.round(currentPrice * 100) / 100;
          Shelly.call("Number.Set", { id: 200, value: roundedPrice });
        }
 
          Timer.set(100, false, updatePriceRanking);
        });
      });
    });
  });
}
 
// ➕ NY FUNKTION: Finder placering af aktuel time (1–24)
function updatePriceRanking() {
  let hour = 0;
  let prices = [];
 
  function readNext() {
    if (hour >= 24) {
      if (prices.length !== 24) {
        print("❌ Kunne ikke opdatere prisplacering – mangler data");
        return;
      }
 
      let sorted = prices.slice();
      sortPricesAscending(sorted);
 
      let currentHour = new Date().getHours();
      let placement = 0;
      for (let i = 0; i < sorted.length; i++) {
        if (sorted[i].hour === currentHour) {
          placement = i + 1;
          break;
        }
      }
 
      if (placement > 0) {
        print("📊 Aktuel time er nr.", placement, "billigst");
        Shelly.call("Number.Set", { id: 201, value: placement });
      } else {
        print("❌ Aktuel time ikke fundet i sorteret liste");
      }
      return;
    }
 
    let key = "price_" + pad(hour);
    Shelly.call("KVS.Get", { key: key }, function (result, error) {
      if (!error && result && result.value !== undefined) {
        prices.push({ hour: hour, price: parseFloat(result.value) });
      }
      hour++;
      Timer.set(50, false, readNext);
    });
  }
 
  readNext();
}
 
function runModeCheck() {
  try {
    if (CONFIG.mode === 1) {
      checkCurrentPriceMode1();
    } else if (CONFIG.mode === 2) {
      fetchPricesAndRunMode(checkCheapestHoursMode2);
    } else if (CONFIG.mode === 3) {
      fetchPricesAndRunMode(checkCheapestSequenceMode3);
    } else {
      print("Ukendt mode:", CONFIG.mode);
    }
  } catch (e) {
    print("Fejl i runModeCheck:", e.message);
  }
}
 
function startCountdownLogger() {
  Timer.set(30000, true, function () {
    let now = new Date();
    let msToNextHour = (60 - now.getMinutes()) * 60 * 1000 - now.getSeconds() * 1000;
    let totalSeconds = Math.floor(msToNextHour / 1000);
    let minutes = Math.floor(totalSeconds / 60);
    let seconds = totalSeconds % 60;
    print("⏳ Tid til næste prischeck: " + pad(minutes) + ":" + pad(seconds));
  });
}
 
function scheduleNextHourlyCheck() {
  let now = new Date();
  let msPastHour = (now.getMinutes() * 60 + now.getSeconds()) * 1000 + now.getMilliseconds();
  let msToNextHour = 3600000 - msPastHour + CONFIG.run_seconds_after_hour * 1000;
 
  let totalSec = Math.floor(msToNextHour / 1000);
  let min = Math.floor(totalSec / 60);
  let sec = totalSec % 60;
 
  print("🕒 Planlægger næste check om " + pad(min) + ":" + pad(sec));
  Timer.set(msToNextHour, false, function () {
    runModeCheck();
    scheduleNextHourlyCheck();
  });
}
 
// Start
readAllPricesFromKVS();
runModeCheck();
startCountdownLogger();
scheduleNextHourlyCheck();