/************************************************************** * * PV-Tabelle ioBroker VIS * * Version 6.0.1 * * Sungrow SH20T Hybridwechselrichter * * ************************************************************** * * Funktionsübersicht * * Erfasst Tages-, Monats- und Jahreswerte für Produktion, Direktverbrauch, Netzeinspeisung, Netzbezug, Batterieladung/-entladung sowie die daraus abgeleiteten Werte Netzbilanz und Batteriebilanz (Sungrow SH20T über Modbus) * Live-Werte in today.*, aktualisiert bei jeder Änderung der Modbus-Register sowie zusätzlich alle 10 Minuten * Feste Struktur mit 31 Tages-Slots (month.days.d01–d31) pro Monat, die laufend mit den aktuellen Werten beschrieben werden – kein separater Mitternachts-Job nötig, dadurch keine Race-Condition mit dem Mitternachts-Reset des Wechselrichters * Monatssumme wird bei jedem Update direkt aus den Tages-Slots neu berechnet (month.*) * Der laufende Monat wird bei jedem Update zusätzlich live in seinen eigenen Archiv-Pfad (archive.._.*) gespiegelt, damit dort nicht bis zum Monatswechsel nur Nullen stehen * Automatische Erkennung von Monatswechseln: abgelaufener Monat wird aus den Tages-Slots summiert und als finaler Gesamtwert ins Archiv geschrieben, danach werden alle 31 Slots auf 0 zurückgesetzt * Berücksichtigt unterschiedliche Monatslängen (28–31 Tage) bei der Summenbildung * Jahressumme (year.*) wird laufend aus abgeschlossenen Monatsarchiven + aktuellem Monat berechnet und zusätzlich dauerhaft je Jahr unter archive..year.* gespeichert – Basis für die Jahresauswahl in der VIS * Jeder Monat bleibt jahresspezifisch unter archive.._.* dauerhaft erhalten (keine Überschreibung zwischen Jahren) – Basis für das Monats-Balkendiagramm * Sichtbarkeits-Schalter für das Monats-Balkendiagramm (system.display.*) – 6 boolesche Datenpunkte, mit denen die VIS steuert, welche Balken (PV Produktion, Direktverbrauch, Batterie, Netzeinspeisung, Netzbezug, Netzbilanz) angezeigt werden. Werden beim Start automatisch angelegt (Default: true) * NEU (6.0.0): system.selectedYear (Jahresauswahl für das Monatsdiagramm, von der VIS beschreibbar) und system.monthChartSVG (fertig berechnetes SVG-Balkendiagramm als String) – das komplette Diagramm wird HIER im Skript gerendert, nicht mehr im Browser. Die VIS muss den Chart-Container-Widget-Text dadurch nur noch per {oid}-Bindung auf system.monthChartSVG zeigen; kein komplexer Auswerte-Code mehr im Widget-HTML nötig. Neuberechnung bei jedem updateToday()-Zyklus sowie sofort bei Änderung von system.selectedYear oder einem der system.display.*-Schalter * Tägliche Prüfung (00:10 Uhr), ob für den Vortag Werte vorliegen; bei fehlenden Werten Warnung per Telegram und in system.warning * Legt beim ersten Start automatisch die komplette Objektstruktur an (inkl. Puffer, damit frisch angelegte States sicher verfügbar sind, bevor geschrieben wird) * Protokolliert letzten archivierten Monat (system.lastMonthArchived) und letzten Update-Zeitpunkt (system.lastUpdate) * **************************************************************/ const BASE = "0_userdata.0.PV-Tabelle"; // Telegram Instanz anpassen const TELEGRAM_INSTANCE = "telegram.0"; // Telegram-Empfänger: // - leeres Array [] => Nachricht geht an ALLE im Adapter hinterlegten Nutzer (Standardverhalten) // - Chat-IDs eintragen => Nachricht geht NUR an diese Personen const TELEGRAM_RECIPIENTS = []; // z.B. [123456789, 987654321] // Max, Anna // Sungrow SH20T Modbus const MODBUS = { production: "modbus.1.inputRegisters.1.13001_Daily_PV_Generation", export: "modbus.1.inputRegisters.1.13004_Daily_export_energy_from_PV", consumption: "modbus.1.inputRegisters.1.13016_Daily_direct_Energy_Consumption", import: "modbus.1.inputRegisters.1.13035_Daily_Import_Energy", batteryCharge: "modbus.1.inputRegisters.1.13039_Daily_Charge_Energy", batteryDischarge: "modbus.1.inputRegisters.1.13025_Daily_battery_discharge_Energy", batterySOC: "modbus.1.inputRegisters.1.13022_Battery_level" }; // Metriken, die pro Tag summiert werden (kumulative Tageswerte) const DAY_METRICS = [ "production","consumption","export","import", "batteryCharge","batteryDischarge", "gridBalance","batteryBalance" ]; // NEU: Sichtbarkeits-Schalter für das Monats-Balkendiagramm in der VIS. // "battery" steuert Ladung+Entladung gemeinsam (ein Balken, zwei Farben). const DISPLAY_TOGGLES = [ "production","consumption","battery","export","import","gridBalance" ]; const DISPLAY_TOGGLE_NAMES = { production: "Anzeige: PV Produktion", consumption: "Anzeige: Direktverbrauch", battery: "Anzeige: Batterie (geladen/entladen)", export: "Anzeige: Netzeinspeisung", import: "Anzeige: Netzbezug", gridBalance: "Anzeige: Netzbilanz" }; // Metrik-Definitionen für das serverseitig gerenderte Monats-Balkendiagramm. // Läuft ausschließlich hier im Backend-Skript (JavaScript-Adapter) - wird nie // von VIS' HTML-Widget-Scanner angefasst, normale Objekt-Literale sind hier // also unproblematisch (anders als im Widget-HTML-Feld selbst). const CHART_MONTH_NAMES = ["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"]; const CHART_MONTH_NAMES_FULL = ["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"]; const CHART_METRICS = [ { key:"production", toggle:"production", color:"#00C853", label:"PV Produktion" }, { key:"consumption", toggle:"consumption", color:"#FFD600", label:"Direktverbrauch" }, { key:"battery", toggle:"battery", colorCharge:"#AA00FF", colorDischarge:"#4A148C", label:"Batterie" }, { key:"export", toggle:"export", color:"#2962FF", label:"Netzeinspeisung" }, { key:"import", toggle:"import", color:"#FF1744", label:"Netzbezug" }, { key:"gridBalance", toggle:"gridBalance", color:"#FFFFFF", label:"Netzbilanz" } ]; // ============================================================ // Hilfsfunktionen // ============================================================ function round(v){ return Math.round(Number(v || 0) * 100) / 100; } function formatDateTime(date){ const day = String(date.getDate()).padStart(2,"0"); const month = String(date.getMonth() + 1).padStart(2,"0"); const year = date.getFullYear(); const hour = String(date.getHours()).padStart(2,"0"); const minute = String(date.getMinutes()).padStart(2,"0"); return `${day}.${month}.${year} ${hour}.${minute} Uhr`; } function getNumber(id){ if(!existsState(id)) return 0; const v = Number(getState(id).val); return isNaN(v) ? 0 : v; } function getString(id, def = ""){ if(!existsState(id)) return def; const v = getState(id).val; return v === null || v === undefined ? def : String(v); } function getBoolean(id, def){ if(!existsState(id)) return def; const v = getState(id).val; return v === null || v === undefined ? def : !!v; } // Rundet eine grobe Schrittweite auf eine "schöne" Zahl (1/2/5/10/20/50/...), // wie es Diagramm-Bibliotheken für Achsenbeschriftungen typischerweise tun. function niceStep(roughStep){ if(roughStep <= 0) return 1; const exponent = Math.floor(Math.log10(roughStep)); const fraction = roughStep / Math.pow(10, exponent); let niceFraction; if(fraction <= 1) niceFraction = 1; else if(fraction <= 2) niceFraction = 2; else if(fraction <= 5) niceFraction = 5; else niceFraction = 10; return niceFraction * Math.pow(10, exponent); } function getMonthName(m){ return ["","Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"][m]; } function daysInMonth(year, month){ // Tag 0 des Folgemonats = letzter Tag des aktuellen Monats return new Date(year, month, 0).getDate(); } function dayId(day){ return "d" + String(day).padStart(2,"0"); } function monthArchivePath(year, month){ return `${BASE}.archive.${year}.${String(month).padStart(2,"0")}_${getMonthName(month)}`; } function ensureObject(id, obj){ if(!existsObject(id)){ setObject(id, obj); } } function ensureState(id, name, type = "number"){ if(!existsObject(id)){ setObject(id, { type: "state", common: { name, type, role: "value", read: true, write: true, def: type === "number" ? 0 : "" }, native: {} }); } } // NEU: boolescher Schalter-State (für die Balken-Sichtbarkeit in der VIS). // Wird nur beim erstmaligen Anlegen mit dem Default-Wert vorbelegt, // bestehende (vom Nutzer bereits umgeschaltete) States bleiben unangetastet. function ensureBooleanState(id, name, defaultValue = true){ if(!existsObject(id)){ setObject(id, { type: "state", common: { name, type: "boolean", role: "switch", read: true, write: true, def: defaultValue }, native: {} }); setState(id, defaultValue, true); } } // ============================================================ // Struktur anlegen // ============================================================ function ensureStructure(){ ensureObject(BASE, {type:"channel", common:{name:"PV-Tabelle"}, native:{}}); ensureObject(`${BASE}.archive`, {type:"channel", common:{name:"Archiv"}, native:{}}); ensureObject(`${BASE}.today`, {type:"channel", common:{name:"Heute"}, native:{}}); ensureObject(`${BASE}.month`, {type:"channel", common:{name:"Monat"}, native:{}}); ensureObject(`${BASE}.month.days`, {type:"channel", common:{name:"Tage im Monat"}, native:{}}); ensureObject(`${BASE}.year`, {type:"channel", common:{name:"Jahr"}, native:{}}); ensureObject(`${BASE}.system`, {type:"channel", common:{name:"System"}, native:{}}); ensureObject(`${BASE}.system.display`, {type:"channel", common:{name:"Anzeige-Schalter Monatsdiagramm"}, native:{}}); // today const DISPLAY_NAMES = { batterySOC: "Ladezustand" }; [...DAY_METRICS, "batterySOC"].forEach(v => ensureState(`${BASE}.today.${v}`, DISPLAY_NAMES[v] || v) ); // month + year Summen DAY_METRICS.forEach(v => { ensureState(`${BASE}.month.${v}`, v); ensureState(`${BASE}.year.${v}`, v); }); // 31 feste Tages-Slots for(let d = 1; d <= 31; d++){ const dp = `${BASE}.month.days.${dayId(d)}`; ensureObject(dp, {type:"channel", common:{name:`Tag ${d}`}, native:{}}); DAY_METRICS.forEach(v => ensureState(`${dp}.${v}`, v)); } ensureState(`${BASE}.system.currentMonthKey`, "currentMonthKey", "string"); ensureState(`${BASE}.system.lastMonthArchived`, "lastMonthArchived", "string"); ensureState(`${BASE}.system.lastUpdate`, "lastUpdate", "string"); ensureState(`${BASE}.system.warning`, "warning", "string"); // NEU: Sichtbarkeits-Schalter für das Monats-Balkendiagramm, Default = an DISPLAY_TOGGLES.forEach(t => ensureBooleanState(`${BASE}.system.display.${t}`, DISPLAY_TOGGLE_NAMES[t], true) ); // NEU (6.0.0): von der VIS beschreibbare Jahresauswahl für das // Monatsdiagramm, plus der fertig gerenderte SVG-String ensureState(`${BASE}.system.selectedYear`, "selectedYear (Monatsdiagramm)", "string"); ensureState(`${BASE}.system.monthChartSVG`, "monthChartSVG (gerendertes Diagramm)", "string"); } function ensureYearArchiveObject(year){ ensureObject(`${BASE}.archive.${year}`, { type:"channel", common:{name:String(year)}, native:{} }); // fester, dauerhafter Datenpunkt-Satz je Jahr fuer die VIS-Jahresauswahl const yp = `${BASE}.archive.${year}.year`; ensureObject(yp, {type:"channel", common:{name:`Jahressumme ${year}`}, native:{}}); DAY_METRICS.forEach(v => ensureState(`${yp}.${v}`, v)); } function ensureMonthArchiveObject(year, month){ ensureYearArchiveObject(year); const mp = monthArchivePath(year, month); ensureObject(mp, {type:"channel", common:{name:getMonthName(month)}, native:{}}); DAY_METRICS.forEach(v => ensureState(`${mp}.${v}`, v)); } // ============================================================ // Werte lesen // ============================================================ function readCurrentValues(){ const production = round(getNumber(MODBUS.production)); const consumption = round(getNumber(MODBUS.consumption)); const exportEnergy = round(getNumber(MODBUS.export)); const importEnergy = round(getNumber(MODBUS.import)); const batteryCharge = round(getNumber(MODBUS.batteryCharge)); const batteryDischarge = round(getNumber(MODBUS.batteryDischarge)); const batterySOC = round(getNumber(MODBUS.batterySOC)); return { production, consumption, export: exportEnergy, import: importEnergy, batteryCharge, batteryDischarge, batterySOC, gridBalance: round(exportEnergy - importEnergy), batteryBalance: round(batteryCharge - batteryDischarge) }; } // ============================================================ // Tages-Slot schreiben (laufend, nicht nur um Mitternacht) // ============================================================ function writeDaySlot(day, values){ const dp = `${BASE}.month.days.${dayId(day)}`; DAY_METRICS.forEach(k => { setState(`${dp}.${k}`, round(values[k] || 0), true); }); } function readDaySlot(day){ const dp = `${BASE}.month.days.${dayId(day)}`; const values = {}; DAY_METRICS.forEach(k => { values[k] = getNumber(`${dp}.${k}`); }); return values; } function resetMonthDays(){ for(let d = 1; d <= 31; d++){ const zero = {}; DAY_METRICS.forEach(k => zero[k] = 0); writeDaySlot(d, zero); } } // ============================================================ // Monat aus Tages-Slots summieren // ============================================================ function sumMonthFromDays(year, month){ const sum = {}; DAY_METRICS.forEach(k => sum[k] = 0); const lastDay = daysInMonth(year, month); for(let d = 1; d <= lastDay; d++){ const v = readDaySlot(d); DAY_METRICS.forEach(k => sum[k] += Number(v[k] || 0)); } // gridBalance/batteryBalance neu aus den Summen ableiten sum.gridBalance = round(sum.export - sum.import); sum.batteryBalance = round(sum.batteryCharge - sum.batteryDischarge); DAY_METRICS.forEach(k => sum[k] = round(sum[k])); return sum; } function recalcMonth(year, month){ const sum = sumMonthFromDays(year, month); DAY_METRICS.forEach(k => setState(`${BASE}.month.${k}`, sum[k], true)); // laufenden Monat live auch in seinen eigenen Archiv-Pfad spiegeln, // damit dort nicht bis zum Monatswechsel nur Nullen stehen ensureMonthArchiveObject(year, month); const mp = monthArchivePath(year, month); DAY_METRICS.forEach(k => setState(`${mp}.${k}`, sum[k], true)); return sum; } // ============================================================ // Jahr = archivierte (abgeschlossene) Monate + laufender Monat // ============================================================ function recalcYear(year){ const sum = {}; DAY_METRICS.forEach(k => sum[k] = 0); const currentKey = getString(`${BASE}.system.currentMonthKey`, ""); for(let m = 1; m <= 12; m++){ const key = `${year}-${String(m).padStart(2,"0")}`; const mp = monthArchivePath(year, m); if(key === currentKey){ // laufender Monat: aktuelle (live berechnete) Summen verwenden DAY_METRICS.forEach(k => sum[k] += getNumber(`${BASE}.month.${k}`)); }else if(existsState(`${mp}.production`)){ // abgeschlossener, archivierter Monat DAY_METRICS.forEach(k => sum[k] += getNumber(`${mp}.${k}`)); } } DAY_METRICS.forEach(k => setState(`${BASE}.year.${k}`, round(sum[k]), true)); // zusaetzlich dauerhaft je Jahr spiegeln, damit auch vergangene Jahre // (nach einem Jahreswechsel) in der VIS-Jahresauswahl abrufbar bleiben ensureYearArchiveObject(year); DAY_METRICS.forEach(k => setState(`${BASE}.archive.${year}.year.${k}`, round(sum[k]), true)); } // ============================================================ // Monatswechsel behandeln // ============================================================ function finalizeMonthToArchive(year, month){ ensureMonthArchiveObject(year, month); const sum = sumMonthFromDays(year, month); const mp = monthArchivePath(year, month); DAY_METRICS.forEach(k => setState(`${mp}.${k}`, sum[k], true)); setState(`${BASE}.system.lastMonthArchived`, `${year}-${String(month).padStart(2,"0")}`, true); log(`PV-Tabelle: Monat ${getMonthName(month)} ${year} archiviert (finaler Gesamtwert)`, "info"); } function checkMonthRollover(date){ const year = date.getFullYear(); const month = date.getMonth() + 1; const key = `${year}-${String(month).padStart(2,"0")}`; const storedKey = getString(`${BASE}.system.currentMonthKey`, ""); if(storedKey === key){ return; // kein Monatswechsel } if(storedKey){ // vorherigen Monat aus storedKey ableiten und final archivieren const [py, pm] = storedKey.split("-").map(Number); finalizeMonthToArchive(py, pm); } resetMonthDays(); ensureMonthArchiveObject(year, month); setState(`${BASE}.system.currentMonthKey`, key, true); log(`PV-Tabelle: Neuer Monat erkannt (${key}), Tages-Slots zurückgesetzt`, "info"); } // ============================================================ // Monats-Balkendiagramm serverseitig als SVG-String rendern // ============================================================ function readMonthValuesForChart(year, month){ const mp = monthArchivePath(year, month); return { production: getNumber(`${mp}.production`), consumption: getNumber(`${mp}.consumption`), export: getNumber(`${mp}.export`), import: getNumber(`${mp}.import`), batteryCharge: getNumber(`${mp}.batteryCharge`), batteryDischarge: getNumber(`${mp}.batteryDischarge`), gridBalance: getNumber(`${mp}.gridBalance`) }; } function renderMonthChartSVG(){ const year = getString(`${BASE}.system.selectedYear`, String(new Date().getFullYear())); const active = CHART_METRICS.filter(m => getBoolean(`${BASE}.system.display.${m.toggle}`, true)); if(active.length === 0){ setState(`${BASE}.system.monthChartSVG`, '
Keine Werte ausgewählt
', true); return; } const barWidth = 16, barGap = 5, groupGap = 20; const groupWidth = active.length * (barWidth + barGap) - barGap + groupGap; const chartHeight = 240, topPad = 16, bottomPad = 26, leftPad = 42, rightPad = 8; const monthsData = {}; for(let m = 1; m <= 12; m++){ monthsData[m] = readMonthValuesForChart(year, m); } let maxPos = 0, maxNeg = 0; for(let m = 1; m <= 12; m++){ const v = monthsData[m]; active.forEach(metric => { if(metric.key === "battery"){ const total = (v.batteryCharge||0) + (v.batteryDischarge||0); if(total > maxPos) maxPos = total; }else if(metric.key === "gridBalance"){ if(v.gridBalance >= 0){ if(v.gridBalance > maxPos) maxPos = v.gridBalance; }else{ const absV = Math.abs(v.gridBalance); if(absV > maxNeg) maxNeg = absV; } }else{ const val = v[metric.key] || 0; if(val > maxPos) maxPos = val; } }); } if(maxPos === 0 && maxNeg === 0){ maxPos = 1; } // "Schöne" Achsen-Obergrenzen/Schrittweiten statt einfacher Bruchteile // des exakten Maximums - dadurch runde Werte wie 100/200/300 statt // z.B. 138,5/277/415,5. Positiver und negativer Bereich verwenden // bewusst dieselbe Schrittweite (posStep), damit die Linienabstände auf // beiden Seiten der Nulllinie optisch konsistent bleiben. const posStep = niceStep(maxPos / 6); const niceMaxPos = maxPos > 0 ? posStep * Math.ceil(maxPos / posStep) : 0; const negStep = posStep; const niceMaxNeg = maxNeg > 0 ? negStep * Math.ceil(maxNeg / negStep) : 0; const plotHeight = chartHeight - topPad - bottomPad; const negRatio = niceMaxNeg > 0 ? (niceMaxNeg / (niceMaxPos + niceMaxNeg)) : 0; const negHeight = plotHeight * negRatio; const posHeight = plotHeight - negHeight; const baselineY = topPad + posHeight; const posScale = niceMaxPos > 0 ? posHeight / niceMaxPos : 0; const negScale = niceMaxNeg > 0 ? negHeight / niceMaxNeg : 0; const totalWidth = leftPad + rightPad + 12 * groupWidth; let svg = ``; // Y-Achse: Hilfslinien in runden Schritten (posStep/negStep), gleichmäßig verteilt if(niceMaxPos > 0){ for(let val = posStep; val <= niceMaxPos + 0.0001; val += posStep){ const y = baselineY - (val * posScale); svg += ``; svg += `${Math.round(val)}`; } } if(niceMaxNeg > 0){ for(let val = negStep; val <= niceMaxNeg + 0.0001; val += negStep){ const y = baselineY + (val * negScale); svg += ``; svg += `${Math.round(-val)}`; } } svg += ``; for(let m = 1; m <= 12; m++){ const vv = monthsData[m]; const groupX = leftPad + (m - 1) * groupWidth; let barX = groupX + (groupGap / 2); active.forEach(metric => { if(metric.key === "battery"){ const charge = vv.batteryCharge || 0; const discharge = vv.batteryDischarge || 0; const chargeH = charge * posScale; const dischargeH = discharge * posScale; svg += ``; svg += ``; }else if(metric.key === "gridBalance"){ const gb = vv.gridBalance || 0; if(gb >= 0){ const h = gb * posScale; svg += ``; }else{ const h2 = Math.abs(gb) * negScale; svg += ``; } }else{ const val2 = vv[metric.key] || 0; const h3 = val2 * posScale; svg += ``; } barX += barWidth + barGap; }); svg += `${CHART_MONTH_NAMES[m-1]}`; } svg += ""; setState(`${BASE}.system.monthChartSVG`, svg, true); } // ============================================================ // Hauptupdate // ============================================================ function updateToday(){ const now = new Date(); checkMonthRollover(now); const v = readCurrentValues(); Object.keys(v).forEach(k => { if(existsState(`${BASE}.today.${k}`)){ setState(`${BASE}.today.${k}`, v[k], true); } }); const day = now.getDate(); writeDaySlot(day, v); recalcMonth(now.getFullYear(), now.getMonth() + 1); recalcYear(now.getFullYear()); // Monats-Diagramm nur neu rendern, wenn das aktuell ausgewählte Jahr // auch das laufende Jahr ist (bei einem vergangenen Jahr ändert sich // durch das laufende Update ohnehin nichts an den Archivwerten) const selYear = getString(`${BASE}.system.selectedYear`, String(now.getFullYear())); if(selYear === String(now.getFullYear())){ renderMonthChartSVG(); } setState(`${BASE}.system.lastUpdate`, formatDateTime(now), true); } // ============================================================ // Prüfung: hat der Vortag Werte bekommen? // ============================================================ function checkYesterday(){ const now = new Date(); const yesterday = new Date(now); yesterday.setDate(yesterday.getDate() - 1); let total = 0; if(yesterday.getMonth() + 1 === now.getMonth() + 1 && yesterday.getFullYear() === now.getFullYear()){ const v = readDaySlot(yesterday.getDate()); total = Object.values(v).reduce((a,b)=>a + Math.abs(Number(b)||0), 0); }else{ const mp = monthArchivePath(yesterday.getFullYear(), yesterday.getMonth() + 1); total = getNumber(`${mp}.production`) + getNumber(`${mp}.consumption`); } if(total <= 0){ const msg = `⚠️ PV-Tabelle: Für ${yesterday.getDate()}.${String(yesterday.getMonth()+1).padStart(2,"0")}.${yesterday.getFullYear()} liegen keine Werte vor`; setState(`${BASE}.system.warning`, msg, true); log(msg, "warn"); try{ if(TELEGRAM_RECIPIENTS.length > 0){ TELEGRAM_RECIPIENTS.forEach(chatId => { sendTo(TELEGRAM_INSTANCE, "send", {text: msg, chatId: chatId}); }); }else{ sendTo(TELEGRAM_INSTANCE, "send", {text: msg}); } }catch(e){ log("Telegram Versand fehlgeschlagen: " + e, "error"); } }else{ setState(`${BASE}.system.warning`, "", true); } } // ============================================================ // Start // ============================================================ setTimeout(() => { ensureStructure(); const now = new Date(); ensureMonthArchiveObject(now.getFullYear(), now.getMonth() + 1); // Default für die Jahresauswahl des Monatsdiagramms setzen, falls noch // kein Wert vorhanden ist (ensureState legt das Objekt an, aber ohne // Startwert) if(getString(`${BASE}.system.selectedYear`, "") === ""){ setState(`${BASE}.system.selectedYear`, String(now.getFullYear()), true); } // kurze Pause, damit frisch angelegte Objekte sicher registriert sind, // bevor direkt im Anschluss Werte hineingeschrieben werden // (relevant vor allem direkt nach einer kompletten Neuanlage des Objektbaums) setTimeout(() => { updateToday(); checkYesterday(); renderMonthChartSVG(); log("PV-Tabelle v6.0.1 gestartet", "info"); }, 6000); }, 5000); // ============================================================ // Sofortige Aktualisierung bei Sungrow Änderungen // ============================================================ on( { id:[ MODBUS.production, MODBUS.export, MODBUS.consumption, MODBUS.import, MODBUS.batteryCharge, MODBUS.batteryDischarge, MODBUS.batterySOC ], change:"ne" }, updateToday ); // ============================================================ // Regelmäßige Aktualisierung alle 10 Minuten // (fängt zusätzlich den Monatswechsel zuverlässig ab) // ============================================================ schedule( "*/10 * * * *", updateToday ); // ============================================================ // Kontrolle: hat der Vortag Werte? - 00:10 Uhr // ============================================================ schedule( "10 0 * * *", checkYesterday ); // ============================================================ // Monats-Diagramm sofort neu rendern, wenn die Jahresauswahl oder // einer der Sichtbarkeits-Schalter über die VIS geändert wird // ============================================================ on( { id:[ `${BASE}.system.selectedYear`, `${BASE}.system.display.production`, `${BASE}.system.display.consumption`, `${BASE}.system.display.battery`, `${BASE}.system.display.export`, `${BASE}.system.display.import`, `${BASE}.system.display.gridBalance` ], change:"ne" }, renderMonthChartSVG ); // ============================================================ // Ende PV-Tabelle v6.0.1 // ============================================================