Slim laden met Smappee + Belpex (2u vooruit kijken + vermogenslimiet + piek-uren blokkeren)

Voor de geïnteresseerden: ik heb een “Slim Laden”-setup uitgewerkt in Homey voor mijn Smappee laadpaal. Tips/advies om dit te optimaliseren zijn absoluut welkom. :slightly_smiling_face:

Wat doet deze setup?

Deze flows + HomeyScript zorgen ervoor dat de auto (max. 2 uur op voorhand) begint te laden wanneer er binnen de komende 2 uur een goedkoper variabel tarief zit (onder een ingestelde prijsdrempel), op voorwaarde dat:

  • Het globale verbruik onder 6 kW blijft (capaciteitstarief/piek wat temperen).

  • Er niet geladen wordt tussen 17u en 20u (piekperiode).

  • De laadkabel effectief aangesloten is.

  • De laadpaal in Smappee op “normaal laden” staat (dus niet op “smart” vanuit Smappee zelf).

Daarnaast stuur ik telkens een statusbericht naar Telegram met een korte evaluatie: tijd OK/blocked, prijs nu, goedkoopste binnen 2u, vermogen, oplaadmodus, en uiteindelijk “Mag laden: JA/NEE”.


Overzicht flows (conceptueel)

1) Script uitvoeren (regelmatig + bij laad-events)

Triggers:

  • Elke 5 minuten

  • Opladen gestart

  • Opladen gestopt

En dan enkel als “Kabel is aangesloten”HomeyScript uitvoeren (SlimLadenSmappeeV2)

Doel: script berekent of laden mag, en zet:

  • boolean variabele: MagSlimLaden

  • tekst variabele: LaadStatusTekst


2) Actie afdwingen op basis van MagSlimLaden (failsafe)

Trigger:

  • Elke 5 minuten

Dan:

  • ALS MagSlimLaden = jaStandaard laden starten

  • ANDERS → Stop laden

Doel: zelfs als er ergens een state mismatch is, duwt dit elke 5 minuten de laadpaal terug naar de juiste stand.


3) Telegram + directe actie bij wijziging MagSlimLaden

Trigger:

  • MagSlimLaden is veranderd

Dan:

  • ALS MagSlimLaden = ja: start laden + Telegram “Slim Laden is gestart” + LaadStatusTekst

  • ALS MagSlimLaden = nee: stop laden + Telegram “Slim Laden is gestopt” + LaadStatusTekst


4) Handmatig aan/uit (optioneel)

Met een flow/schakelaar (“SlimLadenFlow Aan/Uit”) zet ik:

  • bij Aangezet: MagSlimLaden = ja, Telegram “Slim Laden aangezet”, script runnen

  • bij Uitgezet: MagSlimLaden = nee, Telegram “Slim Laden uitgezet”, slimme sturing uit


HomeyScript (SlimLadenSmappeeV2)

// =====================
// CONFIGURATIE
// =====================
const prijsLimiet = 0.10;                         // Max prijs €/kWh
const maxVermogenWatt = 6000;                     // Max capaciteitstarief (Watt)
const laadpaalNaam = "Smappee Laadpaal";          // Naam van je laadpaal apparaat
const energyDongleNaam = "Homey Energy Dongle";   // Naam van je P1 of dongle
const belpexNaam = "Energy Belpex pricing";       // Naam van Belpex app/device

const boolVarNaam = "MagSlimLaden";               // Boolean variabele
const infoVarNaam = "LaadStatusTekst";            // Tekst variabele

// Tijdvenster waarin laden niet mag
const uitsluitStartUur = 17;
const uitsluitEindUur = 20;

// =====================
// HELPERS: Variabelen zetten (Tommi-fix)
// =====================
async function setBooleanVariable(name, value) {
  const vars = await Homey.logic.getVariables();
  let v = Object.values(vars).find(v => v.name === name);

  if (!v) {
    console.log(`ℹ️ Boolean '${name}' bestaat nog niet. Wordt aangemaakt...`);
    v = await Homey.logic.createVariable({ name, type: "boolean", value: false });
  }

  await Homey.logic.updateVariable({
    id: v.id,
    variable: { value: Boolean(value) } // ✅ Tommi-fix
  });

  console.log(`✅ Boolean '${name}' bijgewerkt naar: ${value}`);
}

