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.

Hey Tony, ziet er veelbelovend uit. Ik probeer het script ook wel eens uit samen met de Flows. Ben nieuw bij het gebruik van Homey dus het zal wat experimenteren zijn…

Groeten
Wim