Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /**
- * ============================================================================
- * HOME BATTERY SMART CONTROLLER - FINITE STATE MACHINE (FSM)
- * ============================================================================
- *
- * ALGEMENE WERKING:
- * Dit script functioneert als het "brein" tussen een slimme digitale meter (P1)
- * en een thuisbatterij (bv. Anker Solix). Het script wordt elke paar seconden
- * uitgevoerd en heeft als hoofddoelen:
- * 1. Peak Shaving: Voorkomen dat het netverbruik binnen een kwartier te hoog
- * oploopt (om capaciteitstarief te besparen).
- * 2. Zelfverbruik: Zonne-energie efficiënt opvangen en gebruiken.
- * 3. Reservebeheer: Zorgen dat de batterij altijd voldoende buffer heeft voor
- * onverwachte stroompieken, afhankelijk van het uur van de dag.
- *
- * DE 4 STATES (TOESTANDEN):
- * Het script bepaalt in welke 'State' de batterij zich moet bevinden:
- * - "Native Zelfverbruik": Batterij bestuurt zichzelf. Vangt zonne-energie
- * op en limiteert standaard op zijn eigen hardware-maximum (3500W). Actief bij
- * ruime reserve of zonne-injectie.
- * - "Peak Shaving": Noodingreep! Het kwartiergemiddelde dreigt een drempel
- * (target) te overschrijden. Node-RED neemt de besturing over en berekent
- * proportioneel hoeveel Watt de batterij moet leveren om de piek af te toppen.
- * - "Veilig Opladen": De reserve is te laag. De batterij moet geforceerd
- * bijladen (uit het net of uit zon) tot een veilige grens (Cap_Hoog), maar
- * zonder daarbij zélf een nieuwe piek te veroorzaken.
- * - "Standby Waakhond": Batterij staat stil (0W). Reserve is op peil, er is
- * geen zon en geen piekgevaar. Stroom wordt vastgehouden voor noodgevallen.
- *
- * DE ANTI-SPAM BOUNCER:
- * Om te voorkomen dat we de batterij-omvormer via Modbus overspoelen met commando's
- * voor marginale wijzigingen (zoals 2100W -> 2105W), zit er aan het einde van het
- * script een "bouncer". Deze filtert de output en laat enkel commando's door bij:
- * - Een State-wissel.
- * - Grote vermogenssprongen (> 100W).
- * - Periodieke updates (elke 30 seconden) voor kleine correcties.
- * ============================================================================
- */
- // ============================================================================
- // STAP 1: HELPER FUNCTIE VOOR HOME ASSISTANT DATA (MET ERROR HANDLING)
- // ============================================================================
- // Doel: Voorkomen dat het script met foute data gaat rekenen als een sensor offline is.
- // Werking: Haalt de state van een HA entiteit op. Als een sensor ontbreekt of
- // geen getal is, gooien we een Error. Deze wordt onderaan het script opgevangen
- // (in het catch-blok) om een veilige Failsafe uit te sturen.
- const getHAState = (entity_id) => {
- let haData = global.get('homeassistant');
- if (!haData || !haData.homeAssistant || !haData.homeAssistant.states || !haData.homeAssistant.states[entity_id]) {
- throw new Error(`HA sensor '${entity_id}' ontbreekt.`);
- }
- let val = parseFloat(haData.homeAssistant.states[entity_id].state);
- if (isNaN(val)) {
- throw new Error(`HA sensor '${entity_id}' is NaN.`);
- }
- return val;
- };
- try {
- // ============================================================================
- // STAP 2: INLEZEN VAN ACTUELE SENSORDATA
- // ============================================================================
- // We halen de live data binnen die we nodig hebben voor onze berekeningen.
- let monthly_peak_w = getHAState('sensor.electricity_meter_peak_demand_current_month');
- let avg_demand_w = getHAState('sensor.electricity_meter_average_demand');
- let grid_power = getHAState('sensor.electricity_meter_power'); // P1 meter (positief = afname, negatief = injectie)
- // BELANGRIJK: Zodra je de batterij hebt, controleer goed in HA of deze sensor effectief
- // een POSITIEF getal geeft bij ontladen (stroom leveren) en een NEGATIEF getal bij opladen (stroom opnemen)!
- let battery_discharge_w = getHAState('sensor.solix_battery_discharge_power'); // Wat levert de batterij nú?
- let battery_capacity_kwh = getHAState('sensor.battery_remaining_capacity'); //controleer of the batterij de remaining capacity iderdaad in kwh levert. ALs het een % is zullen we een berekening moeten doen.
- let p_floor_w = getHAState('sensor.base_load_5th_percentile'); // Dynamisch berekend sluipverbruik (5e percentiel)
- // Zet batterijcapaciteit om naar Wattuur (Wh) om makkelijk te kunnen vergelijken met onze drempels.
- let capaciteit_wh = battery_capacity_kwh * 1000;
- // ============================================================================
- // STAP 3: TIJD & DREMPELS BEREKENEN
- // ============================================================================
- // Doel: Bepalen hoeveel reserve de batterij op dit moment van de dag nodig heeft.
- let huidigUur = new Date().getHours();
- // De array met minimum veilige batterijcapaciteit in Wh per uur van de dag (00:00 - 23:00).
- // Ochtend: lage reserve nodig. Avondpiek (15:00-19:00): hoge reserve nodig.
- const tabel_Cap_Laag = [400, 400, 400, 400, 400, 400, 400, 400, 400, 400, 500, 500, 600, 800, 900, 1200, 1300, 1300, 1200, 1000, 500, 500, 400, 400];
- let Cap_Laag = tabel_Cap_Laag[huidigUur];
- // Zodra we geforceerd opladen (State 3), stoppen we pas als we 50Wh bóven het minimum zitten.
- // Dit creëert 'hysteresis' om te voorkomen dat de batterij continu aan/uit schakelt (pendelen).
- let Cap_Hoog = Cap_Laag + 50;
- // Hardcoded hardware & doelstelling parameters
- const marge_laden = 400; // Veiligheidsmarge bij opladen
- let p_bat_max = 3500; // Maximale fysieke output van de batterij-omvormer (Anker Solix)
- let target_w = Math.max(2500, monthly_peak_w); // De piek (in Watt) die we dit kwartier niet willen overschrijden
- // Kwartier-klok: In België wordt het capaciteitstarief gemeten over vaste kwartieren (00-15, 15-30, etc).
- // We berekenen exact hoeveel seconden er al verstreken zijn in het huidige kwartier.
- let now_date = new Date();
- let seconds_left = 900 - ((now_date.getMinutes() % 15) * 60 + now_date.getSeconds());
- let elapsed_seconds = 900 - seconds_left;
- // ============================================================================
- // STAP 4: ADVANCED PEAK VOORSPELLING (DE "STATE 2" MOTOR)
- // ============================================================================
- // Doel: Voorspellen of het huidige kwartier boven onze "target_w" gaat uitkomen.
- // De standaard P1 gemiddelde-sensor toont in het begin van het nieuwe kwartier nog de oude waarde van het vorige kwartier. Daarom gebruiken we de eerste 30 seconden de waarde 0.
- let power_15m = (seconds_left < (900 - 30)) ? avg_demand_w : 0;
- let accum_grid_ws = power_15m * elapsed_seconds; // Reeds verbruikte energie dit kwartier (Watt-seconden)
- // Het "Echte" Huisverbruik = Netafname + Batterij afgifte.
- // Let op: Bij zonne-injectie is grid_power negatief. Dat ronden we af naar 0 met Math.max voor de Peak Shaving berekeningen.
- let current_grid_draw = Math.max(grid_power, 0);
- let current_house_power = current_grid_draw + battery_discharge_w;
- // Bereken de theoretische net-afname zonder dat de huidige actie van de batterij het beeld vervuilt.
- // We knippen negatieve waarden hier níét af, zodat we ook bij overvloedige zon correct kunnen opladen.
- let net_afname_zonder_batterij = grid_power + battery_discharge_w;
- // Voorspellingsalgoritme:
- // We willen niet bij de eerste zware minuut van een kwartier direct de batterij uitputten.
- // We gebruiken een "Alfa" (0.0 tot 1.0) om te weten hoe ver we in het kwartier zitten.
- // Aan het begin (alfa ~0) gaan we ervan uit dat het verbruik straks weer terugzakt naar p_floor_w.
- // Aan het einde (alfa ~1) nemen we de actuele, harde verbruikscijfers serieuzer.
- // Uitleg Alpha Dampening: Als je in minuut 1 van het kwartier een waterkoker (2000W) aanzet
- // voor een kopje thee, schiet het actuele verbruik enorm omhoog. Zonder demping zou de code denken:
- // "We gaan dit hele kwartier 2000W verbruiken, we moeten de batterij volledig opentrekken!" Dat is zonde.
- // Met dampening mixen we het actuele verbruik met het 'normale bodemverbruik'. We gebruiken een
- // factor van 0.30 (30%). Dit zorgt ervoor dat abrupte stijgingen worden "afgevlakt" in onze voorspelling,
- // waardoor we kalmer en veel zuiniger omspringen met de batterijcapaciteit aan het begin van een kwartier.
- let alpha_dampening = 0.30;
- let grace_period_seconds = 60; // De eerste minuut van een kwartier grijpen we nooit in (te onzeker)
- let alfa = elapsed_seconds / 900;
- // Uitleg Voorspellingsformule: P_pred staat voor "Predicted Power" (voorspeld vermogen).
- // We berekenen een 'gewogen gemiddelde' tussen het normale bodemverbruik (p_floor_w) en het
- // werkelijke verbruik op dit exacte moment (current_house_power). Naarmate we verder in het kwartier
- // zitten (alfa stijgt van 0 naar 1), weegt het huidige verbruik steeds zwaarder door in de berekening.
- // De 'Math.max' zorgt voor een absolute ondergrens: de voorspelling mag nóóit lager zijn dan
- // het bekende bodemverbruik, zelfs niet door wiskundige afrondingen.
- let p_pred = Math.max(p_floor_w, (1 - (alfa * alpha_dampening)) * p_floor_w + (alfa * alpha_dampening) * current_house_power);
- // Trigger 1: Voorspelt ons gedempte algoritme dat we de kwartierpiek gaan overschrijden?
- let trigger_1_alfa = (accum_grid_ws + (p_pred * seconds_left)) > (target_w * 900);
- // Trigger 2: Hardware-limiet Check. Zelfs als we max (3500W) leveren, gaan we er dán alsnog over op basis van het huidige verbruik?
- let trigger_2_hardware = (accum_grid_ws + (Math.max(0, current_house_power - p_bat_max) * seconds_left)) > (target_w * 900);
- // Uitleg Piekgevaar: Deze regel beslist finaal of de noodklok luidt (True of False).
- // Het is alléén "Piekgevaar" als de 60-seconden wachttijd (grace period) is afgelopen,
- // EN ten minste één van onze twee voorspellende triggers is afgegaan.
- let Piekgevaar = (elapsed_seconds >= grace_period_seconds) && (trigger_1_alfa || trigger_2_hardware);
- // Uitleg Net_Injectie: De P1 meter levert een negatief getal op als de zonnepanelen meer energie
- // produceren dan het huis verbruikt. Die stroom vloeit naar het net (injectie).
- // Door net_afname_zonder_batterij te gebruiken, blijven we zon zien, óók als de batterij alles zelf wegsnoept.
- let Net_Injectie = (net_afname_zonder_batterij < 0);
- // ============================================================================
- // STAP 5: WAARSCHUWINGS-ALGORITME (Point of No Return)
- // ============================================================================
- // Doel: Een notificatie triggeren (bv via app) als de batterij het niet meer kán redden
- // en menselijk ingrijpen vereist is (een groot apparaat moet NU uit).
- let trigger_warning = false;
- let reaction_time_s = 20; // De tijd die een mens nodig heeft om naar de knop te rennen
- let achievable_baseload_w = 1000; // Wat het verbruik zal zijn nadat de mens heeft ingegrepen
- // Uitleg Worst-Case Scenario: Dit blok berekent het ultieme noodgeval. Het algoritme test in gedachten
- // wat er gebeurt als er nét op de limiet een nood-alarm naar jouw telefoon wordt gestuurd:
- // 1. (t_react): Je hebt 20 seconden nodig om je oven of warmtepomp uit te zetten. Tijdens die
- // 20 seconden blijft het hoge verbruik bestaan (sim_house_ws_react).
- // 2. (t_after): Daarna is het apparaat uit, en val je terug op de basislast van 1000W voor
- // de rest van het kwartier (sim_house_ws_after).
- // 3. We berekenen hoeveel de batterij nog fysiek kan ondersteunen tijdens deze hele noodsituatie
- // rekening houdend met zijn restcapaciteit en hardware limiet (actual_sim_bat_ws).
- // 4. Conclusie (point_of_no_return_peak): Als we het al verbruikte budget (accum_grid_ws) optellen
- // bij dit strakke nood-scenario, en we overschrijden dán alsnog het target, dán moeten we het
- // alarm nu écht direct laten afgaan.
- if (elapsed_seconds >= grace_period_seconds) {
- let t_react = Math.min(seconds_left, reaction_time_s);
- let t_after = seconds_left - t_react;
- let total_sim_house_ws = (current_house_power * t_react) + (achievable_baseload_w * t_after);
- let total_sim_bat_ws = (Math.min(current_house_power, p_bat_max) * t_react) + (Math.min(achievable_baseload_w, p_bat_max) * t_after);
- let available_battery_ws = capaciteit_wh * 3600;
- let actual_sim_bat_ws = Math.min(total_sim_bat_ws, available_battery_ws);
- let sim_grid_ws = Math.max(0, total_sim_house_ws - actual_sim_bat_ws);
- let point_of_no_return_peak = (accum_grid_ws + sim_grid_ws) / 900;
- if (point_of_no_return_peak > target_w) trigger_warning = true;
- }
- // ============================================================================
- // STAP 6: NODE-RED GEHEUGEN OPHALEN (Voor FSM & Anti-Spam)
- // ============================================================================
- // Om transities en spam te beheren, moeten we weten wat we de vorige keer (meestal
- // een paar seconden geleden) hebben gedaan. We initialiseren naar "Native Zelfverbruik".
- let last_state = context.get("last_state") || "Native Zelfverbruik";
- let last_gesigneerd_wattage = context.get("last_gesigneerd_wattage") || 0;
- let last_command_time = context.get("last_command_time") || 0;
- // Deze boolean onthoudt dat we in State "Veilig Opladen" zaten, zodat we erin blijven plakken tot Cap_Hoog bereikt is.
- let state_3_active = context.get("state_3_active") || false;
- let now = Date.now();
- let time_since_last_cmd = (now - last_command_time) / 1000;
- // ============================================================================
- // STAP 7: STATE MACHINE - BEPAAL STATE
- // ============================================================================
- // Doel: Kies de juiste operationele modus, gebruik makend van een strikte prioriteit.
- let gewenste_state = "Native Zelfverbruik";
- if (Piekgevaar && capaciteit_wh <= Cap_Hoog) {
- gewenste_state = "Peak Shaving"; // Piekbestrijding is prioriteit nummer 1 (als we reserve hebben).
- state_3_active = false; // Een eventuele laadcyclus wordt direct afgebroken.
- } else if (state_3_active) {
- // Als we al aan het laden waren, blijf dat doen ("plakkerig") tot het vol is.
- if (capaciteit_wh >= Cap_Hoog) {
- state_3_active = false;
- gewenste_state = Net_Injectie ? "Native Zelfverbruik" : "Standby Waakhond"; // Bepaal waar we heen gaan na het opladen.
- } else {
- gewenste_state = "Veilig Opladen";
- }
- } else if (capaciteit_wh <= Cap_Laag && !Piekgevaar) {
- gewenste_state = "Veilig Opladen"; // Reserve is te ver gezakt, begin met opladen (activeer de plakkerige vlag).
- state_3_active = true;
- } else if (capaciteit_wh <= Cap_Hoog && !Piekgevaar && !Net_Injectie) {
- gewenste_state = "Standby Waakhond"; // We zitten in de veilige zone tussen Laag en Hoog, en het is rustig. Slaap.
- } else {
- gewenste_state = "Native Zelfverbruik"; // Default modus: de hardware mag zijn gang gaan met zelfverbruik/zon.
- }
- // ============================================================================
- // STAP 8: BEREKEN COMMANDO'S (Bedrijfsmodus, Regeling & Vermogen)
- // ============================================================================
- // Doel: Vertaal de "State" naar begrijpelijke commando's voor de batterij-API.
- let gewenst_vermogen = 0;
- let bedrijfsmodus = "zelfverbruik";
- let laad_ontlaadregeling = "ontladen";
- if (gewenste_state === "Native Zelfverbruik") {
- bedrijfsmodus = "zelfverbruik"; // Native hardware afhandeling.
- if (Net_Injectie) {
- laad_ontlaadregeling = "laden";
- // We genereren een "fictief" laadvermogen gelijk aan de net-injectie.
- // De batterij luistert hier in deze State niet naar, maar onze Anti-Spam (stap 9) heeft
- // dit nodig om te zien hoe drastisch de situatie is veranderd als we bv. naar "Peak Shaving" gaan.
- gewenst_vermogen = Math.abs(grid_power);
- } else {
- laad_ontlaadregeling = "ontladen";
- gewenst_vermogen = 0;
- }
- } else if (gewenste_state === "Peak Shaving") {
- bedrijfsmodus = "externe_besturing";
- laad_ontlaadregeling = "ontladen";
- // Bereken het "Ideale" batterijvermogen: precies genoeg om netjes op target_w te landen.
- // (target_w * 900) is ons totale energiebudget voor dit kwartier in Watt-seconden.
- // We trekken daarvan af wat we al verbruikt hebben (accum_grid_ws). Wat overblijft delen
- // we door de resterende seconden. Zo weet je exact hoeveel Watt je de rest van het kwartier
- // continu mag gebruiken zonder de target te overschrijden.
- let allowed_grid_draw_w = (seconds_left > 0) ? ((target_w * 900) - accum_grid_ws) / seconds_left : 0;
- let command_w = Math.max(0, current_house_power - allowed_grid_draw_w);
- // Harde limieten toepassen op de berekening:
- // 1. Lever nooit meer dan het huis vraagt (nul-op-de-meter principe).
- if (command_w > current_house_power) command_w = current_house_power;
- // 2. Overschrijd de hardware-limiet niet.
- if (command_w > p_bat_max) command_w = p_bat_max;
- // 3. Failsafe als de batterij totaal leeg is.
- if (capaciteit_wh <= 1) command_w = 0;
- gewenst_vermogen = command_w;
- } else if (gewenste_state === "Veilig Opladen") {
- bedrijfsmodus = "externe_besturing";
- laad_ontlaadregeling = "laden";
- // Hoeveel stroom is er "over" op het net voordat we zélf een piek veroorzaken met opladen?
- // Let op: we baseren dit op de ware (kale) net-afname zodat de eigen oplaadactie geen wilde schommelingen veroorzaakt.
- let veilige_laadruimte = (target_w - marge_laden) - net_afname_zonder_batterij;
- gewenst_vermogen = Math.min(p_bat_max, Math.max(0, veilige_laadruimte));
- } else if (gewenste_state === "Standby Waakhond") {
- bedrijfsmodus = "externe_besturing";
- laad_ontlaadregeling = "ontladen"; // Regeling maakt niet uit, want we sturen 0W.
- gewenst_vermogen = 0;
- }
- // ============================================================================
- // STAP 9: DE UNIVERSELE ANTI-SPAM REGEL (BOUNCER)
- // ============================================================================
- // Doel: Modbus overbelasting en zenuwachtig gedrag van de omvormer voorkomen.
- // We rekenen intern met een gesigneerd getal (ontladen = +, laden = -) puur
- // om de ware omvang van de verandering ("de sprong") te meten.
- let gesigneerd_wattage = (laad_ontlaadregeling === "ontladen") ? gewenst_vermogen : -gewenst_vermogen;
- let stuur_commando = false;
- let wattage_verschil = Math.abs(gesigneerd_wattage - last_gesigneerd_wattage);
- let state_gewijzigd = (gewenste_state !== last_state);
- let groot_wattage_verschil = (wattage_verschil > 100);
- if (state_gewijzigd) {
- // Specifieke beveiliging: Als we "Peak Shaving" verlaten, maar het
- // vermogensverschil is klein (we balanceren op het randje), wachten we 30 seconden.
- if (last_state === "Peak Shaving" && !groot_wattage_verschil) {
- if (time_since_last_cmd > 30) stuur_commando = true;
- } else {
- // Alle andere state-wissels zijn kritiek en vereisen directe actie.
- stuur_commando = true;
- }
- } else if (groot_wattage_verschil) {
- // We blijven in dezelfde state, maar het vermogen verandert met meer dan 100W (bijv wolkbreuk of oven aan).
- stuur_commando = true;
- } else if (time_since_last_cmd > 30 && wattage_verschil > 10) {
- // Er is langer dan 30 seconden geen commando verstuurd. We staan kleine correcties toe
- // om netjes op schema te blijven (tenzij het verschil < 10W is, dan is het micro-schommeling).
- stuur_commando = true;
- }
- // ============================================================================
- // STAP 10: PAYLOAD KLAARMAKEN OF STOPPEN
- // ============================================================================
- // Bereid de waarschuwings-output voor (dit is Output 2 van de node).
- let msg2 = trigger_warning ? { payload: "WAARSCHUWING: Piek risico! Schakel verbruikers uit." } : null;
- let msg1 = null; // Dit wordt Output 1 (naar de batterij)
- // Alleen als de Bouncer het toelaat, gaan we geheugen updaten en Output 1 vullen.
- if (stuur_commando) {
- context.set("last_state", gewenste_state);
- context.set("last_gesigneerd_wattage", gesigneerd_wattage);
- context.set("last_command_time", now);
- context.set("state_3_active", state_3_active);
- msg1 = {
- payload: {
- state: gewenste_state,
- bedrijfsmodus: bedrijfsmodus,
- laad_ontlaadregeling: laad_ontlaadregeling,
- laad_ontlaadvermogen: Math.round(gewenst_vermogen)
- },
- topic: `${gewenste_state} | ${bedrijfsmodus} | ${laad_ontlaadregeling} | ${Math.round(gewenst_vermogen)}W`
- };
- }
- // Stuur beide outputs uit.
- // Output 1: Batterij commando (of null als geblokkeerd door anti-spam).
- // Output 2: Notificatie trigger (of null als er geen menselijk ingrijpen nodig is).
- return [msg1, msg2];
- } catch (error) {
- // ============================================================================
- // FATAL ERROR FAILSAFE (OPVANGNET)
- // ============================================================================
- // Als er in de code ergens een 'throw new Error' wordt getriggerd (bijv. sensor
- // offline of netwerkfout), stopt de normale logica direct en komt hij hier terecht.
- // We sturen dan een noodcommando om de batterij veilig naar zijn native modus
- // ("Zelfverbruik") te dwingen, zodat deze niet vast blijft hangen in Peak Shaving.
- node.error(`Kritieke Fout, Failsafe geactiveerd: ${error.message}`);
- let failsafe_msg1 = {
- payload: {
- state: "Native Zelfverbruik",
- bedrijfsmodus: "zelfverbruik",
- laad_ontlaadregeling: "ontladen",
- laad_ontlaadvermogen: 0
- },
- topic: "Failsafe Actief | Native Zelfverbruik"
- };
- let failsafe_msg2 = {
- payload: `KRITIEKE FOUT IN SCRIPT: Failsafe geactiveerd wegens probleem (${error.message}). Batterij besturing teruggegeven aan interne Anker hardware.`
- };
- // Bij een error blokkeren we bewust het opslaan van context variabelen om
- // corruptie van the anti-spam timer te voorkomen.
- return [failsafe_msg1, failsafe_msg2];
- }
Advertisement