async function setStringVariable(name, tekst) {
  const vars = await Homey.logic.getVariables();
  let v = Object.values(vars).find(v => v.name === name);

  if (!v) {
    console.log(`ℹ️ Tekstvariabele '${name}' bestaat nog niet. Wordt aangemaakt...`);
    v = await Homey.logic.createVariable({ name, type: "string", value: "" });
  }

  await Homey.logic.updateVariable({
    id: v.id,
    variable: { value: String(tekst) } // ✅ Tommi-fix
  });

  console.log(`✅ Tekstvariabele '${name}' bijgewerkt naar:\n${tekst}`);
}

// =====================
// HELPERS: Brussels tijd
// =====================
function formatTime(date) {
  return date.toLocaleTimeString("nl-BE", {
    hour: "2-digit",
    minute: "2-digit",
    timeZone: "Europe/Brussels"
  });
}

function formatDateTime(date) {
  return date.toLocaleString("nl-BE", {
    day: "2-digit",
    month: "2-digit",
    year: "numeric",
    hour: "2-digit",
    minute: "2-digit",
    timeZone: "Europe/Brussels"
  });
}

function getBrusselsHour(date) {
  return Number(new Intl.DateTimeFormat("nl-BE", {
    hour: "2-digit",
    hour12: false,
    timeZone: "Europe/Brussels"
  }).format(date));
}

// =====================
// HELPERS: Belpex 2u-vooruit berekenen
// =====================
function readCapabilityValue(capObj, key) {
  return capObj[key]?.value;
}

// Probeert eerst per-uur capabilities (h1/h2 of h+1/h+2), anders fallback.
function getNext2hPrices(capObj) {
  const candidates = [
    ["meter_price_h1", "meter_price_h2"],
    ["meter_price_h+1", "meter_price_h+2"],
  ];

  for (const [k1, k2] of candidates) {
    const p1 = readCapabilityValue(capObj, k1);
    const p2 = readCapabilityValue(capObj, k2);

    const arr = [];
    if (typeof p1 === "number") arr.push({ offset: 1, price: p1 });
    if (typeof p2 === "number") arr.push({ offset: 2, price: p2 });
    if (arr.length) return arr;
  }

  return [];
}

function fallbackFrom8hLowest(capObj, currentHour) {
  const lowest8 = readCapabilityValue(capObj, "meter_price_next_8h_lowest");
  const hour8 = readCapabilityValue(capObj, "hour_next_8h_lowest");
  if (typeof lowest8 !== "number" || typeof hour8 !== "number") return null;

  const delta = (hour8 - currentHour + 24) % 24;
  if (delta >= 1 && delta <= 2) {
    return {
      offset: delta,
      price: lowest8,
      targetHour: (currentHour + delta) % 24,
      source: "8h_fallback"
    };
  }
  return null;
}

