@Hansi1234 sagte in Venus V3.0 API Token:
Kannst du deinen Stand hier aktuell halten? Mein Script ist überhaupt noch nicht ausgereift und mit meiner Shelly Emulation hab ich nur Probleme
@hansi1234
dieses Skript läuft nun seid ein paar Tagen bei mir, und sieht ganz gut aus, werde daran aber noch weiter arbeiten und auch noch PV-Forecast wieder einbinden. Aber aktuell läuft es so.
// ============================================================
// MARSTEK_3 Hauptskript
// v1.2.1
//
// Datenpfad: 0_userdata.0.Akku_PV.Marstek_3.
//
// Changelog:
// v1.2.1 - SOC-basierte Lastgewichtung beim Laden und Entladen (sanft, max 65/35)
// v1.2.0 - Shelly 3EM als Tibber-Fallback; Plug-Ausfall sicher abgefangen;
// Ausfallerkennung deaktiviert wenn kein Plug vorhanden oder ausgefallen;
// Fault-Reset bei Plug-Ausfall damit Speicher nicht dauerhaft blockiert bleibt
// v1.1.8 - Lade-Logik auf real verfuegbaren Ueberschuss mit einstellbarem Prozent-Puffer umgebaut; PMAX-Clamp bleibt erhalten
// v1.1.7 - Echter Ein-Speicher-Fallback in der Leistungsverteilung: verfuegbarer Speicher uebernimmt innerhalb seiner Grenzen das volle Ziel
// v1.1.6 - Alternierender Modus vorbereitet: auto-Teilintervall, Verfuegbarkeitspruefung, Ein-Speicher-Fallback
// v1.1.5 - Interne Leistungsquellen nur vorbereitet, 3529 ohne spekulativen Ersatzwert, sicherer Shelly-Fallback
// v1.1.4 - Konfigurierbare Leistungsquellen, sichere Voll-Ladepause bei vollen Speichern, Default weiter Shelly
// v1.1.3 - SOC-Polling entkoppelt, alternierend, bereichsabhaengig und per Datenpunkten konfigurierbar
// v1.1.2 - Haertung, 3529-SOC via Bat.GetStatus/soc, api_ongrid_28, safeSetState, robuster Start
// v1.1.1 - Laden bei PV-Ueberschuss eingebaut
// v1.0.0 - Erste Version
//
// Logik:
// - Regelintervall aktuell 30 Sekunden (INTERVALL_MS)
// - Optional alternierender Modus mit Auto-Teilintervall nach Anzahl verfuegbarer Speicher
// - Tibber lesen mit Alterscheck und Haltezeit; Fallback auf Shelly 3EM wenn Tibber ausfaellt
// - Formel: plug_real + tibber = neues Ziel
// - 50/50 nur wenn beide Speicher fuer die Richtung verfuegbar sind
// - Ein verfuegbarer Speicher uebernimmt das volle Ziel innerhalb seiner Grenzen
// - SOC-Polling entkoppelt, alternierend und bereichsabhaengig
// - Konfigurierbare Leistungsquellen (Default: Shelly Plug)
// - Voll-Ladepause fuer bereits volle Speicher
// - Plug-Ausfall wird sicher abgefangen: Regelung laeuft weiter ohne Feedback
// ============================================================
const dgram = require('dgram');
const DP = '0_userdata.0.Akku_PV.Marstek_3.';
const TIBBER = 'tibberlink.0.LocalPulse.0.Power';
// -------------------------------------------------------
// Leistungsmessung direkt am Speicher (Shelly Plug, Tasmota, o.ae.)
// Tragt hier den ioBroker-Datenpunkt eures Leistungsmessers ein.
// Der Datenpunkt muss die aktuelle Wattleistung des Speichers liefern.
//
// WARUM ist eine Leistungsmessung am Speicher sinnvoll?
// Das Skript weiss dann was der Speicher wirklich tut - nicht nur was
// es ihm befohlen hat. Das ermoeglicht automatische Fehlerkorrektur
// und erkennt wenn ein Speicher nicht reagiert.
//
// Kein Leistungsmesser vorhanden? Einfach '' (leere Anführungszeichen) eintragen.
// Das Skript regelt dann trotzdem - aber ohne Rueckmeldung vom Speicher.
// Faellt ein eingetragener Messer aus, schaltet das Skript automatisch
// in diesen Modus und wieder zurueck sobald Werte zurueckkommen.
const PLUG_28 = 'shelly.1.shellyplugsg3#3528#1.Relay0.Power'; // <-- Datenpunkt eintragen oder '' wenn keiner vorhanden
const PLUG_29 = 'shelly.1.shellyplugsg3#3529#1.Relay0.Power'; // <-- Datenpunkt eintragen oder '' wenn keiner vorhanden
// -------------------------------------------------------
// Netzstrommessung fuer das gesamte Haus (Tibber-Fallback)
// Dieser Datenpunkt wird genutzt wenn der Tibber-Adapter ausfaellt.
// Muss den aktuellen Netzbezug/-einspeisung in Watt liefern.
// Positiv = Netzbezug, Negativ = Einspeisung.
const SHELLY_3EM_TOTAL = 'alias.0.Haus.Strom.Strom-1.Shelly3EM-InstantPower';
const IP_28 = '192.168.145.179';
const IP_29 = '192.168.145.155';
const PORT = 30000;
const INTERVALL_MS = 30000;
const MAX_ENTLADEN_W = 800;
const MAX_LADEN_W = 1250;
const MIN_W = 50;
const EV_W = 10;
const TIBBER_MAX_MS = 25000;
const HALTE_MS = 240000;
const SOC_MIN_PCT = 12;
const SOC_NORMAL_MIN = 25;
const SOC_NORMAL_MAX = 85;
const SOC_POLL_NORMAL_S = 180;
const SOC_POLL_RAND_S = 60;
const API_ONGRID_FACTOR_28 = 1;
const FULL_SOC_PCT = 100;
const FULL_POWER_MAX_W = 15;
const ALTERNIEREND_AKTIV = false;
const AUSFALLERKENNUNG_AKTIV = true;
const AUSFALL_MIN_SOLL_W = 150;
const AUSFALL_MIN_REAL_RATIO = 0.35;
const AUSFALL_ZYKLEN = 3;
const AUSFALL_COOLDOWN_S = 120;
const AUSFALL_PROBE_FAKTOR_PCT = 30;
let udpClient = null;
let ivMain = null;
let currentLoopMs = INTERVALL_MS;
let lastTibberGutTs = 0;
let lastTibberGutW = 0;
let soc28 = 50;
let soc29 = 50;
let lastSocTs28 = 0;
let lastSocTs29 = 0;
let nextSocTarget = '28';
let fullChargeLock28 = false;
let fullChargeLock29 = false;
let alternatingIndex = 0;
let lastCmd28 = 0;
let lastCmd29 = 0;
let lastTarget28 = 0;
let lastTarget29 = 0;
let fault28 = false;
let fault29 = false;
let failCount28 = 0;
let failCount29 = 0;
let cooldownUntil28 = 0;
let cooldownUntil29 = 0;
let probe28 = false;
let probe29 = false;
// ------------------------------------------------------------
// Datenpunkte anlegen
// ------------------------------------------------------------
function initDP() {
createState(DP + 'einstellungen.aktiv', true,
{ name: 'Steuerung aktiv', type: 'boolean', read: true, write: true });
createState(DP + 'einstellungen.max_entladen_w_pro_speicher', MAX_ENTLADEN_W,
{ name: 'Max W pro Speicher (Entladen)', type: 'number', unit: 'W', read: true, write: true });
createState(DP + 'einstellungen.max_laden_w_pro_speicher', MAX_LADEN_W,
{ name: 'Max W pro Speicher (Laden)', type: 'number', unit: 'W', read: true, write: true });
createState(DP + 'einstellungen.min_w', MIN_W,
{ name: 'Min W (unter diesem Wert -> 0W)', type: 'number', unit: 'W', read: true, write: true });
createState(DP + 'einstellungen.laden_drossel_soc', 90,
{ name: 'Laden drosseln ab SOC [%]', type: 'number', unit: '%', read: true, write: true });
createState(DP + 'einstellungen.laden_drossel_faktor', 50,
{ name: 'Drossel-Faktor [%]', type: 'number', unit: '%', read: true, write: true });
createState(DP + 'einstellungen.laden_drossel_max_w', 800,
{ name: 'Drossel max W pro Speicher', type: 'number', unit: 'W', read: true, write: true });
createState(DP + 'einstellungen.lade_puffer_pct', 20,
{ name: 'Lade-Puffer [%] vom verfuegbaren Ueberschuss', type: 'number', unit: '%', read: true, write: true });
createState(DP + 'einstellungen.soc_normal_min_pct', SOC_NORMAL_MIN,
{ name: 'SOC Normalbereich Minimum [%]', type: 'number', unit: '%', read: true, write: true });
createState(DP + 'einstellungen.soc_normal_max_pct', SOC_NORMAL_MAX,
{ name: 'SOC Normalbereich Maximum [%]', type: 'number', unit: '%', read: true, write: true });
createState(DP + 'einstellungen.soc_poll_normal_s', SOC_POLL_NORMAL_S,
{ name: 'SOC Polling Normalbereich [s]', type: 'number', unit: 's', read: true, write: true });
createState(DP + 'einstellungen.soc_poll_rand_s', SOC_POLL_RAND_S,
{ name: 'SOC Polling Randbereich [s]', type: 'number', unit: 's', read: true, write: true });
createState(DP + 'einstellungen.nutze_shelly_28', true,
{ name: 'Nutze Shelly 3528', type: 'boolean', read: true, write: true });
createState(DP + 'einstellungen.nutze_shelly_29', true,
{ name: 'Nutze Shelly 3529', type: 'boolean', read: true, write: true });
createState(DP + 'einstellungen.api_ongrid_faktor_28', API_ONGRID_FACTOR_28,
{ name: 'api_ongrid Faktor 3528', type: 'number', read: true, write: true });
createState(DP + 'einstellungen.voll_ladepause_aktiv', true,
{ name: 'Voll-Ladepause aktiv', type: 'boolean', read: true, write: true });
createState(DP + 'einstellungen.voll_soc_pct', FULL_SOC_PCT,
{ name: 'Voll ab SOC [%]', type: 'number', unit: '%', read: true, write: true });
createState(DP + 'einstellungen.voll_power_max_w', FULL_POWER_MAX_W,
{ name: 'Voll wenn Realleistung kleiner [W]', type: 'number', unit: 'W', read: true, write: true });
createState(DP + 'einstellungen.alternierend_aktiv', ALTERNIEREND_AKTIV,
{ name: 'Alternierender Modus aktiv', type: 'boolean', read: true, write: true });
createState(DP + 'einstellungen.ausfallerkennung_aktiv', AUSFALLERKENNUNG_AKTIV,
{ name: 'Ausfallerkennung aktiv', type: 'boolean', read: true, write: true });
createState(DP + 'einstellungen.ausfall_min_soll_w', AUSFALL_MIN_SOLL_W,
{ name: 'Ausfall min Soll [W]', type: 'number', unit: 'W', read: true, write: true });
createState(DP + 'einstellungen.ausfall_min_real_ratio', AUSFALL_MIN_REAL_RATIO,
{ name: 'Ausfall min Realverhaeltnis', type: 'number', read: true, write: true });
createState(DP + 'einstellungen.ausfall_zyklen', AUSFALL_ZYKLEN,
{ name: 'Ausfall Zyklen', type: 'number', read: true, write: true });
createState(DP + 'einstellungen.ausfall_cooldown_s', AUSFALL_COOLDOWN_S,
{ name: 'Ausfall Cooldown [s]', type: 'number', unit: 's', read: true, write: true });
createState(DP + 'einstellungen.ausfall_probe_faktor_pct', AUSFALL_PROBE_FAKTOR_PCT,
{ name: 'Ausfall Probe Faktor [%]', type: 'number', unit: '%', read: true, write: true });
createState(DP + 'status.tibber_w', 0,
{ name: 'Tibber aktuell', type: 'number', unit: 'W', read: true, write: true });
createState(DP + 'status.plug_28_w', 0,
{ name: 'Plug 3528', type: 'number', unit: 'W', read: true, write: true });
createState(DP + 'status.plug_29_w', 0,
{ name: 'Plug 3529', type: 'number', unit: 'W', read: true, write: true });
createState(DP + 'status.ziel_gesamt_w', 0,
{ name: 'Ziel gesamt', type: 'number', unit: 'W', read: true, write: true });
createState(DP + 'status.gesendet_28_w', 0,
{ name: 'Gesendet 3528', type: 'number', unit: 'W', read: true, write: true });
createState(DP + 'status.gesendet_29_w', 0,
{ name: 'Gesendet 3529', type: 'number', unit: 'W', read: true, write: true });
createState(DP + 'status.letzte_regelung', '',
{ name: 'Letzte Regelung', type: 'string', read: true, write: true });
createState(DP + 'status.soc_28', 0,
{ name: 'SOC 3528', type: 'number', unit: '%', read: true, write: true });
createState(DP + 'status.soc_29', 0,
{ name: 'SOC 3529', type: 'number', unit: '%', read: true, write: true });
createState(DP + 'status.tibber_quelle', '',
{ name: 'Tibber Quelle', type: 'string', read: true, write: true });
createState(DP + 'status.api_ongrid_28', 0,
{ name: 'Marstek intern 3528 [W]', type: 'number', unit: 'W', read: true, write: true });
createState(DP + 'status.api_real_29_test', 0,
{ name: 'Marstek intern 3529 Test [W]', type: 'number', unit: 'W', read: true, write: true });
createState(DP + 'status.soc_alter_28_s', 0,
{ name: 'SOC Alter 3528 [s]', type: 'number', unit: 's', read: true, write: true });
createState(DP + 'status.soc_alter_29_s', 0,
{ name: 'SOC Alter 3529 [s]', type: 'number', unit: 's', read: true, write: true });
createState(DP + 'status.soc_poll_naechst', '28',
{ name: 'Naechster SOC-Poll', type: 'string', read: true, write: true });
createState(DP + 'status.leistungsquelle_28', 'shelly',
{ name: 'Leistungsquelle 3528', type: 'string', read: true, write: true });
createState(DP + 'status.leistungsquelle_29', 'shelly',
{ name: 'Leistungsquelle 3529', type: 'string', read: true, write: true });
createState(DP + 'status.real_28_w', 0,
{ name: 'Realleistung 3528', type: 'number', unit: 'W', read: true, write: true });
createState(DP + 'status.real_29_w', 0,
{ name: 'Realleistung 3529', type: 'number', unit: 'W', read: true, write: true });
createState(DP + 'status.modus_28', 'Standby',
{ name: 'Modus 3528', desc: 'Realer Betriebszustand von Speicher 3528 fuer VIS. > +10W = Laden, < -10W = Entladen, sonst Standby.', type: 'string', read: true, write: true });
createState(DP + 'status.modus_29', 'Standby',
{ name: 'Modus 3529', desc: 'Realer Betriebszustand von Speicher 3529 fuer VIS. > +10W = Laden, < -10W = Entladen, sonst Standby.', type: 'string', read: true, write: true });
createState(DP + 'status.modus_gesamt', 'Standby',
{ name: 'Modus Gesamt', desc: 'Realer Gesamtzustand beider Speicher fuer VIS. > +10W = Laden, < -10W = Entladen, sonst Standby.', type: 'string', read: true, write: true });
createState(DP + 'status.voll_ladepause_28', false,
{ name: 'Voll-Ladepause 3528', type: 'boolean', read: true, write: true });
createState(DP + 'status.voll_ladepause_29', false,
{ name: 'Voll-Ladepause 3529', type: 'boolean', read: true, write: true });
createState(DP + 'status.verfuegbare_speicher_laden', 0,
{ name: 'Verfuegbare Speicher Laden', type: 'number', read: true, write: true });
createState(DP + 'status.verfuegbare_speicher_entladen', 0,
{ name: 'Verfuegbare Speicher Entladen', type: 'number', read: true, write: true });
createState(DP + 'status.teilintervall_ms', INTERVALL_MS,
{ name: 'Teilintervall [ms]', type: 'number', unit: 'ms', read: true, write: true });
createState(DP + 'status.alternierender_speicher', '',
{ name: 'Aktiver Speicher im Wechselmodus', type: 'string', read: true, write: true });
createState(DP + 'status.fehler_28_aktiv', false,
{ name: 'Fehler 3528 aktiv', type: 'boolean', read: true, write: true });
createState(DP + 'status.fehler_29_aktiv', false,
{ name: 'Fehler 3529 aktiv', type: 'boolean', read: true, write: true });
createState(DP + 'status.probe_28_aktiv', false,
{ name: 'Probe 3528 aktiv', type: 'boolean', read: true, write: true });
createState(DP + 'status.probe_29_aktiv', false,
{ name: 'Probe 3529 aktiv', type: 'boolean', read: true, write: true });
}
function safeSetState(path, value, ack) {
try {
if (!existsState(path)) {
log('State fehlt bei setState: ' + path, 'warn');
return;
}
setState(path, value, ack);
} catch(e) {
log('setState Fehler [' + path + ']: ' + e.message, 'warn');
}
}
// ------------------------------------------------------------
// UDP
// ------------------------------------------------------------
function initUDP() {
udpClient = dgram.createSocket('udp4');
udpClient.on('error', function(e) { log('UDP Fehler: ' + e.message, 'warn'); });
initSOCListener();
udpClient.bind(30000, function() {
log('✅ UDP bereit (Port 30000)');
});
}
function querySOC(ip) {
if (!udpClient) return;
const method = (ip === IP_28) ? 'ES.GetStatus' : 'Bat.GetStatus';
udpClient.send(Buffer.from(JSON.stringify(
{ id: 1, method: method, params: { id: 0 } }
)), PORT, ip, function() {});
}
function initSOCListener() {
udpClient.on('message', function(msg, rinfo) {
try {
const data = JSON.parse(msg.toString());
if (!data || !data.result) return;
const r = data.result;
if (r.ongrid_power !== undefined && rinfo.address === IP_28) {
safeSetState(DP + 'status.api_ongrid_28', Math.round(parseFloat(r.ongrid_power)), true);
}
let socVal = null;
if (r.bat_soc !== undefined) socVal = parseFloat(r.bat_soc);
else if (r.soc !== undefined) socVal = parseFloat(r.soc);
if (socVal !== null && !isNaN(socVal)) {
if (rinfo.address === IP_28) {
soc28 = socVal;
lastSocTs28 = Date.now();
safeSetState(DP + 'status.soc_28', Math.round(socVal), true);
} else if (rinfo.address === IP_29) {
soc29 = socVal;
lastSocTs29 = Date.now();
safeSetState(DP + 'status.soc_29', Math.round(socVal), true);
}
}
} catch(e) {
log('UDP Listener Fehler von ' + (rinfo && rinfo.address ? rinfo.address : 'unbekannt') + ': ' + e.message, 'warn');
}
});
}
function sendManual(ip, watt) {
if (!udpClient) return;
const power = Math.round(watt);
const msg = JSON.stringify({
id: 1,
method: 'ES.SetMode',
params: {
id: 0,
config: {
mode: 'Manual',
manual_cfg: {
time_num: 1,
start_time: '00:00:00',
end_time: '23:59:59',
week_set: 127,
power: power,
enable: 1
}
}
}
});
udpClient.send(Buffer.from(msg), PORT, ip, function(err) {
if (err) log('UDP Send Fehler (' + ip + '): ' + err.message, 'warn');
});
}
// ------------------------------------------------------------
// Hilfsfunktionen
// ------------------------------------------------------------
function safeVal(path, fallback) {
try {
const s = getState(path);
if (!s || s.val === null || s.val === undefined) return fallback;
const n = parseFloat(s.val);
return isNaN(n) ? fallback : n;
} catch(e) { return fallback; }
}
function safeBool(path, fallback) {
try {
const s = getState(path);
if (!s || s.val === null) return fallback;
return !!s.val;
} catch(e) { return fallback; }
}
// Liest den summierten Hausstromwert vom Shelly 3EM.
// Wird als Fallback genutzt wenn der Tibber-Adapter ausfaellt.
// Gibt null zurueck wenn kein gueltiger Wert verfuegbar ist.
function getShelly3EMTotal() {
try {
const s = getState(SHELLY_3EM_TOTAL);
if (!s || s.val === null || s.val === undefined) return null;
const v = parseFloat(s.val);
return isNaN(v) ? null : v;
} catch(e) { return null; }
}
function getStateTs(path, fallback) {
try {
const s = getState(path);
if (!s || s.ts === null || s.ts === undefined) return fallback;
const ts = Number(s.ts);
return isNaN(ts) ? fallback : ts;
} catch(e) { return fallback; }
}
function getSocPollIntervalMs(socVal) {
const normalMin = safeVal(DP + 'einstellungen.soc_normal_min_pct', SOC_NORMAL_MIN);
const normalMax = safeVal(DP + 'einstellungen.soc_normal_max_pct', SOC_NORMAL_MAX);
const normalMs = safeVal(DP + 'einstellungen.soc_poll_normal_s', SOC_POLL_NORMAL_S) * 1000;
const randMs = safeVal(DP + 'einstellungen.soc_poll_rand_s', SOC_POLL_RAND_S) * 1000;
return (socVal >= normalMin && socVal <= normalMax) ? normalMs : randMs;
}
function updateSocAgeStatus() {
const now = Date.now();
const age28 = lastSocTs28 > 0 ? Math.max(0, Math.round((now - lastSocTs28) / 1000)) : 0;
const age29 = lastSocTs29 > 0 ? Math.max(0, Math.round((now - lastSocTs29) / 1000)) : 0;
safeSetState(DP + 'status.soc_alter_28_s', age28, true);
safeSetState(DP + 'status.soc_alter_29_s', age29, true);
safeSetState(DP + 'status.soc_poll_naechst', nextSocTarget, true);
}
function maybePollSOC() {
const now = Date.now();
const order = nextSocTarget === '28' ? ['28', '29'] : ['29', '28'];
for (let i = 0; i < order.length; i++) {
const target = order[i];
const ip = target === '28' ? IP_28 : IP_29;
const socVal = target === '28' ? soc28 : soc29;
const lastTs = target === '28' ? lastSocTs28 : lastSocTs29;
const intervalMs = getSocPollIntervalMs(socVal);
const ageMs = lastTs > 0 ? (now - lastTs) : Number.MAX_SAFE_INTEGER;
if (ageMs >= intervalMs) {
querySOC(ip);
nextSocTarget = target === '28' ? '29' : '28';
safeSetState(DP + 'status.soc_poll_naechst', nextSocTarget, true);
return;
}
}
}
// Liest die tatsaechliche Leistung von Speicher 3528.
// Wenn kein Plug eingetragen (PLUG_28 = '') oder der Plug gerade
// keine gueltigen Werte liefert, wird 'keine_messung' zurueckgegeben.
// Das Skript regelt dann weiter - aber ohne Feedback vom Speicher.
function getRealPower28() {
if (!PLUG_28) {
return { value: 0, source: 'keine_messung' };
}
const useShelly = safeBool(DP + 'einstellungen.nutze_shelly_28', true);
if (useShelly) {
try {
const s = getState(PLUG_28);
if (!s || s.val === null || s.val === undefined) return { value: 0, source: 'keine_messung' };
const v = parseFloat(s.val);
if (isNaN(v)) return { value: 0, source: 'keine_messung' };
return { value: v, source: 'shelly' };
} catch(e) { return { value: 0, source: 'keine_messung' }; }
}
const factor = safeVal(DP + 'einstellungen.api_ongrid_faktor_28', API_ONGRID_FACTOR_28);
return { value: safeVal(DP + 'status.api_ongrid_28', 0) * factor, source: 'api_ongrid_28' };
}
// Liest die tatsaechliche Leistung von Speicher 3529.
// Gleiches Verhalten wie getRealPower28 - siehe Kommentar dort.
function getRealPower29() {
if (!PLUG_29) {
return { value: 0, source: 'keine_messung' };
}
const useShelly = safeBool(DP + 'einstellungen.nutze_shelly_29', true);
if (useShelly) {
try {
const s = getState(PLUG_29);
if (!s || s.val === null || s.val === undefined) return { value: 0, source: 'keine_messung' };
const v = parseFloat(s.val);
if (isNaN(v)) return { value: 0, source: 'keine_messung' };
return { value: v, source: 'shelly' };
} catch(e) { return { value: 0, source: 'keine_messung' }; }
}
return { value: 0, source: 'keine_messung' };
}
function updateFullChargeLock(which, socVal, realPower, currentCmd, isChargingMode) {
const active = safeBool(DP + 'einstellungen.voll_ladepause_aktiv', true);
const fullSoc = safeVal(DP + 'einstellungen.voll_soc_pct', FULL_SOC_PCT);
const powerMax = safeVal(DP + 'einstellungen.voll_power_max_w', FULL_POWER_MAX_W);
const wasDischarging = realPower < -EV_W;
const ineffectiveCharge = Math.abs(realPower) <= powerMax;
if (which === '28') {
if (fullChargeLock28 && (socVal < fullSoc || wasDischarging)) fullChargeLock28 = false;
if (active && isChargingMode && currentCmd < -MIN_W && socVal >= fullSoc && ineffectiveCharge) fullChargeLock28 = true;
safeSetState(DP + 'status.voll_ladepause_28', fullChargeLock28, true);
return fullChargeLock28;
}
if (fullChargeLock29 && (socVal < fullSoc || wasDischarging)) fullChargeLock29 = false;
if (active && isChargingMode && currentCmd < -MIN_W && socVal >= fullSoc && ineffectiveCharge) fullChargeLock29 = true;
safeSetState(DP + 'status.voll_ladepause_29', fullChargeLock29, true);
return fullChargeLock29;
}
function isAvailableForCharge(which) {
if (isFaultBlocked(which)) return false;
return which === '28' ? !fullChargeLock28 : !fullChargeLock29;
}
function isAvailableForDischarge(which) {
if (isFaultBlocked(which)) return false;
return which === '28' ? soc28 >= SOC_MIN_PCT : soc29 >= SOC_MIN_PCT;
}
function getAvailableTargets(modus) {
const targets = [];
if (modus === 'Laden') {
if (isAvailableForCharge('28')) targets.push('28');
if (isAvailableForCharge('29')) targets.push('29');
safeSetState(DP + 'status.verfuegbare_speicher_laden', targets.length, true);
} else if (modus === 'Entladen') {
if (isAvailableForDischarge('28')) targets.push('28');
if (isAvailableForDischarge('29')) targets.push('29');
safeSetState(DP + 'status.verfuegbare_speicher_entladen', targets.length, true);
} else {
safeSetState(DP + 'status.verfuegbare_speicher_laden', 0, true);
safeSetState(DP + 'status.verfuegbare_speicher_entladen', 0, true);
}
return targets;
}
function getTeilintervallMs(count) {
const n = Math.max(1, count);
return Math.max(1000, Math.round(INTERVALL_MS / n));
}
function distributeDischargePower(totalW, availableTargets, maxEntladenW) {
const result = { '28': 0, '29': 0 };
const count = Math.max(1, availableTargets.length);
const totalClamped = Math.max(0, Math.min(totalW, maxEntladenW * count));
if (availableTargets.length <= 0) return result;
if (availableTargets.length === 1) {
result[availableTargets[0]] = Math.min(Math.round(totalClamped), maxEntladenW);
return result;
}
// SOC-Gewichtung: wer mehr Ladung hat entlaedt mehr (sanft, max 65/35)
// Bei gleichem SOC: exakt 50/50
const socDiff = soc28 - soc29; // positiv = 28 hat mehr SOC
const shift = Math.max(-0.15, Math.min(0.15, socDiff / 200));
const w28 = Math.round(totalClamped * (0.5 + shift));
const w29 = Math.round(totalClamped * (0.5 - shift));
result['28'] = Math.min(w28, maxEntladenW);
result['29'] = Math.min(w29, maxEntladenW);
return result;
}
function distributeChargePower(totalW, availableTargets, maxLadenW) {
const result = { '28': 0, '29': 0 };
const count = Math.max(1, availableTargets.length);
const totalClamped = Math.min(0, Math.max(totalW, -(maxLadenW * count)));
if (availableTargets.length <= 0) return result;
if (availableTargets.length === 1) {
result[availableTargets[0]] = Math.max(Math.round(totalClamped), -maxLadenW);
return result;
}
// SOC-Gewichtung: wer weniger Ladung hat laedt mehr (sanft, max 65/35)
// Bei gleichem SOC: exakt 50/50
const socDiff = soc28 - soc29; // positiv = 28 hat mehr SOC → 28 bekommt weniger Ladung
const shift = Math.max(-0.15, Math.min(0.15, socDiff / 200));
const w28 = Math.round(totalClamped * (0.5 - shift));
const w29 = Math.round(totalClamped * (0.5 + shift));
result['28'] = Math.max(w28, -maxLadenW);
result['29'] = Math.max(w29, -maxLadenW);
return result;
}
function getEffectiveRealFromLastCmd(sentW, realW) {
if (sentW > 0) return Math.max(0, -realW);
if (sentW < 0) return Math.max(0, realW);
return 0;
}
function updateFaultStatusDPs() {
safeSetState(DP + 'status.fehler_28_aktiv', fault28, true);
safeSetState(DP + 'status.fehler_29_aktiv', fault29, true);
safeSetState(DP + 'status.probe_28_aktiv', probe28, true);
safeSetState(DP + 'status.probe_29_aktiv', probe29, true);
}
// Prueft ob ein Speicher auf Befehle reagiert.
// Wird nur aufgerufen wenn eine Leistungsmessung vorhanden ist (source !== 'keine_messung').
// Wenn kein Plug vorhanden oder ausgefallen: wird diese Funktion nicht aufgerufen
// und ein bestehender Fault-Zustand wird zurueckgesetzt damit der Speicher
// nicht dauerhaft blockiert bleibt.
function updateDeviceFaultState(which, realW) {
if (!safeBool(DP + 'einstellungen.ausfallerkennung_aktiv', AUSFALLERKENNUNG_AKTIV)) {
return;
}
const minSoll = safeVal(DP + 'einstellungen.ausfall_min_soll_w', AUSFALL_MIN_SOLL_W);
const minRatio = safeVal(DP + 'einstellungen.ausfall_min_real_ratio', AUSFALL_MIN_REAL_RATIO);
const failCycles = Math.max(1, Math.round(safeVal(DP + 'einstellungen.ausfall_zyklen', AUSFALL_ZYKLEN)));
const cooldownMs = Math.max(1000, Math.round(safeVal(DP + 'einstellungen.ausfall_cooldown_s', AUSFALL_COOLDOWN_S) * 1000));
const now = Date.now();
let lastCmd = which === '28' ? lastCmd28 : lastCmd29;
let fault = which === '28' ? fault28 : fault29;
let failCount = which === '28' ? failCount28 : failCount29;
let cooldownUntil = which === '28' ? cooldownUntil28 : cooldownUntil29;
let probe = which === '28' ? probe28 : probe29;
if (fault && now >= cooldownUntil) {
probe = true;
}
if (Math.abs(lastCmd) < minSoll) {
failCount = 0;
} else {
const effectiveReal = getEffectiveRealFromLastCmd(lastCmd, realW);
const ratio = effectiveReal / Math.max(1, Math.abs(lastCmd));
if (fault && probe) {
if (ratio >= minRatio) {
fault = false;
probe = false;
failCount = 0;
cooldownUntil = 0;
log('✅ Speicher ' + which + ' reagiert wieder - Fehlerstatus aufgehoben');
} else {
fault = true;
probe = false;
cooldownUntil = now + cooldownMs;
failCount = failCycles;
log('⚠️ Speicher ' + which + ' Probe fehlgeschlagen - erneut auf 0W und Cooldown');
}
} else if (!fault) {
if (ratio < minRatio) {
failCount++;
if (failCount >= failCycles) {
fault = true;
probe = false;
cooldownUntil = now + cooldownMs;
failCount = failCycles;
log('⚠️ Speicher ' + which + ' liefert wiederholt nicht wie angefordert - temporär ausgeblendet');
}
} else {
failCount = 0;
}
}
}
if (which === '28') {
fault28 = fault;
failCount28 = failCount;
cooldownUntil28 = cooldownUntil;
probe28 = probe;
} else {
fault29 = fault;
failCount29 = failCount;
cooldownUntil29 = cooldownUntil;
probe29 = probe;
}
updateFaultStatusDPs();
}
// Setzt den Fault-Zustand eines Speichers zurueck.
// Wird aufgerufen wenn der Plug ausgefallen ist oder nie vorhanden war,
// damit der Speicher nicht dauerhaft auf 0W eingefroren bleibt.
function clearFaultState(which) {
if (which === '28') {
if (fault28) {
fault28 = false;
failCount28 = 0;
cooldownUntil28 = 0;
probe28 = false;
log('ℹ️ Speicher 28: Plug nicht verfuegbar - Fehlerstatus zurueckgesetzt, Regelung ohne Feedback');
}
} else {
if (fault29) {
fault29 = false;
failCount29 = 0;
cooldownUntil29 = 0;
probe29 = false;
log('ℹ️ Speicher 29: Plug nicht verfuegbar - Fehlerstatus zurueckgesetzt, Regelung ohne Feedback');
}
}
updateFaultStatusDPs();
}
function isFaultBlocked(which) {
const now = Date.now();
if (which === '28') return fault28 && now < cooldownUntil28;
return fault29 && now < cooldownUntil29;
}
function isProbeActive(which) {
if (which === '28') return probe28;
return probe29;
}
function applyFaultAndProbe(modus, w28, w29, maxEntladenW, maxLadenW) {
let target28 = w28;
let target29 = w29;
const probeFactor = Math.max(0, Math.min(1, safeVal(DP + 'einstellungen.ausfall_probe_faktor_pct', AUSFALL_PROBE_FAKTOR_PCT) / 100));
// Nie beide Speicher gleichzeitig in Fault/Probe-Reduktion laufen lassen.
// Sonst kann die Gesamtleistung kuenstlich stark einbrechen.
if ((probe28 || isFaultBlocked('28')) && (probe29 || isFaultBlocked('29'))) {
return { '28': Math.round(w28), '29': Math.round(w29) };
}
if (isFaultBlocked('28')) target28 = 0;
if (isFaultBlocked('29')) target29 = 0;
if (isProbeActive('28')) target28 = Math.round(target28 * probeFactor);
if (isProbeActive('29')) target29 = Math.round(target29 * probeFactor);
if (modus === 'Entladen') {
const desiredTotal = Math.max(0, w28 + w29);
let currentTotal = Math.max(0, target28 + target29);
let rest = desiredTotal - currentTotal;
if (rest > 0) {
if (!isFaultBlocked('28') && !isProbeActive('28')) {
const free28 = Math.max(0, maxEntladenW - target28);
const add28 = Math.min(rest, free28);
target28 += add28;
rest -= add28;
}
if (rest > 0 && !isFaultBlocked('29') && !isProbeActive('29')) {
const free29 = Math.max(0, maxEntladenW - target29);
const add29 = Math.min(rest, free29);
target29 += add29;
rest -= add29;
}
}
} else if (modus === 'Laden') {
const desiredTotal = Math.min(0, w28 + w29);
let currentTotal = Math.min(0, target28 + target29);
let rest = desiredTotal - currentTotal;
if (rest < 0) {
if (!isFaultBlocked('28') && !isProbeActive('28')) {
const free28 = Math.max(0, maxLadenW - Math.abs(target28));
const add28 = Math.min(Math.abs(rest), free28);
target28 -= add28;
rest += add28;
}
if (rest < 0 && !isFaultBlocked('29') && !isProbeActive('29')) {
const free29 = Math.max(0, maxLadenW - Math.abs(target29));
const add29 = Math.min(Math.abs(rest), free29);
target29 -= add29;
rest += add29;
}
}
}
return { '28': Math.round(target28), '29': Math.round(target29) };
}
function sendPairCommands(w28, w29) {
sendManual(IP_28, w28);
sendManual(IP_29, w29);
lastCmd28 = w28;
lastCmd29 = w29;
safeSetState(DP + 'status.gesendet_28_w', w28, true);
safeSetState(DP + 'status.gesendet_29_w', w29, true);
}
function sendAlternatingCommand(activeTarget, target28, target29) {
let send28 = lastCmd28;
let send29 = lastCmd29;
if (activeTarget === '28') send28 = target28;
if (activeTarget === '29') send29 = target29;
sendManual(IP_28, send28);
sendManual(IP_29, send29);
lastCmd28 = send28;
lastCmd29 = send29;
safeSetState(DP + 'status.gesendet_28_w', send28, true);
safeSetState(DP + 'status.gesendet_29_w', send29, true);
safeSetState(DP + 'status.alternierender_speicher', activeTarget, true);
}
function applyLoopInterval(newMs) {
const targetMs = Math.max(1000, Math.round(newMs));
if (ivMain && currentLoopMs === targetMs) return;
if (ivMain) clearInterval(ivMain);
ivMain = setInterval(regeln, targetMs);
currentLoopMs = targetMs;
log('⏱️ Regelintervall aktiv: ' + targetMs + 'ms');
}
// ------------------------------------------------------------
// Hauptregelung
// ------------------------------------------------------------
function regeln() {
if (!safeBool(DP + 'einstellungen.aktiv', true)) {
log('⏸️ Steuerung deaktiviert');
return;
}
const maxEntladenW = safeVal(DP + 'einstellungen.max_entladen_w_pro_speicher', MAX_ENTLADEN_W);
const maxLadenW = safeVal(DP + 'einstellungen.max_laden_w_pro_speicher', MAX_LADEN_W);
const minW = safeVal(DP + 'einstellungen.min_w', MIN_W);
maybePollSOC();
soc28 = safeVal(DP + 'status.soc_28', soc28);
soc29 = safeVal(DP + 'status.soc_29', soc29);
if (lastSocTs28 <= 0) lastSocTs28 = getStateTs(DP + 'status.soc_28', 0);
if (lastSocTs29 <= 0) lastSocTs29 = getStateTs(DP + 'status.soc_29', 0);
updateSocAgeStatus();
let tibber;
let tibberQuelle;
try {
const tibberState = getState(TIBBER);
const tibberAlter = tibberState ? (Date.now() - Number(tibberState.ts)) : 999999;
if (tibberAlter <= TIBBER_MAX_MS) {
tibber = parseFloat(tibberState.val) || 0;
lastTibberGutTs = Date.now();
lastTibberGutW = tibber;
tibberQuelle = 'live';
} else if ((Date.now() - lastTibberGutTs) <= HALTE_MS) {
tibber = lastTibberGutW;
tibberQuelle = 'gehalten (' + Math.round((Date.now() - lastTibberGutTs) / 1000) + 's)';
log('⏳ Tibber veraltet - halte letzten Wert: ' + Math.round(tibber) + 'W', 'warn');
} else {
// Tibber ausgefallen und Haltezeit abgelaufen.
// Fallback auf Shelly 3EM als zweite unabhaengige Netzstrommessung.
const shellyFallback = getShelly3EMTotal();
if (shellyFallback !== null) {
tibber = shellyFallback;
tibberQuelle = 'shelly3em_fallback';
log('⚠️ Tibber ausgefallen - nutze Shelly 3EM: ' + Math.round(tibber) + 'W', 'warn');
} else {
// Beide Quellen ausgefallen - sicherster Zustand ist 0W.
log('❌ Tibber UND Shelly 3EM ausgefallen - Standby', 'warn');
sendPairCommands(0, 0);
safeSetState(DP + 'status.tibber_quelle', 'AUSGEFALLEN', true);
safeSetState(DP + 'status.letzte_regelung', new Date().toLocaleTimeString() + ' | Standby: beide Quellen ausgefallen', true);
return;
}
}
} catch(e) {
log('❌ Tibber Lesefehler: ' + e.message, 'warn');
return;
}
const real28Obj = getRealPower28();
const real29Obj = getRealPower29();
const plug28 = real28Obj.value;
const plug29 = real29Obj.value;
const plugGes = plug28 + plug29;
// Ausfallerkennung nur wenn eine Leistungsmessung vorhanden und aktiv ist.
// Bei 'keine_messung': Fault-Zustand zuruecksetzen damit Speicher nicht blockiert bleibt.
if (real28Obj.source !== 'keine_messung') {
updateDeviceFaultState('28', plug28);
} else {
clearFaultState('28');
}
if (real29Obj.source !== 'keine_messung') {
updateDeviceFaultState('29', plug29);
} else {
clearFaultState('29');
}
safeSetState(DP + 'status.tibber_w', Math.round(tibber), true);
safeSetState(DP + 'status.plug_28_w', Math.round(plug28), true);
safeSetState(DP + 'status.plug_29_w', Math.round(plug29), true);
safeSetState(DP + 'status.tibber_quelle', tibberQuelle, true);
safeSetState(DP + 'status.leistungsquelle_28', real28Obj.source, true);
safeSetState(DP + 'status.leistungsquelle_29', real29Obj.source, true);
safeSetState(DP + 'status.real_28_w', Math.round(plug28), true);
safeSetState(DP + 'status.real_29_w', Math.round(plug29), true);
let modus28 = 'Standby';
let modus29 = 'Standby';
let modusGesamtReal = 'Standby';
if (plug28 > EV_W) {
modus28 = 'Laden';
} else if (plug28 < -EV_W) {
modus28 = 'Entladen';
}
if (plug29 > EV_W) {
modus29 = 'Laden';
} else if (plug29 < -EV_W) {
modus29 = 'Entladen';
}
if (plugGes > EV_W) {
modusGesamtReal = 'Laden';
} else if (plugGes < -EV_W) {
modusGesamtReal = 'Entladen';
}
safeSetState(DP + 'status.modus_28', modus28, true);
safeSetState(DP + 'status.modus_29', modus29, true);
safeSetState(DP + 'status.modus_gesamt', modusGesamtReal, true);
const realEntladen = plugGes < -EV_W ? Math.abs(plugGes) : 0;
let zielGesamt = 0;
let modus = 'Standby';
if (plugGes < -EV_W) {
zielGesamt = realEntladen + tibber;
modus = 'Entladen';
} else if (tibber > EV_W) {
zielGesamt = tibber;
modus = 'Entladen';
} else if (tibber < -EV_W) {
const realLaden = plugGes > EV_W ? plugGes : 0;
const verfuegbarerUeberschuss = realLaden + Math.abs(tibber);
const ladePufferPctRaw = safeVal(DP + 'einstellungen.lade_puffer_pct', 20);
const ladePufferPct = Math.max(0, Math.min(100, ladePufferPctRaw)) / 100;
const nutzbarerUeberschuss = Math.round(verfuegbarerUeberschuss * (1 - ladePufferPct));
zielGesamt = -nutzbarerUeberschuss;
modus = 'Laden';
log('🔋 Ladepuffer: verfuegbar ' + Math.round(verfuegbarerUeberschuss) + 'W, Puffer ' + Math.round(ladePufferPct * 100) + '%, nutzbar ' + Math.round(nutzbarerUeberschuss) + 'W');
}
let w28 = 0;
let w29 = 0;
if (modus === 'Entladen') {
const availableDischarge = getAvailableTargets('Entladen');
if (availableDischarge.length <= 0) {
log('🔋 SOC-Schutz: kein Speicher verfuegbar fuer Entladen - Standby');
modus = 'Standby';
zielGesamt = 0;
} else if (zielGesamt < minW) {
modus = 'Standby';
zielGesamt = 0;
} else {
const dist = distributeDischargePower(zielGesamt, availableDischarge, maxEntladenW);
w28 = dist['28'];
w29 = dist['29'];
zielGesamt = w28 + w29;
}
} else if (modus === 'Laden') {
let ladeZiel = Math.max(zielGesamt, -(maxLadenW * 2));
const drosselSoc = safeVal(DP + 'einstellungen.laden_drossel_soc', 90);
const drosselFaktor = safeVal(DP + 'einstellungen.laden_drossel_faktor', 50) / 100;
const drosselMaxW = safeVal(DP + 'einstellungen.laden_drossel_max_w', 800);
const maxSoc = Math.max(soc28, soc29);
if (maxSoc >= drosselSoc) {
const nachFaktor = ladeZiel * drosselFaktor;
const nachAbsolut = Math.max(ladeZiel, -(drosselMaxW * 2));
ladeZiel = Math.max(nachFaktor, nachAbsolut);
log('🔋 Ladedrosselung SOC ' + Math.round(maxSoc) + '%: Faktor→' + Math.round(nachFaktor) + 'W, Absolut→' + Math.round(nachAbsolut) + 'W, Gewählt: ' + Math.round(ladeZiel) + 'W');
}
if (Math.abs(ladeZiel) < minW) {
modus = 'Standby';
zielGesamt = 0;
} else {
const availableChargePre = [];
if (!fullChargeLock28) availableChargePre.push('28');
if (!fullChargeLock29) availableChargePre.push('29');
const dist = distributeChargePower(ladeZiel, availableChargePre, maxLadenW);
w28 = dist['28'];
w29 = dist['29'];
zielGesamt = w28 + w29;
}
}
if (updateFullChargeLock('28', soc28, plug28, w28, modus === 'Laden')) {
if (modus === 'Laden' && w28 < 0) w28 = 0;
}
if (updateFullChargeLock('29', soc29, plug29, w29, modus === 'Laden')) {
if (modus === 'Laden' && w29 < 0) w29 = 0;
}
if (modus === 'Laden') {
const availableChargePost = getAvailableTargets('Laden');
const ladeGesamtNachDrossel = w28 + w29;
if (availableChargePost.length <= 0) {
w28 = 0;
w29 = 0;
modus = 'Standby';
zielGesamt = 0;
} else {
const distPost = distributeChargePower(ladeGesamtNachDrossel, availableChargePost, maxLadenW);
w28 = distPost['28'];
w29 = distPost['29'];
zielGesamt = w28 + w29;
}
}
const faultAdjusted = applyFaultAndProbe(modus, w28, w29, maxEntladenW, maxLadenW);
w28 = faultAdjusted['28'];
w29 = faultAdjusted['29'];
zielGesamt = w28 + w29;
safeSetState(DP + 'status.ziel_gesamt_w', Math.round(zielGesamt), true);
lastTarget28 = w28;
lastTarget29 = w29;
const alternatingActive = safeBool(DP + 'einstellungen.alternierend_aktiv', ALTERNIEREND_AKTIV);
const availableTargets = getAvailableTargets(modus);
const teilintervallMs = getTeilintervallMs(availableTargets.length);
safeSetState(DP + 'status.teilintervall_ms', teilintervallMs, true);
const desiredLoopMs = (alternatingActive && availableTargets.length > 1 && modus !== 'Standby') ? teilintervallMs : INTERVALL_MS;
applyLoopInterval(desiredLoopMs);
if (!alternatingActive || availableTargets.length <= 1 || modus === 'Standby') {
if (modus === 'Standby') safeSetState(DP + 'status.alternierender_speicher', '', true);
sendPairCommands(w28, w29);
} else {
const activeTarget = availableTargets[alternatingIndex % availableTargets.length];
alternatingIndex = (alternatingIndex + 1) % availableTargets.length;
sendAlternatingCommand(activeTarget, w28, w29);
}
const ts = new Date().toLocaleTimeString();
let logModus = modus;
let logEmoji = modus === 'Entladen' ? '⬇️' : modus === 'Laden' ? '⬆️' : '⏸️';
const ladepauseAktiv = (modus === 'Laden') && (w28 === 0 && w29 === 0) && (fullChargeLock28 || fullChargeLock29);
if (ladepauseAktiv) {
logModus = 'Ladepause (voll)';
logEmoji = '⏸️';
}
const activeLogTarget = (alternatingActive && availableTargets.length > 1 && modus !== 'Standby')
? safeVal(DP + 'status.alternierender_speicher', '')
: 'alle';
const info = ts + ' | ' + logEmoji + ' ' + logModus +
' | Tibber: ' + Math.round(tibber) + 'W (' + tibberQuelle + ')' +
' | Gesamt real: ' + Math.round(plugGes) + 'W' +
' | Ziel gesamt: ' + Math.round(zielGesamt) + 'W' +
' | Ziel 3528: ' + w28 + 'W | Gesendet 3528: ' + lastCmd28 + 'W | Ist 3528: ' + Math.round(plug28) + 'W | Quelle 3528: ' + real28Obj.source + ' | SOC 3528: ' + Math.round(soc28) + '%' +
' | Ziel 3529: ' + w29 + 'W | Gesendet 3529: ' + lastCmd29 + 'W | Ist 3529: ' + Math.round(plug29) + 'W | Quelle 3529: ' + real29Obj.source + ' | SOC 3529: ' + Math.round(soc29) + '%' +
' | Aktiv neu gesetzt: ' + activeLogTarget;
safeSetState(DP + 'status.letzte_regelung', info, true);
log('📤 ' + info);
}
// ------------------------------------------------------------
// Start / Stop
// ------------------------------------------------------------
function start() {
log('🚀 MARSTEK_3 Hauptskript v1.2.1 gestartet');
initDP();
setTimeout(function() {
initUDP();
setTimeout(function() {
regeln();
applyLoopInterval(INTERVALL_MS);
log('✅ Start-Regelintervall: ' + (INTERVALL_MS / 1000) + 's');
}, 1500);
}, 1000);
}
start();
onStop(function() {
if (ivMain) clearInterval(ivMain);
if (udpClient) {
sendManual(IP_28, 0);
sendManual(IP_29, 0);
setTimeout(function() {
try { udpClient.close(); } catch(e) {}
}, 500);
}
log('🛑 MARSTEK_3 gestoppt - 0W gesendet');
}, 1500);
Im Vis2 sieht es bei mir so aus:
[image: 1774867265557-screenshot-2026-03-30-124020.png]