Skip to content
  • Home
  • Aktuell
  • Tags
  • 0 Ungelesen 0
  • Kategorien
  • Unreplied
  • Beliebt
  • GitHub
  • Docu
  • Hilfe
Skins
  • Light
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Standard: (Kein Skin)
  • Kein Skin
Einklappen
ioBroker Logo

Community Forum

donate donate
  1. ioBroker Community Home
  2. Deutsch
  3. Praktische Anwendungen (Showcase)
  4. Regelmässiger Historienexport als Excel-Tabelle

NEWS

  • Jahresrückblick 2025 – unser neuer Blogbeitrag ist online! ✨
    BluefoxB
    Bluefox
    16
    1
    1.9k

  • Neuer Blogbeitrag: Monatsrückblick - Dezember 2025 🎄
    BluefoxB
    Bluefox
    13
    1
    919

  • Weihnachtsangebot 2025! 🎄
    BluefoxB
    Bluefox
    25
    1
    2.2k

Regelmässiger Historienexport als Excel-Tabelle

Geplant Angeheftet Gesperrt Verschoben Praktische Anwendungen (Showcase)
7 Beiträge 3 Kommentatoren 548 Aufrufe 6 Watching
  • Älteste zuerst
  • Neuste zuerst
  • Meiste Stimmen
Antworten
  • In einem neuen Thema antworten