// =====================
// MAIN SCRIPT
// =====================
async function main() {
  console.log("🧪 SlimLadenSmappeeV2: Brussels Time + PrijsLimiet");

  const now = new Date();
  const uur = getBrusselsHour(now);
  const tijdString = formatTime(now);
  const datumTijd = formatDateTime(now);

  const tijdOK = !(uur >= uitsluitStartUur && uur < uitsluitEindUur);

  const devices = await Homey.devices.getDevices();
  const laadpaal = Object.values(devices).find(d => d.name === laadpaalNaam);
  const dongle = Object.values(devices).find(d => d.name === energyDongleNaam);
  const belpex = Object.values(devices).find(d => d.name === belpexNaam);

  if (!laadpaal) return console.log("❌ Laadpaal niet gevonden.");
  if (!dongle) return console.log("❌ Energy dongle niet gevonden.");
  if (!belpex) return console.log("❌ Belpex-apparaat niet gevonden.");

  // Huidige prijs
  const prijsNu = belpex.capabilitiesObj["meter_price_h0"]?.value;
  if (typeof prijsNu !== "number") return console.log("❌ Geen actuele Belpex-prijs gevonden.");

  // Goedkoopste binnen 2 uur (per-hour caps), anders fallback op 8h-lowest als die toevallig binnen 2h valt
  const nextArr = getNext2hPrices(belpex.capabilitiesObj);
  let laagste2u = null;

  if (nextArr.length) {
    const minEntry = nextArr.reduce((min, e) => (min && min.price <= e.price) ? min : e, null);
    laagste2u = {
      price: minEntry.price,
      offset: minEntry.offset,
      targetHour: (uur + minEntry.offset) % 24,
      source: "per_hour_caps"
    };
  } else {
    laagste2u = fallbackFrom8hLowest(belpex.capabilitiesObj, uur);
  }

  const uurLaagste2u = laagste2u?.targetHour ?? null;
  const prijsLaagste2u = laagste2u?.price ?? null;

  const prijsZaktBinnen2uOnderLimiet =
    (typeof prijsLaagste2u === "number") && (prijsLaagste2u < prijsLimiet);

  // Vermogen (P1/dongle)
  let huidigVermogen = null;
  if (dongle.capabilitiesObj.meter_power) {
    huidigVermogen = dongle.capabilitiesObj.meter_power.value;
  } else if (dongle.capabilitiesObj.measure_power) {
    huidigVermogen = dongle.capabilitiesObj.measure_power.value;
  }
  if (typeof huidigVermogen !== "number") return console.log("❌ Geen geldig vermogen gemeten.");

  const vermogenOK = huidigVermogen < maxVermogenWatt;

  // Laadpaal mode check (moet NIET "smart" zijn)
  const chargingMode = laadpaal.capabilitiesObj["charging_mode"]?.value ?? null;
  const opladenOK = chargingMode !== "smart";

  // Logica:
  // - Nu laden als prijs nu OK is
  // - OF als er binnen 2u een prijs onder limiet zit (=> max. 2u op voorhand starten)
  const prijsOK = prijsNu <= prijsLimiet;
  const magLaden = tijdOK && vermogenOK && opladenOK && (prijsOK || prijsZaktBinnen2uOnderLimiet);

  const goedkoopsteTekst = (typeof prijsLaagste2u === "number" && uurLaagste2u !== null)
    ? `€${prijsLaagste2u.toFixed(3)} om ${uurLaagste2u}u (${laagste2u?.source ?? "?"})`
    : "n.v.t.";

  const tekst =
`🔋 Slim Laden Evaluatie (${datumTijd})
🕒 Tijd: ${tijdString} → ${tijdOK ? "✅ OK" : "❌ Blok (17–20u)"}
💶 Prijs nu: €${prijsNu.toFixed(3)} → ${prijsOK ? "✅ OK" : "❌ Te hoog"}
📉 Goedkoopste binnen 2u: ${goedkoopsteTekst} → ${prijsZaktBinnen2uOnderLimiet ? "✅ onder limiet" : "❌ niet onder limiet"}
⚡ Vermogen: ${huidigVermogen.toFixed(0)} W → ${vermogenOK ? "✅ OK" : "❌ Te hoog"}
🔌 Oplaadmodus: ${chargingMode ?? "Onbekend"} → ${opladenOK ? "✅ OK" : "❌ Niet toegestaan (smart)"}
🚗 Mag laden: ${magLaden ? "✅ JA" : "❌ NEE"}`;

  console.log(tekst);

  await setBooleanVariable(boolVarNaam, magLaden);
  await setStringVariable(infoVarNaam, tekst);
}

await main();

Telegram output (voorbeeld)

In Telegram krijg ik dan meldingen in deze stijl:

  • “Slim Laden is gestart” + de evaluatie (tijd/prijs/vermogen/mode)

  • “Slim Laden is gestopt” + idem


Ideeën voor optimalisatie (waar ik zelf nog aan denk)

  • Niet enkel “onder prijsLimiet”, maar ook écht het goedkoopste uur targetten (starten op T-2u van dat uur, maar pas effectief laden vanaf een bepaalde grens).

  • Extra check: minimum laadtijd of hysterese (om veel start/stop te vermijden bij schommelingen rond 6 kW).

  • Dynamische vermogensgrens: bv. ‘s nachts iets hoger, overdag lager.

  • EV SoC / gewenste vertrektijd meenemen (als je die data hebt via auto-integratie).

  • Integrate van info afkomstig uit zonnepanelen & opbrengst.