Navigation

    Logo
    • Register
    • Login
    • Search
    • Recent
    • Tags
    • Unread
    • Categories
    • Unreplied
    • Popular
    • GitHub
    • Docu
    • Hilfe
    1. Home
    2. Deutsch
    3. Praktische Anwendungen (Showcase)
    4. Regelmässiger Historienexport als Excel-Tabelle

    NEWS

    • Monatsrückblick – September 2025

    • Neues Video "KI im Smart Home" - ioBroker plus n8n

    • Neues Video über Aliase, virtuelle Geräte und Kategorien

    Regelmässiger Historienexport als Excel-Tabelle

    This topic has been deleted. Only users with topic management privileges can see it.
    • BertDerKleine
      BertDerKleine last edited by 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');
      
      bahnuhr 1 Reply Last reply Reply Quote 1
      • bahnuhr
        bahnuhr Forum Testing Most Active @BertDerKleine last edited by

        @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.

        BertDerKleine 1 Reply Last reply Reply Quote 0
        • BertDerKleine
          BertDerKleine @bahnuhr last edited by

          @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 Reply Last reply Reply Quote 0
          • K
            klassisch Most Active last edited by

            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.

            BertDerKleine 1 Reply Last reply Reply Quote 0
            • BertDerKleine
              BertDerKleine @klassisch last edited by

              @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 Reply Last reply Reply Quote 1
              • K
                klassisch Most Active @BertDerKleine last edited by

                @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!

                BertDerKleine 1 Reply Last reply Reply Quote 0
                • BertDerKleine
                  BertDerKleine @klassisch last edited by

                  @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 Reply Last reply Reply Quote 0
                  • First post
                    Last post

                  Support us

                  ioBroker
                  Community Adapters
                  Donate

                  935
                  Online

                  32.1k
                  Users

                  80.8k
                  Topics

                  1.3m
                  Posts

                  3
                  7
                  177
                  Loading More Posts
                  • Oldest to Newest
                  • Newest to Oldest
                  • Most Votes
                  Reply
                  • Reply as topic
                  Log in to reply
                  Community
                  Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen
                  The ioBroker Community 2014-2023
                  logo