Anmelden zum Antworten
Dieses Thema wurde gelöscht. Nur Nutzer mit entsprechenden Rechten können es sehen.
  • BertDerKleineB Offline
    BertDerKleineB Offline
    BertDerKleine
    schrieb am zuletzt editiert von BertDerKleine
    #1

    Hallo,
    hier ein recht banaler Anwendungsfall. Ich möchte gerne für einige Datenpunkte auch außerhalb von iobroker bestimmte Kerndaten haben, um damit z.B. in Excel rumspielen zu können.

    Dabei will ich nicht jedes Datenfragment aus der History-DB pro Datenpunkt haben, sondern mir reicht hier ein Wert pro Tag, i.d.R. der Höchste Zählerstand eines Wertes.

    Das ganze will ich einfach automatisch einmal pro Monat zugesendet bekommen per Email.

    Als Zusatzfeature, das ich über einen extra Datenpunkt steuern kann, kann das Ding auch sofort ein ganzes Jahr in dieser Form exportieren.

    Vielleicht habt Ihr ja ähnliche Anforderungen, dann könnt Ihr hierdrauf aufsetzen.

    Ich hoffe, alles ist hinreichend sprechend und auskommentiert, dass es gut lesbar ist.
    Das Logging kann man ja leicht entfernen, wenn es nervt. :-)

    /**
     * ================================================================
     * 📊 IOBROKER MONATLICHER HISTORIEN-BERICHT ALS EXCEL PER E-MAIL
     * ================================================================
     *
     * 🎯 ZWECK DES SKRIPTS:
     * Dieses Skript erstellt automatisch am Anfang jedes Monats eine Excel-Datei
     * mit allen Tageswerten des VORMONATS für konfigurierte Datenpunkte und sendet sie per E-Mail.
     * Alternativ kann es auch manuell gestartet werden, dann werden die Daten des AKTUELLEN Monats ausgegeben.
     * Bei Bedarf kann auch ein Jahresbericht (365 Tage) erstellt werden.
     *
     * ✅ WAS ES TUT:
     * 1. Liest Email-Absender und -Empfänger aus konfigurierbaren Datenpunkten.
     * 2. Ermittelt automatisch den richtigen Zeitraum (Vormonat, aktueller Monat oder 365 Tage).
     * 3. Holt für jeden konfigurierten Datenpunkt die Historie des Zeitraums.
     * 4. Aggregiert die Werte pro Tag (konfigurierbar pro Spalte).
     * 5. Wendet optional Multiplikator an (z. B. 1000 für Liter statt m³).
     * 6. Legt fest, wieviele Nachkommastellen verwendet werden.
     * 7. Kann leere Werte mit 0 auffüllen (optional).
     * 8. Erstellt eine ECHTE Excel-Datei (.xlsx) mit formatierter Tabelle.
     * 9. Sendet die Excel-Datei per E-Mail mit konfigurierbarem Betreff.
     *
     *  Gebastelt 2025 von BertDerKleine
     * 
     * 🧩 KONFIGURIERBAR:
     * - Liste der Datenpunkte mit:
     *   - id: Datenpunkt-Name
     *   - agg: 'max|min|avg|sum'
     *   - spaltenName: Anzeigename in Excel
     *   - nachkommastellen: Anzahl der Nachkommastellen (optional, default: 2)
     *   - multiplikator: Faktor für Multiplikation (optional, default: 1)
     *   - fuellenMitNull: Ob leere Werte mit 0 gefüllt werden sollen (optional, default: false)
     *
     * 📥 VORAUSSETZUNGEN:
     * - History-Adapter (history.0, sql.0, etc.) muss aktiv sein
     * - Email-Adapter (email.0) muss konfiguriert und verbunden sein
     * - Folgende Konfigurations-Datenpunkte müssen existieren:
     *   - 0_userdata.0.Email-Absender (string)
     *   - 0_userdata.0.Email-Empfaenger (string)
     *   - 0_userdata.0.Email-Betreff (string)
     *   - 0_userdata.0.VollJahresDaten (zahl: 0,1,2,3 Bei 0 läuft das Skript normal, bei 1/2/3 werden die Daten von 1-3 Jahren ausgegeben)
     *   - exceljs-Library muss installiert sein:
     *   → Im ioBroker JavaScript-Adapter in den Instanzeinstellungen unter
     *      "additional npm modules" eintragen: "exceljs"
     *
     * 📤 AUSGABE:
     * - Echte Excel-Datei (.xlsx) mit formatierter Tabelle
     * - Datum im Format TT.MM.JJJJ
     * - Zahlen als echte Excel-Zahlen (kein Text)
     * - Automatische Spaltenbreite
     * - Abwechselnde Zeilenfarben (Zebra-Stripes)
     * - Auto-Filter in der Kopfzeile
     * - Zwei Blätter: Daten + Zusammenfassung
     *
     * 🚀 AUTOMATISIERUNG:
     * Plane dieses Skript am 1. jedes Monats um 00:05 Uhr:
     *
     * ================================================================
     * 🔧 KONFIGURATION – HIER ANPASSEN!
     * ================================================================
     */
    
    // ✅ exceljs-Library laden (muss installiert sein: exceljs in "additional npm modules")
    try {
        const ExcelJS = require('exceljs');
        log('✅ ExcelJS erfolgreich geladen!', 'info');
    } catch (e) {
        log(`❌ Fehler beim Laden von ExcelJS: ${e.message}`, 'error');
        log('💡 Lösung: ExcelJS in den Instanzeinstellungen des JavaScript-Adapters unter "additional npm modules" hinzufügen', 'error');
        return;
    }
    
    const ExcelJS = require('exceljs');
    
    // Liste der zu verarbeitenden Datenpunkte
    // Jeder Eintrag: { id: '...', agg: 'max|min|avg|sum', spaltenName: '...', nachkommastellen?: number, multiplikator?: number, fuellenMitNull?: boolean }
    const Datenpunkte = [
        { id: 'alias.0.Aussentemperatur', agg: 'max', spaltenName: 'Höchste Aussentemperatur', nachkommastellen: 1, multiplikator: 1, fuellenMitNull: false },
        { id: 'alias.0.Aussentemperatur', agg: 'min', spaltenName: 'Niedrigste Aussentemperatur', nachkommastellen: 1, multiplikator: 1, fuellenMitNull: false },
        { id: 'alias.0.Regen', agg: 'max', spaltenName: 'Regenmenge', nachkommastellen: 1, multiplikator: 1, fuellenMitNull: true },
        { id: 'alias.0.GasH', agg: 'max', spaltenName: 'Gas für Heizung', nachkommastellen: 1, multiplikator: 1, fuellenMitNull: true },
        { id: 'alias.0.GasW', agg: 'max', spaltenName: 'Gas für Warmwasser', nachkommastellen: 1, multiplikator: 1, fuellenMitNull: true },
        { id: 'statistics.0.save.sumDelta.alias.0.Wasser.day', agg: 'max', spaltenName: 'Wasserverbrauch', nachkommastellen: 0, multiplikator: 1000, fuellenMitNull: true }, // in Liter
        { id: 'statistics.0.save.sumDelta.alias.0.PV_Erzeugung.day', agg: 'max', spaltenName: 'Strom-PV-Erzeugung', nachkommastellen: 0, multiplikator: 1, fuellenMitNull: false },
        { id: 'statistics.0.save.sumDelta.alias.0.Netzbezug.day', agg: 'max', spaltenName: 'Strom-Netzbezug', nachkommastellen: 2, multiplikator: 1, fuellenMitNull: false },
        { id: 'statistics.0.save.sumDelta.alias.0.Netzeinspeisung.day', agg: 'max', spaltenName: 'Strom-Netzeinspeisung', nachkommastellen: 2, multiplikator: 1, fuellenMitNull: false },
        { id: 'statistics.0.save.sumDelta.0_userdata.0.Verbrauch.day', agg: 'max', spaltenName: 'Strom-Verbrauch', nachkommastellen: 2, multiplikator: 1, fuellenMitNull: false }
    ];
    
    // Datenpunkte für Email-Konfiguration
    const EmailAbsenderDatenpunkt = '0_userdata.0.Email-Absender';
    const EmailEmpfaengerDatenpunkt = '0_userdata.0.Email-Empfaenger';
    const EmailBetreffDatenpunkt = '0_userdata.0.Email-Betreff';
    const VollJahresDatenDatenpunkt = '0_userdata.0.VollJahresDaten'; // "YES" oder "NO"
    
    // Adapter-Instanzen (anpassen, falls abweichend)
    const EmailAdapterInstanz = 'email.0';
    const HistoryAdapterInstanz = 'history.0'; // z. B. 'sql.0', 'influxdb.0'
    
    // Globale Variablen (lokal im Skript)
    let alleAggregiertenDaten = {};
    let sortierteTage = [];
    
    /**
     * ================================================================
     * HILFSFUNKTIONEN
     * ================================================================
     */
    
    function formatiereDatumFuerExcel(datum) {
        const jahr = datum.getFullYear();
        const monat = (datum.getMonth() + 1).toString().padStart(2, '0');
        const tag = datum.getDate().toString().padStart(2, '0');
        return `${tag}.${monat}.${jahr}`;
    }
    
    function bereinigeWerte(werte) {
        return werte.filter(wert => wert !== null && wert !== undefined && !isNaN(wert));
    }
    
    // Funktion: Historische Daten abrufen (optimiert für lange Zeiträume)
    async function holeHistorie(id, start, ende) {
        // ✅ Schätzung: wie viele Werte könnten es sein?
        const geschätzteTage = Math.ceil((ende.getTime() - start.getTime()) / (24 * 60 * 60 * 1000));
        const geschätzteWerteProTag = 150; // Anpassen je nach deinen Daten
        const geschätzteAnzahl = geschätzteTage * geschätzteWerteProTag;
        
        // ✅ Sicherstellen, dass genug Werte abgerufen werden
        const count = Math.min(200000, Math.max(50000, geschätzteAnzahl)); // Max 200.000, Min 50.000
    
        return new Promise((resolve, reject) => {
            sendTo(HistoryAdapterInstanz, 'getHistory', {
                id: id,
                options: { 
                    start: start.getTime(), 
                    end: ende.getTime(), 
                    aggregate: 'none', 
                    count: count 
                }
            }, (ergebnis) => {
                if (!ergebnis || !ergebnis.result || ergebnis.error) {
                    reject(new Error(ergebnis?.error || 'Unbekannter Fehler beim History-Abruf'));
                } else {
                    resolve(ergebnis.result);
                }
            });
        });
    }
    
    // Funktion: Einheit aus Datenpunkt auslesen
    function leseEinheitAusDatenpunkt(dpId) {
        try {
            const obj = getObject(dpId);
            if (obj && obj.common && obj.common.unit) {
                return obj.common.unit;
            }
        } catch (e) {
            log(`⚠️ Fehler beim Lesen der Einheit von ${dpId}: ${e.message}`, 'warn');
        }
        return ''; // Falls keine Einheit vorhanden
    }
    
    /**
     * ================================================================
     * HAUPTPROGRAMM: EXCEL EXPORT
     * ================================================================
     */
    
    async function HauptprogrammExcelExport() {
        try {
            log('🚀 Starte Monatsbericht...', 'info');
            
            // ✅ Email-Konfiguration aus Datenpunkten lesen
            const absenderState = getState(EmailAbsenderDatenpunkt);
            const empfaengerState = getState(EmailEmpfaengerDatenpunkt);
            const betreffState = getState(EmailBetreffDatenpunkt);
            const vollJahresState = getState(VollJahresDatenDatenpunkt);
    
            if (!absenderState || !absenderState.val) {
                log(`❌ Email-Absender-Datenpunkt "${EmailAbsenderDatenpunkt}" nicht gefunden oder leer!`, 'error');
                return;
            }
            if (!empfaengerState || !empfaengerState.val) {
                log(`❌ Email-Empfänger-Datenpunkt "${EmailEmpfaengerDatenpunkt}" nicht gefunden oder leer!`, 'error');
                return;
            }
            if (!betreffState || !betreffState.val) {
                log(`❌ Email-Betreff-Datenpunkt "${EmailBetreffDatenpunkt}" nicht gefunden oder leer!`, 'error');
                return;
            }
    
            const emailAbsender = absenderState.val.toString();
            const emailEmpfaenger = empfaengerState.val.toString();
            const emailBetreff = betreffState.val.toString();
            const vollJahresDaten = vollJahresState?.val?.toString().toUpperCase();
    
            // ✅ Neue Jahresausgabe-Steuerung
            let jahreZurueck = 0; // Standard: normaler Modus
            if (vollJahresDaten === '1' || vollJahresDaten === '2' || vollJahresDaten === '3') {
                jahreZurueck = parseInt(vollJahresDaten, 10);
            }
    
            log(`✅ Email von: ${emailAbsender} → an: ${emailEmpfaenger}`, 'info');
            log(`✅ Betreff: ${emailBetreff}`, 'info');
            log(`✅ Jahresausgabe-Modus: ${jahreZurueck > 0 ? jahreZurueck + ' Jahre' : 'Normal'}`, 'info');
    
            // ✅ Zeitraum berechnen
            const jetzt = new Date();
            let startDatum, endDatum;
    
            if (jahreZurueck > 0) {
                // X Jahre zurück
                startDatum = new Date(jetzt);
                startDatum.setFullYear(startDatum.getFullYear() - jahreZurueck);
                startDatum.setHours(0, 0, 0, 0);
                endDatum = new Date(jetzt);
                endDatum.setHours(23, 59, 59, 999);
                log(`📅 Zeitraum: Letzte ${jahreZurueck} Jahr(e) (${startDatum.toLocaleDateString()} bis ${endDatum.toLocaleDateString()})`, 'info');
            } else {
                // Prüfen, ob automatisch (1. des Monats) oder manuell
                const istAutomatisch = jetzt.getDate() === 1 && jetzt.getHours() === 0 && jetzt.getMinutes() <= 10;
                
                if (istAutomatisch) {
                    // Vormonat
                    startDatum = new Date(jetzt.getFullYear(), jetzt.getMonth() - 1, 1);
                    endDatum = new Date(jetzt.getFullYear(), jetzt.getMonth(), 0);
                    endDatum.setHours(23, 59, 59, 999);
                    log(`📅 Zeitraum: Vormonat (${startDatum.toLocaleDateString()} bis ${endDatum.toLocaleDateString()})`, 'info');
                } else {
                    // Aktueller Monat (manueller Start)
                    startDatum = new Date(jetzt.getFullYear(), jetzt.getMonth(), 1);
                    endDatum = new Date(jetzt);
                    endDatum.setHours(23, 59, 59, 999);
                    log(`📅 Zeitraum: Aktueller Monat (${startDatum.toLocaleDateString()} bis heute)`, 'info');
                }
            }
    
            // ✅ Für jeden Datenpunkt Historie abrufen + aggregieren
            alleAggregiertenDaten = {}; // Lokale Variable
    
            for (const dp of Datenpunkte) {
                log(`⏳ Verarbeite Datenpunkt: ${dp.id} → Spalte: "${dp.spaltenName}" (Aggregation: ${dp.agg})`, 'info');
    
                let historieDaten;
                try {
                    // ✅ Dynamische Anzahl von Werten basierend auf Zeitraum
                    const geschätzteTage = Math.ceil((endDatum.getTime() - startDatum.getTime()) / (1000 * 60 * 60 * 24));
                    const geschätzteAnzahl = geschätzteTage * 10; // Annahme: max. 10 Werte pro Tag
                    const count = Math.max(20000, geschätzteAnzahl);
    
                    historieDaten = await holeHistorie(dp.id, startDatum, endDatum); // Kein count mehr
                } catch (fehler) {
                    log(`⚠️ Fehler beim Abruf von ${dp.id}: ${fehler.message}`, 'warn');
                    continue;
                }
    
                if (!historieDaten || historieDaten.length === 0) {
                    log(`⚠️ Keine Daten für ${dp.id} gefunden.`, 'warn');
                    continue;
                }
    
                // Gruppieren nach Tag
                const gruppiertNachTag = {};
                historieDaten.forEach(eintrag => {
                    if (eintrag && eintrag.val !== null && !isNaN(eintrag.val)) {
                        const eintragDatum = new Date(eintrag.ts);
                        const tagSchluessel = `${eintragDatum.getFullYear()}-${(eintragDatum.getMonth() + 1).toString().padStart(2, '0')}-${eintragDatum.getDate().toString().padStart(2, '0')}`;
                        if (!gruppiertNachTag[tagSchluessel]) gruppiertNachTag[tagSchluessel] = [];
                        gruppiertNachTag[tagSchluessel].push(eintrag.val);
                    }
                });
    
                // Pro Tag aggregieren
                for (const [tagSchluessel, werte] of Object.entries(gruppiertNachTag)) {
                    const bereinigteWerte = bereinigeWerte(werte);
                    if (bereinigteWerte.length === 0) continue;
    
                    let aggregierterWert;
                    const aggTyp = (dp.agg || 'max').toLowerCase();
    
                    switch (aggTyp) {
                        case 'max':
                            aggregierterWert = Math.max(...bereinigteWerte);
                            break;
                        case 'min':
                            aggregierterWert = Math.min(...bereinigteWerte);
                            break;
                        case 'avg':
                            aggregierterWert = bereinigteWerte.reduce((summe, wert) => summe + wert, 0) / bereinigteWerte.length;
                            break;
                        case 'sum':
                            aggregierterWert = bereinigteWerte.reduce((summe, wert) => summe + wert, 0);
                            break;
                        default:
                            log(`⚠️ Unbekannter Aggregationstyp "${aggTyp}" für ${dp.id}. Nutze 'max'.`, 'warn');
                            aggregierterWert = Math.max(...bereinigteWerte);
                    }
    
                    // Multiplikator anwenden
                    aggregierterWert = aggregierterWert * (dp.multiplikator || 1);
    
                    // Eindeutigen Schlüssel erstellen
                    const eindeutigerSchluessel = `${dp.id}__${dp.agg}__${dp.spaltenName}`;
                    if (!alleAggregiertenDaten[tagSchluessel]) alleAggregiertenDaten[tagSchluessel] = {};
                    alleAggregiertenDaten[tagSchluessel][eindeutigerSchluessel] = aggregierterWert;
                }
            }
    
            if (Object.keys(alleAggregiertenDaten).length === 0) {
                log('❌ Keine Daten für irgendeinen Datenpunkt gefunden!', 'error');
                return;
            }
    
            // ✅ Sortierte Tage
            sortierteTage = Object.keys(alleAggregiertenDaten).sort();
    
            log(`✅ Daten für ${sortierteTage.length} Tage aggregiert.`, 'info');
    
            // ✅ Excel-Datei erstellen
            const excelBase64 = await erstelleExcelDatei();
            log('✅ Excel-Datei mit formatierter Tabelle erfolgreich erstellt.', 'info');
    
            // ✅ Intelligenter Dateiname
            const zeitstempel = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
            let dateiname;
            if (jahreZurueck > 0) {
                dateiname = `${jahreZurueck}-Jahresbericht_${zeitstempel}.xlsx`;
            } else {
                // Prüfen, ob automatisch (1. des Monats) oder manuell
                const jetzt = new Date();
                const istAutomatisch = jetzt.getDate() === 1 && jetzt.getHours() === 0 && jetzt.getMinutes() <= 10;
                if (istAutomatisch) {
                    dateiname = `Vormonatsbericht_${zeitstempel}.xlsx`;
                } else {
                    dateiname = `Monatsbericht_${zeitstempel}.xlsx`;
                }
            }
    
            // ✅ E-Mail senden
            sendeEmailMitAnhang(excelBase64, dateiname, emailAbsender, emailEmpfaenger, emailBetreff);
    
            // ✅ Timer bereinigen (ohne Skript zu beenden)
            setTimeout(() => {
                log('✅ Aktuelle Ausführung beendet - Scheduler bleibt aktiv', 'info');
                // Timer bereinigen
                for (let i = 1; i < 1000; i++) {
                    clearTimeout(i);
                    clearInterval(i);
                }
            }, 3000);
    
        } catch (fehler) {
            log(`❌ Fehler im Hauptskript: ${fehler.message}`, 'error');
        }
    }
    
    // Funktion: Echte Excel-Datei mit formatierter Tabelle erstellen
    async function erstelleExcelDatei() {
        try {
            log('⏳ Erstelle Excel-Datei...', 'info');
            
            // Erstelle ein neues Workbook
            const workbook = new ExcelJS.Workbook();
    
            // ✅ Dynamischen Tabellennamen erstellen
            let tabellenName = "Daten";
            if (sortierteTage.length > 0) {
                const erstesDatum = sortierteTage[0];
                const [jahr, monat] = erstesDatum.split('-');
                tabellenName = `Daten ${jahr}-${monat.padStart(2, '0')}`;
            }
    
            // ✅ Arbeitsblatt erstellen
            const worksheet = workbook.addWorksheet(tabellenName);
    
            // Header-Zeile
            const ueberschriften = ['Datum', ...Datenpunkte.map(dp => dp.spaltenName)];
            worksheet.addRow(ueberschriften);
    
            // ✅ Daten sammeln für Tabelle
            const tabellenDaten = [];
    
            // Datenzeilen
            for (const tagSchluessel of sortierteTage) {
                const teile = tagSchluessel.split('-').map(Number);
                const datumObj = new Date(teile[0], teile[1] - 1, teile[2]);
                const zeile = [formatiereDatumFuerExcel(datumObj)];
    
                for (let dpIndex = 0; dpIndex < Datenpunkte.length; dpIndex++) {
                    const dp = Datenpunkte[dpIndex];
                    const eindeutigerSchluessel = `${dp.id}__${dp.agg}__${dp.spaltenName}`;
                    let wert = alleAggregiertenDaten[tagSchluessel]?.[eindeutigerSchluessel];
    
                    if (wert !== undefined) {
                        // ✅ ACHTUNG: Multiplikator wurde bereits in Hauptfunktion angewendet!
                        // Hier nur noch Nachkommastellen anwenden
                        const nachkommastellen = dp.nachkommastellen !== undefined ? dp.nachkommastellen : 2;
                        wert = parseFloat(wert.toFixed(nachkommastellen));
                        zeile.push(wert);
                    } else {
                        // Auffüllen mit Null, wenn konfiguriert
                        if (dp.fuellenMitNull) {
                            zeile.push(0);
                        } else {
                            zeile.push(null);
                        }
                    }
                }
                worksheet.addRow(zeile);
                tabellenDaten.push(zeile);
            }
    
            log(`✅ ${sortierteTage.length} Datenzeilen vorbereitet`, 'info');
    
            // ✅ ECHTE EXCEL-TABELLE ERSTELLEN
            if (tabellenDaten.length > 0) {
                worksheet.addTable({
                    name: 'DatenTabelle',
                    ref: 'A1',
                    headerRow: true,
                    totalsRow: false,
                    style: {
                        theme: 'TableStyleMedium2',
                        showRowStripes: true,
                        showColumnStripes: false
                    },
                    columns: ueberschriften.map(name => ({ name: name, filterButton: true })),
                    rows: tabellenDaten
                });
            }
    
            // ✅ Spaltenbreiten und Zahlenformate
            worksheet.columns.forEach(column => {
                column.width = 20;
            });
    
            // Zahlenformate für Daten-Spalten (ab Spalte B)
            for (let i = 1; i < ueberschriften.length; i++) {
                const dp = Datenpunkte[i - 1]; // i-1 weil erste Spalte Datum ist
                const nachkommastellen = dp.nachkommastellen !== undefined ? dp.nachkommastellen : 2;
                const einheit = leseEinheitAusDatenpunkt(dp.id);
                const column = worksheet.getColumn(i + 1); // +1 weil Excel-Spalten bei A=1 beginnen
                column.numFmt = nachkommastellen === 0 
                    ? `0 " ${einheit}"` 
                    : `0.${'0'.repeat(nachkommastellen)} " ${einheit}"`;
            }
    
            // ✅ Zweites Blatt: Zusammenfassung
            const zusammenfassungBlatt = workbook.addWorksheet('Zusammenfassung');
    
            // Überschrift in Zelle A1
            const titelZelle = zusammenfassungBlatt.getCell('A1');
            titelZelle.value = 'Zusammenfassung';
            titelZelle.font = { bold: true, size: 14, color: { argb: 'FFFFFFFF' } };
            titelZelle.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF4472C4' } };
    
            // Leerzeile in A2
            zusammenfassungBlatt.getCell('A2').value = '';
    
            // Header-Zeile in Zeile 3
            const headerZellen = ['A3', 'B3', 'C3', 'D3', 'E3'];
            const headerWerte = ['Datenpunkt', 'Minimum', 'Maximum', 'Durchschnitt', 'Summe'];
            
            for (let i = 0; i < headerZellen.length; i++) {
                const zelle = zusammenfassungBlatt.getCell(headerZellen[i]);
                zelle.value = headerWerte[i];
                zelle.font = { bold: true };
                zelle.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFD9E1F2' } };
            }
    
            // Daten für Zusammenfassung sammeln und eintragen
            let aktuelleZeile = 4; // Start ab Zeile 4
            
            for (let i = 0; i < Datenpunkte.length; i++) {
                const dp = Datenpunkte[i];
                
                // Werte sammeln und bereinigen
                const werte = [];
                for (const tag of sortierteTage) {
                    const eindeutigerSchluessel = `${dp.id}__${dp.agg}__${dp.spaltenName}`;
                    let val = alleAggregiertenDaten[tag]?.[eindeutigerSchluessel];
                    if (val !== undefined && val !== null && !isNaN(val)) {
                        // ✅ ACHTUNG: Multiplikator wurde bereits in Hauptfunktion angewendet!
                        // Hier nur noch Nachkommastellen anwenden
                        const nachkommastellen = dp.nachkommastellen !== undefined ? dp.nachkommastellen : 2;
                        val = parseFloat(val.toFixed(nachkommastellen));
                        werte.push(val);
                    }
                }
    
                if (werte.length === 0) {
                    log(`🔍   Keine gültigen Werte für ${dp.spaltenName}`, 'warn');
                    continue;
                }
    
                // Berechnungen
                const min = Math.min(...werte);
                const max = Math.max(...werte);
                const avg = werte.reduce((sum, val) => sum + val, 0) / werte.length;
                const summe = werte.reduce((sum, val) => sum + val, 0);
    
                // Werte direkt in Zellen schreiben
                zusammenfassungBlatt.getCell(`A${aktuelleZeile}`).value = dp.spaltenName;
                zusammenfassungBlatt.getCell(`B${aktuelleZeile}`).value = min;
                zusammenfassungBlatt.getCell(`C${aktuelleZeile}`).value = max;
                zusammenfassungBlatt.getCell(`D${aktuelleZeile}`).value = avg;
                zusammenfassungBlatt.getCell(`E${aktuelleZeile}`).value = summe;
    
               // Zahlen formatieren
                for (let col of ['B', 'C', 'D', 'E']) {
                    const zelle = zusammenfassungBlatt.getCell(`${col}${aktuelleZeile}`);
                    const nachkommastellen = dp.nachkommastellen !== undefined ? dp.nachkommastellen : 2;
                    const einheit = leseEinheitAusDatenpunkt(dp.id);
                    zelle.numFmt = nachkommastellen === 0 
                        ? `0 " ${einheit}"` 
                        : `0.${'0'.repeat(nachkommastellen)} " ${einheit}"`;
                }
    
                aktuelleZeile++;
            }
    
            // Falls keine Daten gefunden wurden
            if (aktuelleZeile === 4) {
                zusammenfassungBlatt.getCell('A4').value = 'Keine Daten verfügbar';
                zusammenfassungBlatt.getCell('A4').font = { italic: true, color: { argb: 'FF808080' } };
            }
    
            // Spaltenbreiten für Zusammenfassung
            for (let col of ['A', 'B', 'C', 'D', 'E']) {
                zusammenfassungBlatt.getColumn(col).width = 20;
            }
    
            // ✅ IN BINÄREN BUFFER UMWANDELN
            const uint8Array = await workbook.xlsx.writeBuffer();
            const buffer = Buffer.from(uint8Array);
            const base64 = buffer.toString('base64');
            
            log('✅ Excel-Datei erfolgreich erstellt', 'info');
            return base64;
    
        } catch (fehler) {
            log(`❌ Fehler beim Erstellen der Excel-Datei: ${fehler.message}`, 'error');
            log(`❌ Stack: ${fehler.stack}`, 'error');
            throw fehler;
        }
    }
    
    // Funktion: E-Mail senden
    function sendeEmailMitAnhang(excelBase64, dateiname, absenderAdresse, empfaengerAdresse, betreff) {
        const text = `Hallo,\n\nIm Anhang findest du den monatlichen Aggregationsbericht als formatierte Excel-Tabelle.\n\nAutomatisch generiert von ioBroker.\nDatum: ${new Date().toLocaleString()}`;
    
        sendTo(EmailAdapterInstanz, {
            from: absenderAdresse,
            to: empfaengerAdresse,
            subject: betreff,
            text: text,
            attachments: [{
                filename: dateiname,
                content: excelBase64,
                encoding: 'base64',
                contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
            }]
        }, (fehler, ergebnis) => {
            if (fehler && typeof fehler === 'object' && fehler.result && fehler.result.includes('250')) {
                log(`✅ E-Mail erfolgreich gesendet (Server: ${fehler.result})`, 'info');
            } else if (fehler) {
                log(`❌ E-Mail-Fehler: ${JSON.stringify(fehler)}`, 'error');
            } else {
                log(`✅ E-Mail mit Anhang "${dateiname}" gesendet.`, 'info');
            }
        });
    }
    
    /**
     * ================================================================
     * SCHEDULE + MANUELLEN START
     * ================================================================
     */
    
    // Zeitplanung: 1. Tag jedes Monats um 00:05 Uhr
    schedule('5 0 1 * *', HauptprogrammExcelExport);
    
    // Manueller Start: Nur wenn Skript neu geladen wird und Adapter schon läuft
    if (existsState('system.adapter.javascript.0.uptime') && 
        getState('system.adapter.javascript.0.uptime').val > 60) {
        log('🔁 Manueller Start des Skripts erkannt', 'info');
        setTimeout(HauptprogrammExcelExport, 2000); // Kleine Verzögerung für sicheres Starten
    }
    
    log('✅ Skript geladen - bereit für manuellen oder zeitgesteuerten Start', 'info');
    
    bahnuhrB 1 Antwort Letzte Antwort
    1
    • BertDerKleineB BertDerKleine

      Hallo,
      hier ein recht banaler Anwendungsfall. Ich möchte gerne für einige Datenpunkte auch außerhalb von iobroker bestimmte Kerndaten haben, um damit z.B. in Excel rumspielen zu können.

      Dabei will ich nicht jedes Datenfragment aus der History-DB pro Datenpunkt haben, sondern mir reicht hier ein Wert pro Tag, i.d.R. der Höchste Zählerstand eines Wertes.

      Das ganze will ich einfach automatisch einmal pro Monat zugesendet bekommen per Email.

      Als Zusatzfeature, das ich über einen extra Datenpunkt steuern kann, kann das Ding auch sofort ein ganzes Jahr in dieser Form exportieren.

      Vielleicht habt Ihr ja ähnliche Anforderungen, dann könnt Ihr hierdrauf aufsetzen.

      Ich hoffe, alles ist hinreichend sprechend und auskommentiert, dass es gut lesbar ist.
      Das Logging kann man ja leicht entfernen, wenn es nervt. :-)

      /**
       * ================================================================
       * 📊 IOBROKER MONATLICHER HISTORIEN-BERICHT ALS EXCEL PER E-MAIL
       * ================================================================
       *
       * 🎯 ZWECK DES SKRIPTS:
       * Dieses Skript erstellt automatisch am Anfang jedes Monats eine Excel-Datei
       * mit allen Tageswerten des VORMONATS für konfigurierte Datenpunkte und sendet sie per E-Mail.
       * Alternativ kann es auch manuell gestartet werden, dann werden die Daten des AKTUELLEN Monats ausgegeben.
       * Bei Bedarf kann auch ein Jahresbericht (365 Tage) erstellt werden.
       *
       * ✅ WAS ES TUT:
       * 1. Liest Email-Absender und -Empfänger aus konfigurierbaren Datenpunkten.
       * 2. Ermittelt automatisch den richtigen Zeitraum (Vormonat, aktueller Monat oder 365 Tage).
       * 3. Holt für jeden konfigurierten Datenpunkt die Historie des Zeitraums.
       * 4. Aggregiert die Werte pro Tag (konfigurierbar pro Spalte).
       * 5. Wendet optional Multiplikator an (z. B. 1000 für Liter statt m³).
       * 6. Legt fest, wieviele Nachkommastellen verwendet werden.
       * 7. Kann leere Werte mit 0 auffüllen (optional).
       * 8. Erstellt eine ECHTE Excel-Datei (.xlsx) mit formatierter Tabelle.
       * 9. Sendet die Excel-Datei per E-Mail mit konfigurierbarem Betreff.
       *
       *  Gebastelt 2025 von BertDerKleine
       * 
       * 🧩 KONFIGURIERBAR:
       * - Liste der Datenpunkte mit:
       *   - id: Datenpunkt-Name
       *   - agg: 'max|min|avg|sum'
       *   - spaltenName: Anzeigename in Excel
       *   - nachkommastellen: Anzahl der Nachkommastellen (optional, default: 2)
       *   - multiplikator: Faktor für Multiplikation (optional, default: 1)
       *   - fuellenMitNull: Ob leere Werte mit 0 gefüllt werden sollen (optional, default: false)
       *
       * 📥 VORAUSSETZUNGEN:
       * - History-Adapter (history.0, sql.0, etc.) muss aktiv sein
       * - Email-Adapter (email.0) muss konfiguriert und verbunden sein
       * - Folgende Konfigurations-Datenpunkte müssen existieren:
       *   - 0_userdata.0.Email-Absender (string)
       *   - 0_userdata.0.Email-Empfaenger (string)
       *   - 0_userdata.0.Email-Betreff (string)
       *   - 0_userdata.0.VollJahresDaten (zahl: 0,1,2,3 Bei 0 läuft das Skript normal, bei 1/2/3 werden die Daten von 1-3 Jahren ausgegeben)
       *   - exceljs-Library muss installiert sein:
       *   → Im ioBroker JavaScript-Adapter in den Instanzeinstellungen unter
       *      "additional npm modules" eintragen: "exceljs"
       *
       * 📤 AUSGABE:
       * - Echte Excel-Datei (.xlsx) mit formatierter Tabelle
       * - Datum im Format TT.MM.JJJJ
       * - Zahlen als echte Excel-Zahlen (kein Text)
       * - Automatische Spaltenbreite
       * - Abwechselnde Zeilenfarben (Zebra-Stripes)
       * - Auto-Filter in der Kopfzeile
       * - Zwei Blätter: Daten + Zusammenfassung
       *
       * 🚀 AUTOMATISIERUNG:
       * Plane dieses Skript am 1. jedes Monats um 00:05 Uhr:
       *
       * ================================================================
       * 🔧 KONFIGURATION – HIER ANPASSEN!
       * ================================================================
       */
      
      // ✅ exceljs-Library laden (muss installiert sein: exceljs in "additional npm modules")
      try {
          const ExcelJS = require('exceljs');
          log('✅ ExcelJS erfolgreich geladen!', 'info');
      } catch (e) {
          log(`❌ Fehler beim Laden von ExcelJS: ${e.message}`, 'error');
          log('💡 Lösung: ExcelJS in den Instanzeinstellungen des JavaScript-Adapters unter "additional npm modules" hinzufügen', 'error');
          return;
      }
      
      const ExcelJS = require('exceljs');
      
      // Liste der zu verarbeitenden Datenpunkte
      // Jeder Eintrag: { id: '...', agg: 'max|min|avg|sum', spaltenName: '...', nachkommastellen?: number, multiplikator?: number, fuellenMitNull?: boolean }
      const Datenpunkte = [
          { id: 'alias.0.Aussentemperatur', agg: 'max', spaltenName: 'Höchste Aussentemperatur', nachkommastellen: 1, multiplikator: 1, fuellenMitNull: false },
          { id: 'alias.0.Aussentemperatur', agg: 'min', spaltenName: 'Niedrigste Aussentemperatur', nachkommastellen: 1, multiplikator: 1, fuellenMitNull: false },
          { id: 'alias.0.Regen', agg: 'max', spaltenName: 'Regenmenge', nachkommastellen: 1, multiplikator: 1, fuellenMitNull: true },
          { id: 'alias.0.GasH', agg: 'max', spaltenName: 'Gas für Heizung', nachkommastellen: 1, multiplikator: 1, fuellenMitNull: true },
          { id: 'alias.0.GasW', agg: 'max', spaltenName: 'Gas für Warmwasser', nachkommastellen: 1, multiplikator: 1, fuellenMitNull: true },
          { id: 'statistics.0.save.sumDelta.alias.0.Wasser.day', agg: 'max', spaltenName: 'Wasserverbrauch', nachkommastellen: 0, multiplikator: 1000, fuellenMitNull: true }, // in Liter
          { id: 'statistics.0.save.sumDelta.alias.0.PV_Erzeugung.day', agg: 'max', spaltenName: 'Strom-PV-Erzeugung', nachkommastellen: 0, multiplikator: 1, fuellenMitNull: false },
          { id: 'statistics.0.save.sumDelta.alias.0.Netzbezug.day', agg: 'max', spaltenName: 'Strom-Netzbezug', nachkommastellen: 2, multiplikator: 1, fuellenMitNull: false },
          { id: 'statistics.0.save.sumDelta.alias.0.Netzeinspeisung.day', agg: 'max', spaltenName: 'Strom-Netzeinspeisung', nachkommastellen: 2, multiplikator: 1, fuellenMitNull: false },
          { id: 'statistics.0.save.sumDelta.0_userdata.0.Verbrauch.day', agg: 'max', spaltenName: 'Strom-Verbrauch', nachkommastellen: 2, multiplikator: 1, fuellenMitNull: false }
      ];
      
      // Datenpunkte für Email-Konfiguration
      const EmailAbsenderDatenpunkt = '0_userdata.0.Email-Absender';
      const EmailEmpfaengerDatenpunkt = '0_userdata.0.Email-Empfaenger';
      const EmailBetreffDatenpunkt = '0_userdata.0.Email-Betreff';
      const VollJahresDatenDatenpunkt = '0_userdata.0.VollJahresDaten'; // "YES" oder "NO"
      
      // Adapter-Instanzen (anpassen, falls abweichend)
      const EmailAdapterInstanz = 'email.0';
      const HistoryAdapterInstanz = 'history.0'; // z. B. 'sql.0', 'influxdb.0'
      
      // Globale Variablen (lokal im Skript)
      let alleAggregiertenDaten = {};
      let sortierteTage = [];
      
      /**
       * ================================================================
       * HILFSFUNKTIONEN
       * ================================================================
       */
      
      function formatiereDatumFuerExcel(datum) {
          const jahr = datum.getFullYear();
          const monat = (datum.getMonth() + 1).toString().padStart(2, '0');
          const tag = datum.getDate().toString().padStart(2, '0');
          return `${tag}.${monat}.${jahr}`;
      }
      
      function bereinigeWerte(werte) {
          return werte.filter(wert => wert !== null && wert !== undefined && !isNaN(wert));
      }
      
      // Funktion: Historische Daten abrufen (optimiert für lange Zeiträume)
      async function holeHistorie(id, start, ende) {
          // ✅ Schätzung: wie viele Werte könnten es sein?
          const geschätzteTage = Math.ceil((ende.getTime() - start.getTime()) / (24 * 60 * 60 * 1000));
          const geschätzteWerteProTag = 150; // Anpassen je nach deinen Daten
          const geschätzteAnzahl = geschätzteTage * geschätzteWerteProTag;
          
          // ✅ Sicherstellen, dass genug Werte abgerufen werden
          const count = Math.min(200000, Math.max(50000, geschätzteAnzahl)); // Max 200.000, Min 50.000
      
          return new Promise((resolve, reject) => {
              sendTo(HistoryAdapterInstanz, 'getHistory', {
                  id: id,
                  options: { 
                      start: start.getTime(), 
                      end: ende.getTime(), 
                      aggregate: 'none', 
                      count: count 
                  }
              }, (ergebnis) => {
                  if (!ergebnis || !ergebnis.result || ergebnis.error) {
                      reject(new Error(ergebnis?.error || 'Unbekannter Fehler beim History-Abruf'));
                  } else {
                      resolve(ergebnis.result);
                  }
              });
          });
      }
      
      // Funktion: Einheit aus Datenpunkt auslesen
      function leseEinheitAusDatenpunkt(dpId) {
          try {
              const obj = getObject(dpId);
              if (obj && obj.common && obj.common.unit) {
                  return obj.common.unit;
              }
          } catch (e) {
              log(`⚠️ Fehler beim Lesen der Einheit von ${dpId}: ${e.message}`, 'warn');
          }
          return ''; // Falls keine Einheit vorhanden
      }
      
      /**
       * ================================================================
       * HAUPTPROGRAMM: EXCEL EXPORT
       * ================================================================
       */
      
      async function HauptprogrammExcelExport() {
          try {
              log('🚀 Starte Monatsbericht...', 'info');
              
              // ✅ Email-Konfiguration aus Datenpunkten lesen
              const absenderState = getState(EmailAbsenderDatenpunkt);
              const empfaengerState = getState(EmailEmpfaengerDatenpunkt);
              const betreffState = getState(EmailBetreffDatenpunkt);
              const vollJahresState = getState(VollJahresDatenDatenpunkt);
      
              if (!absenderState || !absenderState.val) {
                  log(`❌ Email-Absender-Datenpunkt "${EmailAbsenderDatenpunkt}" nicht gefunden oder leer!`, 'error');
                  return;
              }
              if (!empfaengerState || !empfaengerState.val) {
                  log(`❌ Email-Empfänger-Datenpunkt "${EmailEmpfaengerDatenpunkt}" nicht gefunden oder leer!`, 'error');
                  return;
              }
              if (!betreffState || !betreffState.val) {
                  log(`❌ Email-Betreff-Datenpunkt "${EmailBetreffDatenpunkt}" nicht gefunden oder leer!`, 'error');
                  return;
              }
      
              const emailAbsender = absenderState.val.toString();
              const emailEmpfaenger = empfaengerState.val.toString();
              const emailBetreff = betreffState.val.toString();
              const vollJahresDaten = vollJahresState?.val?.toString().toUpperCase();
      
              // ✅ Neue Jahresausgabe-Steuerung
              let jahreZurueck = 0; // Standard: normaler Modus
              if (vollJahresDaten === '1' || vollJahresDaten === '2' || vollJahresDaten === '3') {
                  jahreZurueck = parseInt(vollJahresDaten, 10);
              }
      
              log(`✅ Email von: ${emailAbsender} → an: ${emailEmpfaenger}`, 'info');
              log(`✅ Betreff: ${emailBetreff}`, 'info');
              log(`✅ Jahresausgabe-Modus: ${jahreZurueck > 0 ? jahreZurueck + ' Jahre' : 'Normal'}`, 'info');
      
              // ✅ Zeitraum berechnen
              const jetzt = new Date();
              let startDatum, endDatum;
      
              if (jahreZurueck > 0) {
                  // X Jahre zurück
                  startDatum = new Date(jetzt);
                  startDatum.setFullYear(startDatum.getFullYear() - jahreZurueck);
                  startDatum.setHours(0, 0, 0, 0);
                  endDatum = new Date(jetzt);
                  endDatum.setHours(23, 59, 59, 999);
                  log(`📅 Zeitraum: Letzte ${jahreZurueck} Jahr(e) (${startDatum.toLocaleDateString()} bis ${endDatum.toLocaleDateString()})`, 'info');
              } else {
                  // Prüfen, ob automatisch (1. des Monats) oder manuell
                  const istAutomatisch = jetzt.getDate() === 1 && jetzt.getHours() === 0 && jetzt.getMinutes() <= 10;
                  
                  if (istAutomatisch) {
                      // Vormonat
                      startDatum = new Date(jetzt.getFullYear(), jetzt.getMonth() - 1, 1);
                      endDatum = new Date(jetzt.getFullYear(), jetzt.getMonth(), 0);
                      endDatum.setHours(23, 59, 59, 999);
                      log(`📅 Zeitraum: Vormonat (${startDatum.toLocaleDateString()} bis ${endDatum.toLocaleDateString()})`, 'info');
                  } else {
                      // Aktueller Monat (manueller Start)
                      startDatum = new Date(jetzt.getFullYear(), jetzt.getMonth(), 1);
                      endDatum = new Date(jetzt);
                      endDatum.setHours(23, 59, 59, 999);
                      log(`📅 Zeitraum: Aktueller Monat (${startDatum.toLocaleDateString()} bis heute)`, 'info');
                  }
              }
      
              // ✅ Für jeden Datenpunkt Historie abrufen + aggregieren
              alleAggregiertenDaten = {}; // Lokale Variable
      
              for (const dp of Datenpunkte) {
                  log(`⏳ Verarbeite Datenpunkt: ${dp.id} → Spalte: "${dp.spaltenName}" (Aggregation: ${dp.agg})`, 'info');
      
                  let historieDaten;
                  try {
                      // ✅ Dynamische Anzahl von Werten basierend auf Zeitraum
                      const geschätzteTage = Math.ceil((endDatum.getTime() - startDatum.getTime()) / (1000 * 60 * 60 * 24));
                      const geschätzteAnzahl = geschätzteTage * 10; // Annahme: max. 10 Werte pro Tag
                      const count = Math.max(20000, geschätzteAnzahl);
      
                      historieDaten = await holeHistorie(dp.id, startDatum, endDatum); // Kein count mehr
                  } catch (fehler) {
                      log(`⚠️ Fehler beim Abruf von ${dp.id}: ${fehler.message}`, 'warn');
                      continue;
                  }
      
                  if (!historieDaten || historieDaten.length === 0) {
                      log(`⚠️ Keine Daten für ${dp.id} gefunden.`, 'warn');
                      continue;
                  }
      
                  // Gruppieren nach Tag
                  const gruppiertNachTag = {};
                  historieDaten.forEach(eintrag => {
                      if (eintrag && eintrag.val !== null && !isNaN(eintrag.val)) {
                          const eintragDatum = new Date(eintrag.ts);
                          const tagSchluessel = `${eintragDatum.getFullYear()}-${(eintragDatum.getMonth() + 1).toString().padStart(2, '0')}-${eintragDatum.getDate().toString().padStart(2, '0')}`;
                          if (!gruppiertNachTag[tagSchluessel]) gruppiertNachTag[tagSchluessel] = [];
                          gruppiertNachTag[tagSchluessel].push(eintrag.val);
                      }
                  });
      
                  // Pro Tag aggregieren
                  for (const [tagSchluessel, werte] of Object.entries(gruppiertNachTag)) {
                      const bereinigteWerte = bereinigeWerte(werte);
                      if (bereinigteWerte.length === 0) continue;
      
                      let aggregierterWert;
                      const aggTyp = (dp.agg || 'max').toLowerCase();
      
                      switch (aggTyp) {
                          case 'max':
                              aggregierterWert = Math.max(...bereinigteWerte);
                              break;
                          case 'min':
                              aggregierterWert = Math.min(...bereinigteWerte);
                              break;
                          case 'avg':
                              aggregierterWert = bereinigteWerte.reduce((summe, wert) => summe + wert, 0) / bereinigteWerte.length;
                              break;
                          case 'sum':
                              aggregierterWert = bereinigteWerte.reduce((summe, wert) => summe + wert, 0);
                              break;
                          default:
                              log(`⚠️ Unbekannter Aggregationstyp "${aggTyp}" für ${dp.id}. Nutze 'max'.`, 'warn');
                              aggregierterWert = Math.max(...bereinigteWerte);
                      }
      
                      // Multiplikator anwenden
                      aggregierterWert = aggregierterWert * (dp.multiplikator || 1);
      
                      // Eindeutigen Schlüssel erstellen
                      const eindeutigerSchluessel = `${dp.id}__${dp.agg}__${dp.spaltenName}`;
                      if (!alleAggregiertenDaten[tagSchluessel]) alleAggregiertenDaten[tagSchluessel] = {};
                      alleAggregiertenDaten[tagSchluessel][eindeutigerSchluessel] = aggregierterWert;
                  }
              }
      
              if (Object.keys(alleAggregiertenDaten).length === 0) {
                  log('❌ Keine Daten für irgendeinen Datenpunkt gefunden!', 'error');
                  return;
              }
      
              // ✅ Sortierte Tage
              sortierteTage = Object.keys(alleAggregiertenDaten).sort();
      
              log(`✅ Daten für ${sortierteTage.length} Tage aggregiert.`, 'info');
      
              // ✅ Excel-Datei erstellen
              const excelBase64 = await erstelleExcelDatei();
              log('✅ Excel-Datei mit formatierter Tabelle erfolgreich erstellt.', 'info');
      
              // ✅ Intelligenter Dateiname
              const zeitstempel = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
              let dateiname;
              if (jahreZurueck > 0) {
                  dateiname = `${jahreZurueck}-Jahresbericht_${zeitstempel}.xlsx`;
              } else {
                  // Prüfen, ob automatisch (1. des Monats) oder manuell
                  const jetzt = new Date();
                  const istAutomatisch = jetzt.getDate() === 1 && jetzt.getHours() === 0 && jetzt.getMinutes() <= 10;
                  if (istAutomatisch) {
                      dateiname = `Vormonatsbericht_${zeitstempel}.xlsx`;
                  } else {
                      dateiname = `Monatsbericht_${zeitstempel}.xlsx`;
                  }
              }
      
              // ✅ E-Mail senden
              sendeEmailMitAnhang(excelBase64, dateiname, emailAbsender, emailEmpfaenger, emailBetreff);
      
              // ✅ Timer bereinigen (ohne Skript zu beenden)
              setTimeout(() => {
                  log('✅ Aktuelle Ausführung beendet - Scheduler bleibt aktiv', 'info');
                  // Timer bereinigen
                  for (let i = 1; i < 1000; i++) {
                      clearTimeout(i);
                      clearInterval(i);
                  }
              }, 3000);
      
          } catch (fehler) {
              log(`❌ Fehler im Hauptskript: ${fehler.message}`, 'error');
          }
      }
      
      // Funktion: Echte Excel-Datei mit formatierter Tabelle erstellen
      async function erstelleExcelDatei() {
          try {
              log('⏳ Erstelle Excel-Datei...', 'info');
              
              // Erstelle ein neues Workbook
              const workbook = new ExcelJS.Workbook();
      
              // ✅ Dynamischen Tabellennamen erstellen
              let tabellenName = "Daten";
              if (sortierteTage.length > 0) {
                  const erstesDatum = sortierteTage[0];
                  const [jahr, monat] = erstesDatum.split('-');
                  tabellenName = `Daten ${jahr}-${monat.padStart(2, '0')}`;
              }
      
              // ✅ Arbeitsblatt erstellen
              const worksheet = workbook.addWorksheet(tabellenName);
      
              // Header-Zeile
              const ueberschriften = ['Datum', ...Datenpunkte.map(dp => dp.spaltenName)];
              worksheet.addRow(ueberschriften);
      
              // ✅ Daten sammeln für Tabelle
              const tabellenDaten = [];
      
              // Datenzeilen
              for (const tagSchluessel of sortierteTage) {
                  const teile = tagSchluessel.split('-').map(Number);
                  const datumObj = new Date(teile[0], teile[1] - 1, teile[2]);
                  const zeile = [formatiereDatumFuerExcel(datumObj)];
      
                  for (let dpIndex = 0; dpIndex < Datenpunkte.length; dpIndex++) {
                      const dp = Datenpunkte[dpIndex];
                      const eindeutigerSchluessel = `${dp.id}__${dp.agg}__${dp.spaltenName}`;
                      let wert = alleAggregiertenDaten[tagSchluessel]?.[eindeutigerSchluessel];
      
                      if (wert !== undefined) {
                          // ✅ ACHTUNG: Multiplikator wurde bereits in Hauptfunktion angewendet!
                          // Hier nur noch Nachkommastellen anwenden
                          const nachkommastellen = dp.nachkommastellen !== undefined ? dp.nachkommastellen : 2;
                          wert = parseFloat(wert.toFixed(nachkommastellen));
                          zeile.push(wert);
                      } else {
                          // Auffüllen mit Null, wenn konfiguriert
                          if (dp.fuellenMitNull) {
                              zeile.push(0);
                          } else {
                              zeile.push(null);
                          }
                      }
                  }
                  worksheet.addRow(zeile);
                  tabellenDaten.push(zeile);
              }
      
              log(`✅ ${sortierteTage.length} Datenzeilen vorbereitet`, 'info');
      
              // ✅ ECHTE EXCEL-TABELLE ERSTELLEN
              if (tabellenDaten.length > 0) {
                  worksheet.addTable({
                      name: 'DatenTabelle',
                      ref: 'A1',
                      headerRow: true,
                      totalsRow: false,
                      style: {
                          theme: 'TableStyleMedium2',
                          showRowStripes: true,
                          showColumnStripes: false
                      },
                      columns: ueberschriften.map(name => ({ name: name, filterButton: true })),
                      rows: tabellenDaten
                  });
              }
      
              // ✅ Spaltenbreiten und Zahlenformate
              worksheet.columns.forEach(column => {
                  column.width = 20;
              });
      
              // Zahlenformate für Daten-Spalten (ab Spalte B)
              for (let i = 1; i < ueberschriften.length; i++) {
                  const dp = Datenpunkte[i - 1]; // i-1 weil erste Spalte Datum ist
                  const nachkommastellen = dp.nachkommastellen !== undefined ? dp.nachkommastellen : 2;
                  const einheit = leseEinheitAusDatenpunkt(dp.id);
                  const column = worksheet.getColumn(i + 1); // +1 weil Excel-Spalten bei A=1 beginnen
                  column.numFmt = nachkommastellen === 0 
                      ? `0 " ${einheit}"` 
                      : `0.${'0'.repeat(nachkommastellen)} " ${einheit}"`;
              }
      
              // ✅ Zweites Blatt: Zusammenfassung
              const zusammenfassungBlatt = workbook.addWorksheet('Zusammenfassung');
      
              // Überschrift in Zelle A1
              const titelZelle = zusammenfassungBlatt.getCell('A1');
              titelZelle.value = 'Zusammenfassung';
              titelZelle.font = { bold: true, size: 14, color: { argb: 'FFFFFFFF' } };
              titelZelle.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF4472C4' } };
      
              // Leerzeile in A2
              zusammenfassungBlatt.getCell('A2').value = '';
      
              // Header-Zeile in Zeile 3
              const headerZellen = ['A3', 'B3', 'C3', 'D3', 'E3'];
              const headerWerte = ['Datenpunkt', 'Minimum', 'Maximum', 'Durchschnitt', 'Summe'];
              
              for (let i = 0; i < headerZellen.length; i++) {
                  const zelle = zusammenfassungBlatt.getCell(headerZellen[i]);
                  zelle.value = headerWerte[i];
                  zelle.font = { bold: true };
                  zelle.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFD9E1F2' } };
              }
      
              // Daten für Zusammenfassung sammeln und eintragen
              let aktuelleZeile = 4; // Start ab Zeile 4
              
              for (let i = 0; i < Datenpunkte.length; i++) {
                  const dp = Datenpunkte[i];
                  
                  // Werte sammeln und bereinigen
                  const werte = [];
                  for (const tag of sortierteTage) {
                      const eindeutigerSchluessel = `${dp.id}__${dp.agg}__${dp.spaltenName}`;
                      let val = alleAggregiertenDaten[tag]?.[eindeutigerSchluessel];
                      if (val !== undefined && val !== null && !isNaN(val)) {
                          // ✅ ACHTUNG: Multiplikator wurde bereits in Hauptfunktion angewendet!
                          // Hier nur noch Nachkommastellen anwenden
                          const nachkommastellen = dp.nachkommastellen !== undefined ? dp.nachkommastellen : 2;
                          val = parseFloat(val.toFixed(nachkommastellen));
                          werte.push(val);
                      }
                  }
      
                  if (werte.length === 0) {
                      log(`🔍   Keine gültigen Werte für ${dp.spaltenName}`, 'warn');
                      continue;
                  }
      
                  // Berechnungen
                  const min = Math.min(...werte);
                  const max = Math.max(...werte);
                  const avg = werte.reduce((sum, val) => sum + val, 0) / werte.length;
                  const summe = werte.reduce((sum, val) => sum + val, 0);
      
                  // Werte direkt in Zellen schreiben
                  zusammenfassungBlatt.getCell(`A${aktuelleZeile}`).value = dp.spaltenName;
                  zusammenfassungBlatt.getCell(`B${aktuelleZeile}`).value = min;
                  zusammenfassungBlatt.getCell(`C${aktuelleZeile}`).value = max;
                  zusammenfassungBlatt.getCell(`D${aktuelleZeile}`).value = avg;
                  zusammenfassungBlatt.getCell(`E${aktuelleZeile}`).value = summe;
      
                 // Zahlen formatieren
                  for (let col of ['B', 'C', 'D', 'E']) {
                      const zelle = zusammenfassungBlatt.getCell(`${col}${aktuelleZeile}`);
                      const nachkommastellen = dp.nachkommastellen !== undefined ? dp.nachkommastellen : 2;
                      const einheit = leseEinheitAusDatenpunkt(dp.id);
                      zelle.numFmt = nachkommastellen === 0 
                          ? `0 " ${einheit}"` 
                          : `0.${'0'.repeat(nachkommastellen)} " ${einheit}"`;
                  }
      
                  aktuelleZeile++;
              }
      
              // Falls keine Daten gefunden wurden
              if (aktuelleZeile === 4) {
                  zusammenfassungBlatt.getCell('A4').value = 'Keine Daten verfügbar';
                  zusammenfassungBlatt.getCell('A4').font = { italic: true, color: { argb: 'FF808080' } };
              }
      
              // Spaltenbreiten für Zusammenfassung
              for (let col of ['A', 'B', 'C', 'D', 'E']) {
                  zusammenfassungBlatt.getColumn(col).width = 20;
              }
      
              // ✅ IN BINÄREN BUFFER UMWANDELN
              const uint8Array = await workbook.xlsx.writeBuffer();
              const buffer = Buffer.from(uint8Array);
              const base64 = buffer.toString('base64');
              
              log('✅ Excel-Datei erfolgreich erstellt', 'info');
              return base64;
      
          } catch (fehler) {
              log(`❌ Fehler beim Erstellen der Excel-Datei: ${fehler.message}`, 'error');
              log(`❌ Stack: ${fehler.stack}`, 'error');
              throw fehler;
          }
      }
      
      // Funktion: E-Mail senden
      function sendeEmailMitAnhang(excelBase64, dateiname, absenderAdresse, empfaengerAdresse, betreff) {
          const text = `Hallo,\n\nIm Anhang findest du den monatlichen Aggregationsbericht als formatierte Excel-Tabelle.\n\nAutomatisch generiert von ioBroker.\nDatum: ${new Date().toLocaleString()}`;
      
          sendTo(EmailAdapterInstanz, {
              from: absenderAdresse,
              to: empfaengerAdresse,
              subject: betreff,
              text: text,
              attachments: [{
                  filename: dateiname,
                  content: excelBase64,
                  encoding: 'base64',
                  contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
              }]
          }, (fehler, ergebnis) => {
              if (fehler && typeof fehler === 'object' && fehler.result && fehler.result.includes('250')) {
                  log(`✅ E-Mail erfolgreich gesendet (Server: ${fehler.result})`, 'info');
              } else if (fehler) {
                  log(`❌ E-Mail-Fehler: ${JSON.stringify(fehler)}`, 'error');
              } else {
                  log(`✅ E-Mail mit Anhang "${dateiname}" gesendet.`, 'info');
              }
          });
      }
      
      /**
       * ================================================================
       * SCHEDULE + MANUELLEN START
       * ================================================================
       */
      
      // Zeitplanung: 1. Tag jedes Monats um 00:05 Uhr
      schedule('5 0 1 * *', HauptprogrammExcelExport);
      
      // Manueller Start: Nur wenn Skript neu geladen wird und Adapter schon läuft
      if (existsState('system.adapter.javascript.0.uptime') && 
          getState('system.adapter.javascript.0.uptime').val > 60) {
          log('🔁 Manueller Start des Skripts erkannt', 'info');
          setTimeout(HauptprogrammExcelExport, 2000); // Kleine Verzögerung für sicheres Starten
      }
      
      log('✅ Skript geladen - bereit für manuellen oder zeitgesteuerten Start', 'info');
      
      bahnuhrB Online
      bahnuhrB Online
      bahnuhr
      Forum Testing Most Active
      schrieb am zuletzt editiert von
      #2

      @bertderkleine sagte in Regelmässiger Historienexport als Excel-Tabelle:

      try { const ExcelJS = require('exceljs'); log('✅ ExcelJS erfolgreich geladen!', 'info'); } catch (e) { log(❌ Fehler beim Laden von ExcelJS: ${e.message}, 'error'); log('💡 Lösung: Führe aus: cd /opt/iobroker && npm install exceljs', 'error'); return; }

      Frage:
      Warum npm ?
      Müsste es nicht reichen "exceljs" bei javascript in der Instanz einzutragen.

      Ansonsten:
      Sehr gute Arbeit.
      Hat bestimmt sehr viel Zeit gekostet.
      Habe auch schon viel mit "exceljs" gemacht.


      Wenn ich helfen konnte, dann Daumen hoch (Pfeil nach oben)!
      Danke.
      gute Forenbeiträge: https://forum.iobroker.net/topic/51555/hinweise-f%C3%BCr-gute-forenbeitr%C3%A4ge
      ScreenToGif :https://www.screentogif.com/downloads.html

      BertDerKleineB 1 Antwort Letzte Antwort
      0
      • bahnuhrB bahnuhr

        @bertderkleine sagte in Regelmässiger Historienexport als Excel-Tabelle:

        try { const ExcelJS = require('exceljs'); log('✅ ExcelJS erfolgreich geladen!', 'info'); } catch (e) { log(❌ Fehler beim Laden von ExcelJS: ${e.message}, 'error'); log('💡 Lösung: Führe aus: cd /opt/iobroker && npm install exceljs', 'error'); return; }

        Frage:
        Warum npm ?
        Müsste es nicht reichen "exceljs" bei javascript in der Instanz einzutragen.

        Ansonsten:
        Sehr gute Arbeit.
        Hat bestimmt sehr viel Zeit gekostet.
        Habe auch schon viel mit "exceljs" gemacht.

        BertDerKleineB Offline
        BertDerKleineB Offline
        BertDerKleine
        schrieb am zuletzt editiert von
        #3

        @bahnuhr sagte in Regelmässiger Historienexport als Excel-Tabelle:

        Frage:
        Warum npm ?
        Müsste es nicht reichen "exceljs" bei javascript in der Instanz einzutragen.

        Ehrlicherweise muss ich sagen, ich habs einfach gemacht ohne Nachzudenken. Ich hatte mal ein Skript nachgebaut, wo ich was in die Instanz eintragen sollte und bei dem schien das doch völlig egal zu sein. Später war es bei einem anderen Fall so, dass esnur per Installation auf Kommandozeile klappt, nicht in der Instanz des Adapters.
        Ende vom Lied ist, dass ich es gar nicht mehr versucht habe.
        Vermutlich werde ich das nochmal umstellen später.

        1 Antwort Letzte Antwort
        0
        • K Offline
          K Offline
          klassisch
          Most Active
          schrieb am zuletzt editiert von
          #4

          Respekt! Klasse, das hätte ich schon vor Jahren brauchen können.
          Meiner Meinung nach eine wichtige Sache, die auch einen Adapter verdienen würde.
          Mittlerweile habe ich es für mich mit python auf Basis der json files im Dateisystem gelöst. Nicht so elegant, nicht so automatisert, nicht so integriert; kommen aber auch xlsx und csv raus.

          BertDerKleineB 1 Antwort Letzte Antwort
          0
          • K klassisch

            Respekt! Klasse, das hätte ich schon vor Jahren brauchen können.
            Meiner Meinung nach eine wichtige Sache, die auch einen Adapter verdienen würde.
            Mittlerweile habe ich es für mich mit python auf Basis der json files im Dateisystem gelöst. Nicht so elegant, nicht so automatisert, nicht so integriert; kommen aber auch xlsx und csv raus.

            BertDerKleineB Offline
            BertDerKleineB Offline
            BertDerKleine
            schrieb am zuletzt editiert von
            #5

            @klassisch
            Danke. Ich habe jetzt das Skript oben nochmal aktualisiert.
            Neuerungen:

            • Hinweise bzgl. des npm Moduls korrigiert. Jetzt wird empfohlen, exceljs lokal über den Eintrag in der Instanz zu hinterlegen, nicht mehr global per Kommandozeile
            • Nachkommastellen pro Spalte konfigurierbar
            • optionaler Multiplikator z.B. um von m3 auf Liter zu kommen
            • optionales Auffüllen von Leerfeldern mit Nullen
            • die Einheitennahmen aus den Datenpunkten werden jetzt in die Zellformatierung übernommen, so dass es netter zu lesen ist
            • Probleme mit vielen Daten für den Ganzjahresexport sind behoben
            • über den gesonderten DP "0_userdata.0.VollJahresDaten" kann man jetzt 1,2 oder 3 ganze Jahre gesamthaft auswerfen lassen. Solange der DP auf Null steht, läuft das normale monatliche Modell, sonst einfach 1,2 oder 3 eintragen.

            Ich denke, jetzt ist es schön rund. :-)

            K 1 Antwort Letzte Antwort
            1
            • BertDerKleineB BertDerKleine

              @klassisch
              Danke. Ich habe jetzt das Skript oben nochmal aktualisiert.
              Neuerungen:

              • Hinweise bzgl. des npm Moduls korrigiert. Jetzt wird empfohlen, exceljs lokal über den Eintrag in der Instanz zu hinterlegen, nicht mehr global per Kommandozeile
              • Nachkommastellen pro Spalte konfigurierbar
              • optionaler Multiplikator z.B. um von m3 auf Liter zu kommen
              • optionales Auffüllen von Leerfeldern mit Nullen
              • die Einheitennahmen aus den Datenpunkten werden jetzt in die Zellformatierung übernommen, so dass es netter zu lesen ist
              • Probleme mit vielen Daten für den Ganzjahresexport sind behoben
              • über den gesonderten DP "0_userdata.0.VollJahresDaten" kann man jetzt 1,2 oder 3 ganze Jahre gesamthaft auswerfen lassen. Solange der DP auf Null steht, läuft das normale monatliche Modell, sonst einfach 1,2 oder 3 eintragen.

              Ich denke, jetzt ist es schön rund. :-)

              K Offline
              K Offline
              klassisch
              Most Active
              schrieb am zuletzt editiert von
              #6

              @bertderkleine sagte in Regelmässiger Historienexport als Excel-Tabelle:

              Ich habe jetzt das Skript oben nochmal aktualisiert.
              Neuerungen:

              Sieht super aus!

              • Nachkommastellen pro Spalte konfigurierbar

              Warum? Bei meinen Python Scripts habe ich das einfach so in XLS geschrieben wie es ist und in xls dann die Nachbearbeitung gemacht.

              • optionaler Multiplikator z.B. um von m3 auf Liter zu kommen

              Auch das ist in xls bequemer, denke ich.

              • optionales Auffüllen von Leerfeldern mit Nullen

              Das habe ich nicht verstanden. Welche Leerfelder?

              • Probleme mit vielen Daten für den Ganzjahresexport sind behoben

              Gibt es da Beschränkungen mit der Datenmenge oder Rechenleistung?

              Ich denke, jetzt ist es schön rund. :-)

              Saubere Arbeit, Respekt!

              BertDerKleineB 1 Antwort Letzte Antwort
              0
              • K klassisch

                @bertderkleine sagte in Regelmässiger Historienexport als Excel-Tabelle:

                Ich habe jetzt das Skript oben nochmal aktualisiert.
                Neuerungen:

                Sieht super aus!

                • Nachkommastellen pro Spalte konfigurierbar

                Warum? Bei meinen Python Scripts habe ich das einfach so in XLS geschrieben wie es ist und in xls dann die Nachbearbeitung gemacht.

                • optionaler Multiplikator z.B. um von m3 auf Liter zu kommen

                Auch das ist in xls bequemer, denke ich.

                • optionales Auffüllen von Leerfeldern mit Nullen

                Das habe ich nicht verstanden. Welche Leerfelder?

                • Probleme mit vielen Daten für den Ganzjahresexport sind behoben

                Gibt es da Beschränkungen mit der Datenmenge oder Rechenleistung?

                Ich denke, jetzt ist es schön rund. :-)

                Saubere Arbeit, Respekt!

                BertDerKleineB Offline
                BertDerKleineB Offline
                BertDerKleine
                schrieb am zuletzt editiert von
                #7

                @klassisch sagte in Regelmässiger Historienexport als Excel-Tabelle:

                • Nachkommastellen pro Spalte konfigurierbar

                Warum? Bei meinen Python Scripts habe ich das einfach so in XLS geschrieben wie es ist und in xls dann die Nachbearbeitung gemacht.

                Klar geht das, aber jetzt wollte ich es direkt so haben, wie es am Ende aussehen soll. Wenn man mal dran ist ...

                • optionaler Multiplikator z.B. um von m3 auf Liter zu kommen

                Auch das ist in xls bequemer, denke ich.

                Bequemer absolut, aber wie gesagt, ich wollte es jetzt direkt rund haben,

                • optionales Auffüllen von Leerfeldern mit Nullen

                Das habe ich nicht verstanden. Welche Leerfelder?

                Bei manchen Datenpunkten, wie "Regen" gibt es halt an manchen Tagen gar keine Daten. Da kann es besser sein, eine Null statt einer Lücke zu haben. Ist aber eh optional.

                • Probleme mit vielen Daten für den Ganzjahresexport sind behoben

                Gibt es da Beschränkungen mit der Datenmenge oder Rechenleistung?

                Ich habe alte Datenreihen, die amfangs noch viel zu viele Datenänderungen aufgezeichnet hatten, was pro Tag sehr viele Einträge erzeugt. Und das Skript saugt sich ja für das vorgegebene Zeitfenster alles rein. Bei einem Jahr war der Puffer zu schnell voll und spätere Tage blieben leer. Jetzt ist der Puffer größer und es wird in Salamitaktik gearbeitet.

                1 Antwort Letzte Antwort
                0
                Antworten
                • In einem neuen Thema antworten
                Anmelden zum Antworten
                • Älteste zuerst
                • Neuste zuerst
                • Meiste Stimmen


                Support us

                ioBroker
                Community Adapters
                Donate

                770

                Online

                32.6k

                Benutzer

                82.2k

                Themen

                1.3m

                Beiträge
                Community
                Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen | Einwilligungseinstellungen
                ioBroker Community 2014-2025
                logo
                • Anmelden

                • Du hast noch kein Konto? Registrieren

                • Anmelden oder registrieren, um zu suchen
                • Erster Beitrag
                  Letzter Beitrag
                0
                • Home
                • Aktuell
                • Tags
                • Ungelesen 0
                • Kategorien
                • Unreplied
                • Beliebt
                • GitHub
                • Docu
                • Hilfe