// ===================== KONFIGURATION =====================
const hyper1 = {
name: 'Hyper1',
inputLimitDP: 'zendure-solarflow.0.aaaaaa.bbbbbbbb.control.setInputLimit',
outputLimitDP: 'zendure-solarflow.0.aaaaaa.bbbbbbbb.control.setOutputLimit',
acModeDP: 'zendure-solarflow.0.aaaaaa.bbbbbbbb.control.acMode',
socDP: 'zendure-solarflow.0.aaaaaa.bbbbbbbb.electricLevel',
};
const hyper2 = {
name: 'Hyper2',
inputLimitDP: 'zendure-solarflow.0.cccccc.dddddddd.control.setInputLimit',
outputLimitDP: 'zendure-solarflow.0.cccccc.dddddddd.control.setOutputLimit',
acModeDP: 'zendure-solarflow.0.cccccc.dddddddd.control.acMode',
socDP: 'zendure-solarflow.0.cccccc.dddddddd.electricLevel',
};
const logHTMLDP = '0_userdata.0.Eigene_Variablen.PV.Solarflow.SolarflowLog';
const hausverbrauchDP = 'mqtt.0.openWB.global.WHouseConsumption';
const evuLeistungDP = 'shelly.1.shellypro3em#ggggggggggg#1.EM0.TotalActivePower';
const hausverbrauchBisherDP = 'mqtt.0.openWB.global.DailyYieldHausverbrauchKwh';
const pv1LeistungDP = 'mqtt.0.openWB.pv.1.W';
const pv2LeistungDP = 'zendure-solarflow.0.aaaaaa.bbbbbbbb.solarInputPower';
const priceLoadingDP = '0_userdata.0.Eigene_Variablen.PV.Solarflow.PriceLoading';
const preisLadenModusDP = '0_userdata.0.Eigene_Variablen.PV.Solarflow.PreisLadenModus'; // 0=konservativ, 1=aggressiv
const stromGuenstigDP = 'tibberlink.0.Homes.8f8a4278-17b9-48fb-ffff-eeeeeeeeeeee.Eigene_Werte.load_batterie';
const stromTeuerDP = 'tibberlink.0.Homes.8f8a4278-17b9-48fb-ffff-eeeeeeeeeeee.Eigene_Werte.expensive_single_hour';
const pvForecastTodayDP = 'pvforecast.1.summary.energy.today';
const pvForecastRestDP = 'pvforecast.1.summary.energy.nowUntilEndOfDay';
const socMin = 10;
const socMax = 100;
const ladenStartSchwelle = 100;
const ladenStoppSchwelle = 30;
const entladenStartSchwelle = 40;
const entladenStoppSchwelle = 20;
const netzPufferWatt = 30;
const maxLeistung = xxx; // Anpassen
const maxLadeleistungGesamt = maxLeistung * 2;
const kapazitaetKWh1 = x.y; // Anpassen
const kapazitaetKWh2 = x.y; // Anpassen
// Parameter für eAuto laden
const ampereSetDP = 'go-e.0.ampere'; // Wallbox-Stromstärke setzen
const allowChargingDP = 'go-e.0.allow_charging'; // Laden zulassen
const ladenVorrangDP = '0_userdata.0.Eigene_Variablen.PV.Laden_Vorrang';
const ladeleistungReserveDP = '0_userdata.0.Eigene_Variablen.openWB.Ladeleistung_Reserve';
const openWBLadepunkt1DP = 'mqtt.0.openWB.lp.1.W';
const autoMaxSOCDP = 80; // Ziel-SOC des E-Autos
const autoSocDP = 'mqtt.0.openWB.lp.1.%Soc'; // Beispiel-Datenpunkt für Auto-SOC
const autoPrioGrenzeSocDP = '0_userdata.0.Eigene_Variablen.PV.Solarflow.Grenze_Prio_SOC';
const cargeModeDP = '0_userdata.0.Eigene_Variablen.openWB.ChargeMode';
const ladepunktDP = '0_userdata.0.Eigene_Variablen.openWB.ChargePointEnabled';
const autoPlugstatDP = 'mqtt.0.openWB.lp.1.boolPlugStat'
const minAmp = 6; // Mindeststromstärke
const maxAmp = 16; // Maximale Stromstärke
const ampereWatt = 690; // 1 A entspricht 690 W bei 3-phasig (oder 230 für 1-phasig)
const rampUpBuffer = 150; // zusätzlich benötigte Watt je Schritt (zur Sicherheit)
const minEinspeisungW = 1400; // Mindestwert für Ladefreigabe
const rampUpDelay = 1 * 30 * 1000; // 30 Sekunden Mindestverzögerung zwischen Ramp-Ups
//Später z.B. zwischen Sommer-/Winterlogik unterscheiden (z.B. aggressiveres Entladen im Winter)
//const monat = (new Date()).getMonth() + 1; // Januar = 1
//const istWinter = monat <= 2 || monat >= 11;
const maxDurchlaeufeImLog = 2;
let logDurchlaeufe = [];
// Logging-Funktion VIS
function visLog(meldung, typ = 'info', amAnfang = false) {
const zeit = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const farben = {
info: '', ok: '#28a745', warn: '#d39e00', error: '#dc3545',
laden: '#007f3f', entladen: '#0057b7', stop: '#6c757d',
mode: '#17a2b8', header: '#6c757d'
};
const style = farben[typ] ? ` style="color:${farben[typ]}"` : '';
const eintrag = `
${zeit} | ${meldung} |
`;
if (logDurchlaeufe.length === 0) logDurchlaeufe.unshift([]);
if (amAnfang) logDurchlaeufe[0].unshift(eintrag);
else logDurchlaeufe[0].push(eintrag);
while (logDurchlaeufe.length > maxDurchlaeufeImLog) logDurchlaeufe.pop();
let html = '';
for (let i = 0; i < logDurchlaeufe.length; i++) {
html += logDurchlaeufe[i].join('');
if (i < logDurchlaeufe.length - 1) {
html += `──────────────────────────── |
`;
}
}
setState(logHTMLDP, ``);
log(meldung);
}
// Hilfsfunktionen
function getStateVal(id, def = 0) {
const s = getState(id);
return (s && typeof s.val !== 'undefined') ? s.val : def;
}
function getSOC(h) {
return getStateVal(h.socDP);
}
function getKWh(soc, kapazitaet) {
return (soc / 100) * kapazitaet;
}
function isPreisGuenstig() {
return getStateVal(stromGuenstigDP, false) === true;
}
function isPreisTeuer() {
return getStateVal(stromTeuerDP, false) === true;
}
function verteileLeistungNachSOC(gesamt, soc1, soc2, modus = 'laden') {
const limit = 2;
const maxProHyper = 1200;
const pv2BonusProzent = 2;
// === Preis-Laden aktiv ===
if (gesamt <= 0) return [0, 0];
let effektiverSOC1 = soc1;
let effektiverSOC2 = soc2;
if (modus === 'laden') {
effektiverSOC1 += pv2BonusProzent;
}
let g1 = 0.5, g2 = 0.5;
const diff = effektiverSOC1 - effektiverSOC2;
if (modus === 'laden') {
if (diff > limit) {
g1 = 0.3; g2 = 0.7;
} else if (diff < -limit) {
g1 = 0.7; g2 = 0.3;
}
} else if (modus === 'entladen') {
if (diff > limit) {
g1 = 0.7; g2 = 0.3;
} else if (diff < -limit) {
g1 = 0.3; g2 = 0.7;
}
}
let leistung1 = Math.min(Math.round(gesamt * g1), maxProHyper);
let leistung2 = Math.min(Math.round(gesamt * g2), maxProHyper);
// Korrektur falls Summe nicht mit Gesamtleistung übereinstimmt
let korrektur = Math.round(gesamt) - (leistung1 + leistung2);
if (korrektur !== 0) {
if (leistung1 > leistung2) leistung1 += korrektur;
else leistung2 += korrektur;
}
return [leistung1, leistung2];
}
function setLademodus(h, leistung) {
setState(h.acModeDP, 1);
setState(h.inputLimitDP, Math.round(Math.max(0, leistung)));
}
function setEntlademodus(h, leistung) {
setState(h.acModeDP, 2);
setState(h.outputLimitDP, Math.round(Math.max(0, leistung)));
}
function stopHyper(h) {
setState(h.outputLimitDP,0);
setState(h.inputLimitDP,0);
}
// Prüft, ob ein Moduswechsel zwischen Laden und Entladen aktuell erlaubt ist
function delayWechselErlaubt(altModus, neuModus, letzterWechselZeitpunkt, delaySekunden) {
if ((altModus === 1 && neuModus === 2) || (altModus === 2 && neuModus === 1)) {
const vergangen = (Date.now() - letzterWechselZeitpunkt) / 1000;
return vergangen >= delaySekunden;
}
return true;
}
function pruefeObSonnenuntergangErreicht() {
const jetzt = new Date();
const sunsetUhrzeit = getState('0_userdata.0.Eigene_Variablen.Zeiten.Sonnenuntergang_minus_60').val; // z.B. "20:29"
if (!sunsetUhrzeit || typeof sunsetUhrzeit !== 'string' || !sunsetUhrzeit.match(/^\d{2}:\d{2}$/)) {
log("⚠️ Sonnenuntergangszeit ungültig: " + sunsetUhrzeit, 'warn');
return;
}
// Aktuelles Datum + Sonnenuntergangszeit kombinieren
const [stunden, minuten] = sunsetUhrzeit.split(':').map(Number);
const sunset = new Date(jetzt.getFullYear(), jetzt.getMonth(), jetzt.getDate(), stunden, minuten, 0);
if (!amountHoursHeuteBerechnet && jetzt >= sunset) {
amountHoursHeuteBerechnet = true;
const soc1 = getState(hyper1.socDP).val;
const soc2 = getState(hyper2.socDP).val;
const hausverbrauchProStunde = 0.6; // fixer Nachtverbrauch
passeAmountHoursAn(soc1, soc2, hausverbrauchProStunde);
}
}
function passeAmountHoursAn(soc1, soc2, verbrauchProStunde = 0.6) {
const kap1 = getKWh(soc1, kapazitaetKWh1);
const kap2 = getKWh(soc2, kapazitaetKWh2);
const gesamtKWh = kap1 + kap2;
// Zeiten holen
const sunsetStr = getStateVal('0_userdata.0.Eigene_Variablen.Zeiten.Sonnenuntergang_minus_60', '');
const sunriseStr = getStateVal('0_userdata.0.Eigene_Variablen.Zeiten.Sonnenaufgang_plus_60', '');
if (!sunsetStr.match(/^\d{2}:\d{2}$/) || !sunriseStr.match(/^\d{2}:\d{2}$/)) {
log(`❌ Fehler: Sonnenzeiten ungültig – Sunset: ${sunsetStr}, Sunrise: ${sunriseStr}`, 'warn');
return;
}
const now = new Date();
const [sunsetH, sunsetM] = sunsetStr.split(':').map(Number);
const [sunriseH, sunriseM] = sunriseStr.split(':').map(Number);
const sunset = new Date(now.getFullYear(), now.getMonth(), now.getDate(), sunsetH, sunsetM);
const sunrise = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, sunriseH, sunriseM);
const nachtDauerStunden = (sunrise - sunset) / (1000 * 60 * 60);
const abdeckbareStunden = gesamtKWh / verbrauchProStunde;
const fehlendeStunden = Math.max(0, nachtDauerStunden - abdeckbareStunden);
// AmountHours linear zwischen 2 (voller Akku) und 12 (Akku leer)
let amountHours;
if (fehlendeStunden <= 0) {
amountHours = 4;
} else if (fehlendeStunden >= nachtDauerStunden) {
amountHours = 12;
} else {
const skala = (fehlendeStunden / nachtDauerStunden); // 0...1
amountHours = 4 + skala * 10; // linear zwischen 2 und 12
}
amountHours = Math.round(amountHours);
setState('tibberlink.0.Homes.8f8a4278-17b9-48fb-ffff-eeeeeeeeeeee.Calculations.2.AmountHours', amountHours);
visLog(`🌙 Nacht: ${nachtDauerStunden.toFixed(1)}h | Akku reicht für ~${abdeckbareStunden.toFixed(1)}h → fehlend: ${fehlendeStunden.toFixed(1)}h → AmountHours: ${amountHours}`, 'info');
}
// ===================== HAUPTSTEUERUNG =====================
// Globale Variablen für Modus-Hysterese
let lastMode = 0; // 0=stop,1=laden,2=entladen
let lastSwitchTime = 0; // Timestamp in ms
const modusDelaySek = 20; // Verzögerung zwischen Laden und Entladen
function steuerung(h1, h2) {
logDurchlaeufe.unshift([]);
const jetztTs = Date.now();
const jetzt = new Date();
// PV-Leistung erfassen
let pvLeistung1 = -1 * getStateVal(pv1LeistungDP, 0); // Solaredge
let pvLeistung2 = getStateVal(pv2LeistungDP, 0); // Hyper 2
// SOC und Verbrauch
const soc1 = getSOC(h1);
const soc2 = getSOC(h2);
const avgSOC = (soc1 + soc2) / 2;
const autoSOC = getStateVal(autoSocDP, 100); // aktueller Auto-SOC
const speicherMinSOC = Math.min(soc1, soc2); // niedrigster Speicher-SOC
const aktuelleEinspeisung = getState(evuLeistungDP).val; // Positiv = Netzbezug, negativ = Einspeisung
const ladepunkt1Leistung = getStateVal(openWBLadepunkt1DP, 0);
const hausverbrauch = getStateVal(hausverbrauchDP, 0);
const hausverbrauch_lp1 = getStateVal(hausverbrauchDP, 0) + ladepunkt1Leistung;
const hausverbrauchBisher = getStateVal(hausverbrauchBisherDP, 0);
const chargeMode = getStateVal(cargeModeDP, 0);
// eAuto Parameter
const ladenVorrang = getStateVal(ladenVorrangDP, false);
const autoPrioGrenze = getStateVal(autoPrioGrenzeSocDP, 80);
const autoPlugstat = getStateVal(autoPlugstatDP, 0);
let lastAmpere = getState(ampereSetDP).val || minAmp;
let lastRampUpTime = Date.now();
// Zustand: Speicher hat Vorrang
// Eingangsgrößen
const nettoEinspeisung = aktuelleEinspeisung + ladepunkt1Leistung; // Berücksichtigt den Autoverbrauch
if (ladenVorrang === 0) {
if (speicherMinSOC < autoPrioGrenze) {
//setState(ladepunktDP, false);
//visLog(`🔋 SOC ${speicherMinSOC}% < Grenze ${autoPrioGrenze}%, Auto-Laden deaktiviert`, 'warn');
if (nettoEinspeisung > -1500) {
//Auto darf nicht laden
//setState(ladepunktDP, false);
//visLog(`🔋 SOC ${speicherMinSOC}% < Grenze ${autoPrioGrenze}%, Netto-Einspeisung (${nettoEinspeisung} W) < 1400 W → Auto-Laden deaktiviert`, 'warn');
} else {
//Auto darf laden
//setState(ladepunktDP, true);
//visLog(`🔋 SOC ${speicherMinSOC}% < Grenze, aber Netto-Einspeisung (${nettoEinspeisung} W) > 1400 W → Auto-Laden erlaubt`, 'ok');
}
} else {
// SOC hoch genug → Auto darf sowieso laden
setState(ladepunktDP, true);
visLog(`🔋 SOC ${speicherMinSOC}% ≥ Grenze ${autoPrioGrenze}% → Auto-Laden aktiviert`, 'ok');
}
}
// Zustand: Auto hat Vorrang
if (ladenVorrang === 1 && autoPlugstat === 1 && chargeMode != 0) {
const nettoEinspeisung = aktuelleEinspeisung + ladepunkt1Leistung;
const moeglicheLadeleistung = pvLeistung1 - hausverbrauch_lp1;
//visLog(`Mögliche Ladelesitung ${moeglicheLadeleistung}`, 'warn');
//if (nettoEinspeisung <= -minEinspeisungW && autoSOC < autoMaxSOCDP) {
if (moeglicheLadeleistung >= minEinspeisungW && autoSOC < autoMaxSOCDP) {
//Mindestbedingung erfüllt → Ladefreigabe
//setState(ladepunktDP, true);
setLademodus(h1, 0);
setLademodus(h2, 0);
setState(allowChargingDP, 1);
let availableWatt = Math.abs(nettoEinspeisung);
let targetAmp = Math.floor(availableWatt / ampereWatt);
// Begrenzen auf zulässige Ampere
if (targetAmp > maxAmp) targetAmp = maxAmp;
if (targetAmp < minAmp) targetAmp = minAmp;
// RAMP-UP: nur erhöhen wenn genug Leistung + Zeit seit letzter Änderung
if (targetAmp > lastAmpere) {
const neededWatt = (targetAmp * ampereWatt) + rampUpBuffer;
if (availableWatt >= neededWatt && Date.now() - lastRampUpTime > rampUpDelay) {
setState(ampereSetDP, targetAmp);
visLog(`🔼 Ramp-Up: Erhöhe auf ${targetAmp} A bei ${availableWatt} W Überschuss`, 'ok');
lastAmpere = targetAmp;
lastRampUpTime = Date.now();
} else {
visLog(`🔼 Ramp-Up: Noch nicht genug Reserve/Delay für ${targetAmp} A`, 'info');
}
}
// Runterregeln sofort wenn nötig
else if (targetAmp < lastAmpere) {
setState(ampereSetDP, targetAmp);
visLog(`🔽 Reduziere Stromstärke auf ${targetAmp} A (nur ${availableWatt} W verfügbar)`, 'warn');
lastAmpere = targetAmp;
lastRampUpTime = Date.now(); // optional auch hier setzen
} else {
visLog(`⏸ Strom bleibt bei ${lastAmpere} A`, 'debug');
}
} else {
// Zu wenig Einspeisung → Deaktivieren
//setState(ladepunktDP, false);
setState(allowChargingDP, 0);
visLog(`⚠️ Einspeisung zu gering (${Math.round(nettoEinspeisung)} W) → eAuto Laden deaktiviert`, 'warn');
setState(ampereSetDP, minAmp); // evtl. zurücksetzen
lastAmpere = minAmp;
}
}
/*if (ladenVorrang === 1 && autoPlugstat === 1) {
* if (autoSOC < autoMaxSOCDP) {
* visLog(`🚗 Auto-SOC (${autoSOC} %) < Ziel (${autoMaxSOCDP} %) → Speicher wird nicht geladen`, 'warn');
* setLademodus(h1, 0);
* setLademodus(h2, 0);
* setState(ladepunktDP, true); // Auto darf laden
* setState(cargeModeDP, 1); // Modus Min+PV
* return;
* } else {
* visLog(`🚗 Auto voll (SOC ≥ ${autoMaxSOCDP} %) → Speicher darf geladen werden`, 'ok');
* setState(cargeModeDP, 2)
* setState(ladepunktDP, false); // Auto laden beenden
* }
}*/
// PV-Leistung berechnen, abhängig vom SOC von Hyper1
let pvLeistungGesamt = (soc1 >= 100) ? (pvLeistung1 + pvLeistung2) : pvLeistung1;
// Optional: Plausibilitätsprüfung
if (pvLeistung1 < 0 || pvLeistung2 < 0) {
visLog(`⚠️ PV-Leistung negativ: PV1=${pvLeistung1}W, PV2=${pvLeistung2}W`, 'warn');
pvLeistung1 = Math.max(0, pvLeistung1);
pvLeistung2 = Math.max(0, pvLeistung2);
// Nochmal prüfen, ob SOC1 voll ist – um richtige Addition zu machen
pvLeistungGesamt = (soc1 >= 100) ? (pvLeistung1 + pvLeistung2) : pvLeistung1;
}
if (pvLeistungGesamt === 0) {
visLog('⚠️ PV-Leistung = 0W – evtl. Messproblem?', 'warn');
}
// Die Leistung, die direkt zur Deckung des Hausverbrauchs beiträgt:
const pvLeistungRaw = pvLeistungGesamt;
// Forecast & Ziel
const pvRest = getStateVal(pvForecastRestDP, 0) / 1000;
const zielSOC = socMax;
// Preisstatus
const preisErlaubt = getStateVal(priceLoadingDP, false) === true;
const preisGuenstig = isPreisGuenstig();
const preisTeuer = isPreisTeuer();
// Verbrauchsprognose
const stundeJetzt = jetzt.getHours() + jetzt.getMinutes()/60 + jetzt.getSeconds()/3600;
const verbrauchProStunde = stundeJetzt > 1 ? hausverbrauchBisher / stundeJetzt : hausverbrauchBisher;
const gesamtKWh = getKWh(soc1, kapazitaetKWh1) + getKWh(soc2, kapazitaetKWh2);
const fehlendeKapazitaet_kWh = Math.max(0, ((zielSOC/100)*(kapazitaetKWh1+kapazitaetKWh2)) - gesamtKWh);
const prognoseRestverbrauch = Math.max(0, verbrauchProStunde*24 - hausverbrauchBisher);
const pvRestEffektiv = Math.max(0, pvRest - prognoseRestverbrauch);
// Energie aus Akkus
//const leistungAusAkkus = hausverbrauch_lp1 - pvLeistungRaw;
const evuLeistung = getStateVal(evuLeistungDP, 0); // positiv = Netzbezug, negativ = Einspeisung
// Aktuelle Lade-/Entladeleistung der Hyper
const hyper1Leistung = getStateVal(hyper1.inputLimitDP, 0) - getStateVal(hyper1.outputLimitDP, 0); // positiv = Laden, negativ = Entladen
const hyper2Leistung = getStateVal(hyper2.inputLimitDP, 0) - getStateVal(hyper2.outputLimitDP, 0);
// Netto-Leistung Richtung Netz inkl. Akkus
const leistungAusAkkus = evuLeistung - hyper1Leistung - hyper2Leistung;
// Auto-Laden blockiert Entladen
//const autoLaedt = ladepunkt1Leistung > 0;
//const hyperUnterGrenze = soc1 < autoPrioGrenze || soc2 < autoPrioGrenze;
visLog('⚙️ Steuerung gestartet', 'header', true);
visLog(`🏠 Haus: ${hausverbrauch_lp1}W | ☀️ PV: ${pvLeistungRaw}W`);
visLog(`🔋 SOC Ø: ${avgSOC.toFixed(1)}% | Ziel-SOC: ${zielSOC}% | Energie: ${gesamtKWh.toFixed(2)}kWh`);
visLog(`💰 Preis: ${preisTeuer?'teuer':preisGuenstig?'günstig':'neutral'} | PreisLaden: ${preisErlaubt}`);
visLog(`📈 Fehlende Kapazität: ${fehlendeKapazitaet_kWh.toFixed(2)} kWh | 🔮 PV Rest effektiv: ${pvRestEffektiv.toFixed(2)} kWh`);
//visLog(`🔌 EVU: ${Math.round(evuLeistung)}W | Hyper1: ${Math.round(hyper1Leistung)}W | Hyper2: ${Math.round(hyper2Leistung)}W → Aus Akkus: ${Math.round(leistungAusAkkus)}W`);
// Laden erlaubt?
let ladenErlaubt = false;
const minSOC = Math.min(soc1, soc2);
if (leistungAusAkkus < -ladenStartSchwelle && minSOC < zielSOC) {
ladenErlaubt = true;
visLog('☀️ PV-Überschuss → Laden erlaubt', 'ok');
}
// Preis-Lademodus auslesen
let preisLadenModus = getStateVal(preisLadenModusDP, 0); // 0=konservativ, 1=aggressiv
// NEU: Wenn Auto lädt und Preis günstig ist, dann immer aggressiver Lademodus
if (ladepunkt1Leistung > 0 && preisGuenstig) {
preisLadenModus = 1;
visLog('🚗 Auto lädt & Preis günstig → Aggressiver Lademodus aktiviert', 'warn');
}
if (!ladenErlaubt && preisGuenstig && preisErlaubt) {
const stundeJetzt = jetzt.getHours() + jetzt.getMinutes()/60;
const sonnenstandSpät = stundeJetzt >= 17;
const prognoseReichtNichtMehr = fehlendeKapazitaet_kWh > pvRestEffektiv * 0.5;
const socZiemlichLeer = avgSOC < 90;
if (
preisLadenModus === 1 ||
(preisLadenModus === 0 && (
prognoseReichtNichtMehr ||
(sonnenstandSpät && socZiemlichLeer)
))
) {
ladenErlaubt = true;
const art = preisLadenModus === 1
? 'aggressiv (immer bei günstigem Preis)'
: sonnenstandSpät && socZiemlichLeer
? 'konservativ (spät & SOC niedrig)'
: 'konservativ (PV reicht nicht)';
visLog(`💶 Preis-Laden erlaubt (${art})`, 'laden');
}
}
// Entladen erlaubt?
let entladenErlaubt = false;
if (ladepunkt1Leistung <= 500 && leistungAusAkkus > entladenStartSchwelle) {
entladenErlaubt = true;
if (preisTeuer) entladenErlaubt = true;
else if (preisGuenstig) entladenErlaubt = avgSOC > 80;
else entladenErlaubt = avgSOC > 30;
}
// Ziel-Modus festlegen
const aktuellerModus = getStateVal(h1.acModeDP, 0);
let zielModus = 0;
if (ladenErlaubt) zielModus = 1;
else if (entladenErlaubt) zielModus = 2;
// Moduswechsel verzögert bei Laden <-> Entladen
if (!delayWechselErlaubt(aktuellerModus, zielModus, lastSwitchTime, modusDelaySek)) {
const rest = modusDelaySek - ((jetztTs - lastSwitchTime) / 1000);
visLog(`⏳ Moduswechsel ${aktuellerModus}->${zielModus} blockiert (${rest.toFixed(0)}s Verzögerung)`, 'warn');
zielModus = aktuellerModus;
}
// Moduswechsel durchführen
if (zielModus !== aktuellerModus) {
if (zielModus === 1 || zielModus === 2) {
setState(h1.acModeDP, zielModus);
lastMode = zielModus;
lastSwitchTime = jetztTs;
visLog(`🔄 Moduswechsel → ${zielModus === 1 ? 'Laden' : 'Entladen'}`, 'mode');
} else {
visLog(`⏹ Moduswechsel auf 0 (Stopp) → AC-Modus bleibt auf ${aktuellerModus}`, 'stop');
}
}
// Aktion je Modus
if (zielModus === 1) {
// Laden
let ueberschuss = Math.max(0, -leistungAusAkkus - netzPufferWatt);
let ladeLimit = preisGuenstig && preisErlaubt ? maxLadeleistungGesamt : ueberschuss;
if (getStateVal(ladenVorrangDP, false) && ladepunkt1Leistung > 0 && chargeMode === 2) {
ladeLimit = Math.min(getStateVal(ladeleistungReserveDP, maxLadeleistungGesamt), ladeLimit);
visLog('🔌 Lade-Vorrang aktiv & Auto lädt → Ladeleistung begrenzt auf Reserve', 'warn');
}
const ladeleistung = Math.min(ladeLimit, maxLadeleistungGesamt);
let [l1, l2] = verteileLeistungNachSOC(ladeleistung, soc1, soc2, 'laden');
// === Preis-Laden aktiv ===
if (preisErlaubt && preisGuenstig) {
visLog(`💰 Preis-Laden aktiv → Hyper1 & Hyper2 erhalten je 1200 W`, 'laden');
let l1 = 1200;
let l2 = 1200;
}
const l1_begrenzt = Math.min(l1, 1200);
const l2_begrenzt = Math.min(l2, 1200);
//setLademodus(h1, l1_begrenzt);
if (soc1 >= 100) {
visLog(`🔋 ${h1.name} ist voll (100 %) → auf Ausgang (AC-Modus) schalten`, 'entladen');
setState(h1.acModeDP, 2); // AC-Ausgang aktivieren
setState(h1.inputLimitDP, 0); // Kein Laden mehr
setState(h1.outputLimitDP, 0); // Kein Entladen
} else {
setLademodus(h1, l1_begrenzt);
}
setLademodus(h2, l2_begrenzt);
if ((l1_begrenzt > 0 || l2_begrenzt > 0) && (soc1 < 100 || soc2 < 100) && (!preisErlaubt || !preisGuenstig)) {
visLog(`⚡ Mögl. Ladeleistung gesamt: ${Math.round(l1_begrenzt+l2_begrenzt)}W -> ${h1.name}: ${Math.round(l1_begrenzt)}W | ${h2.name}: ${Math.round(l2_begrenzt)}W`, 'laden');
}
//visLog(`⚡ Mögl. Ladeleistung gesamt: ${Math.round(ladeLimit)}W`);
} else if (zielModus === 2) {
// Entladen
const entladeGesamt = Math.max(0, Math.min(leistungAusAkkus, maxLadeleistungGesamt));
const [e1,e2] = verteileLeistungNachSOC(entladeGesamt, soc1, soc2, 'entladen');
if (e1||e2 && (soc1 >= 10 || soc2 >= 10)) {
setEntlademodus(h1, e1);
setEntlademodus(h2, e2);
visLog(`⚡ Entladen: ${h1.name} ${e1}W | ${h2.name} ${e2}W`, 'entladen');
} else {
stopHyper(h1); stopHyper(h2);
visLog('⚠️ Keine Entladeleistung → gestoppt', 'warn');
}
} else {
// Stopp
stopHyper(h1); stopHyper(h2);
visLog('⏹ Speicher STOP', 'stop');
}
}
function getStateVal(id, def = 0) {
try {
const s = getState(id);
return (s && typeof s.val !== 'undefined') ? s.val : def;
} catch(e) {
log(`❌ Fehler beim Lesen von ${id}: ${e}`, 'error');
return def;
}
}
// Trigger
//on({ id: hausverbrauchDP, change: "ne" }, () => steuerung(hyper1, hyper2));
on({ id: evuLeistungDP, change: "ne" }, () => steuerung(hyper1, hyper2));
schedule('*/5 * * * *', () => steuerung(hyper1, hyper2));
// ========================== AmountHours einmal täglich zum Sonnenuntergang berechnen ==========================
let amountHoursHeuteBerechnet = false;
// Trigger bei Änderung der Sonnenuntergangszeit
on({id: '0_userdata.0.Eigene_Variablen.Zeiten.Sonnenuntergang_minus_60', change: 'any'}, () => {
amountHoursHeuteBerechnet = false; // Reset bei neuem Sonnenuntergang
visLog('🔄 AmountHours zurückgesetzt – neuer Sonnenuntergangswert erkannt', 'info');
});
// Prüfen alle 5 Minuten, ob Sonnenuntergang erreicht ist
schedule("*/5 * * * *", () => {
pruefeObSonnenuntergangErreicht();
});