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. E-Ink OpenEpaperLink - JSON-Steuerung per Javascript

NEWS

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

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

  • Weihnachtsangebot 2025! 🎄
    BluefoxB
    Bluefox
    25
    1
    2.1k

E-Ink OpenEpaperLink - JSON-Steuerung per Javascript

Geplant Angeheftet Gesperrt Verschoben Praktische Anwendungen (Showcase)
11 Beiträge 3 Kommentatoren 575 Aufrufe 5 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

    Nachdem die meisten OEPL Steuerungen anscheinend über den Weg der Screenshots mit Puppeteer gehen, hier mal mein Ansatz über JSONs.

    Mein Puppeteer Skript habe ich hierdurch ersetzt und bin sehr zufrieden damit.

    Mittels der Konfigurationsblöcke kann ich sehr simpel umsetzen, was ich mag und jeder Tag hat seine eigene Zeitsteuerung. Ich habe mal ein paar Varianten reinkopiert, die ich durchgespielt habe.

    Vielleicht hilfts ja jemandem. :relaxed:

    /**
     * ############################################################################
     *  OpenEPaperLink – vollautomatische ePaper-Steuerung mit Zeitschaltung
     *  und Signalbildern für Fenster- / Tür-Status, realisiert über JSON-Templates
     * ############################################################################
     *
     *  Im August 2025 von BertDerKleine
     *
     *  Zweck
     *  =====
     *  Dieses Skript versorgt alle registrierten OpenEPaperLink-Tags (WLAN-ePaper-
     *  Displays) zyklisch mit aktuellen Daten aus ioBroker.  Durch konfigurierbare
     *  Zeitfenster und individuelle Update-Intervalle wird die Batterielaufzeit
     *  maximal geschont.  Neu hinzugekommen ist die Möglichkeit, einfache
     *  „Signalbilder“ einzublenden: anhand eines Boolean-Datenpunktes wird
     *  automatisch zwischen zwei Icon-Dateien (z. B. offen / geschlossen)
     *  gewechselt.
     *  Ziel war es, eine Lösung via JSON-Templates hinzubekommen, ohne den Umweg
     *  über Screenshots mit Puppeteer. U.a. umgeht dies das Problem, das Puppeteer
     *  immer nur eine Sache gleichzeitig tun kann, was zu Konflikten führen kann,
     *  wenn man viele Tags aktualisieren muss. 
     *
     *  Ablauf im Überblick
     *  -------------------
     *  1.  Konstanten am Kopf definieren Display-Größe und Access-Point.
     *  2.  In `tagConfig` pro Tag eintragen:
     *        – MAC-Adresse
     *        – Anzeige-Titel
     *        – Update-Intervall in Minuten
     *        – ZeitFenster (z. B. „07:00-23:00“)
     *        – body: Array aller anzuzeigenden Elemente
     *  3.  `starteTagUpdates()` startet für jeden Tag ein eigenes Intervall
     *      und führt sofort ein erstes Update aus.
     *  4.  `aktualisiereTag()` prüft das ZeitFenster, baut das JSON zusammen
     *      und sendet es per HTTP-POST an den Access-Point.
     *
     *  Element-Typen
     *  -------------
     *  •  Text:             { dp: 'pfad/zum/Datenpunkt', font: 'Nolo90', … }
     *  •  Fortschrittsbalken: { type: 'progressBar', dp: '…', fillColor: 2, … }
     *  •  Bild:             { image: { filename: '/Bild.jpg', x: 10, y: 20 } }
     *  •  Signalbild:       { type: 'signalImage',
     *                          dp: 'alias.0.Fenster_Kueche',
     *                          images: { true: '/offen.jpg', false: '/zu.jpg' },
     *                          x, y, imageWidth, imageHeight, label, … }
     *
     *  Voraussetzungen
     *  ---------------
     *  • ioBroker mit JavaScript-Adapter
     *  • Node-Modul „luxon“ (nur für zuverlässige Sommer-/Winterzeit)
     *      npm install luxon
     *  • Bild- und Icon-Dateien liegen im Root des Access-Points als 8bit sRGB JPGs
     *    rein schwarz, weiss und rot.
     *  ############################################################################
     */
    
    const http          = require('http');
    const querystring   = require('querystring');
    const { DateTime }  = require('luxon');
    
    /* --------------------------------------------------------------------------
     *  Globale Konstanten
     * -------------------------------------------------------------------------- */
    const AP_IP               = '192.168.178.233';   // IP des OpenEPaperLink-AP
    const BILDSCHIRM_BREITE   = 296;
    const BILDSCHIRM_HÖHE     = 152;
    const TITEL_BALKEN_HÖHE   = 20;
    const TITEL_SCHRIFT       = 'calibrib16';
    
    /* --------------------------------------------------------------------------
     *  Tag-Konfiguration
     *  Jeder Eintrag definiert exakt ein ePaper-Display.
     * -------------------------------------------------------------------------- */
    // Konfiguration der Tags mit MAC-Adresse, Titel, Update-Intervall, Zeitfenster und Inhalt
    const tagConfig = [
      {
        mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für DG Außentemperatur 
        title: 'Aussentemperatur DG', // Titel, der auf dem Tag angezeigt wird
        updateIntervalMinutes: 30, // Update alle 30 Minuten
        ZeitFenster: '07:00-23:00', // Updates zwischen 07:00 und 23:00 (Europe/Berlin)
        body: [
          // Temperatur mit großer Schrift, linksbündig
          { dp: 'alias.0.Aussentemperatur_kombiniert', font: 'Nolo90', color: 1, suffix: '°C', x: 10, y: 40, decimalPlaces: 1 },
          // Regenmenge pro Tag, unten links
          { dp: 'alias.0.Regen_pro_Tag', font: 'bahnschrift30', color: 2, suffix: 'mm', x: 5, y: 127, decimalPlaces: 1 },
          // Luftfeuchtigkeit, rechtsbündig unten
          { dp: 'alias.0.Luftfeuchtigkeit_Gartenhaus', font: 'bahnschrift30', color: 1, prefix: 'LF ', suffix: '%', x: 296, y: 115, align: 2, decimalPlaces: 0 }
        ]
      },
      
      {
        mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für Waage 
        title: 'Waage', 
        updateIntervalMinutes: 10, 
        ZeitFenster: '06:30-12:00', 
        body: [
          // Körpergewicht mit großer Schrift, linksbündig
          { dp: 'alias.0.Körpergewicht', font: 'Nolo70', color: 1, suffix: ' kg', x: 10, y: 40, decimalPlaces: 2 },
          // Widerstand, unten links
          { dp: 'alias.0.Körperwiderstand', font: 'bahnschrift30', color: 2, suffix: 'Ohm', x: 5, y: 127, decimalPlaces: 0 },
          // BMI, rechtsbündig unten
          { dp: 'alias.0.BMI', font: 'bahnschrift30', color: 1, prefix: 'BMI: ', suffix: '', x: 296, y: 115, align: 2, decimalPlaces: 1 }
        ]
      },
      {
        mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für die Kalendervorschau
        title: 'Kalender',
        updateIntervalMinutes: 120,
        ZeitFenster: '07:00-23:00',
        body: [
          // Kalendertext, oben links
          { dp: 'alias.0.Kalendervorschau', font: 'bahnschrift20', color: 1, x: 0, y: 25 }
        ]
      },
      {
        mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für Akku-Status
        title: 'Akku-Status',
        updateIntervalMinutes: 30,
        ZeitFenster: '07:00-23:00',
        body: [
          // Akku-Prozentsatz, rechtsbündig
          { dp: 'alias.0.Akku_Ladezustand', font: 'Nolo90', color: 1, suffix: '%', x: BILDSCHIRM_BREITE, y: 40, align: 2, decimalPlaces: 0 },
          // Fortschrittsbalken für Akku, unten
          { dp: 'alias.0.Akku_Ladezustand', type: 'progressBar', fillColor: 2, height: 32, y: BILDSCHIRM_HÖHE - 32 }
        ]
      },
      {
        mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für Stromwerte
        title: 'Stromwerte',
        updateIntervalMinutes: 60,
        ZeitFenster: '07:00-23:00',
        body: [
          { image: { filename: '/Sonne.jpg', x: 10, y: 25 } },
          // PV-Erzeugung, rechtsbündig
          { dp: 'sourceanalytix.0.alias__0__PV_Erzeugungszählerstand.currentYear.consumed.01_currentDay', font: 'bahnschrift30', color: 1, prefix: 'PV: ', suffix: ' kWh', x: BILDSCHIRM_BREITE, y: 28, align: 2, decimalPlaces: 1 },
          // Netzbezug, rechtsbündig
          { dp: 'sourceanalytix.0.alias__0__Netzbezugszählerstand.currentYear.consumed.01_currentDay', font: 'bahnschrift30', color: 2, prefix: 'Bezug: ', suffix: ' kWh', x: BILDSCHIRM_BREITE, y: 58, align: 2, decimalPlaces: 1 },
          // Netzeinspeisung, rechtsbündig
          { dp: 'sourceanalytix.0.alias__0__Netzeinspeisezählerstand.currentYear.delivered.01_currentDay', font: 'bahnschrift30', color: 1, prefix: 'Einsp.: ', suffix: ' kWh', x: BILDSCHIRM_BREITE, y: 88, align: 2, decimalPlaces: 1 },
          // Verbrauch, rechtsbündig
          { dp: 'sourceanalytix.0.alias__0__Verbrauchszaehlerstand.currentYear.consumed.01_currentDay', font: 'bahnschrift30', color: 1, prefix: 'Verbr.: ', suffix: ' kWh', x: BILDSCHIRM_BREITE, y: 118, align: 2, decimalPlaces: 1 }
        ]
      },
      {
        mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für Wasserverbrauch
        title: 'Wasserverbrauch',
        updateIntervalMinutes: 45,
        ZeitFenster: '07:00-23:00',
        body: [
          { image: { filename: '/Wasser.jpg', x: 10, y: 25, } },
          { dp: 'alias.0.Tages-Wasserverbrauch', font: 'Nolo90', color: 1, prefix: '', suffix: ' l', x: BILDSCHIRM_BREITE, y: 50, align: 2, decimalPlaces: 0 },
        ]
      },
      {
        mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für offene Fenster/Türen
        title: 'Fensterstatus',
        updateIntervalMinutes: 15,
        ZeitFenster: '07:00-23:00',
        body: [
          // Reihe 1 von links nach rechts; die verwendeten Signalbilder sind 48x48 Pixel gross
          { type: 'signalImage', dp: 'alias.0.Fenster_Küche', images: { true: '/window_open48.jpg', false: '/window_closed48.jpg' }, x: 38, y: 22, imageWidth: 48, imageHeight: 48, label: 'Küche', labelColor: 1, labelSpacing: 2 },
          { type: 'signalImage', dp: 'alias.0.Fenster_Wohnzimmer', images: { true: '/window_open48.jpg', false: '/window_closed48.jpg' }, x: 124, y: 22, imageWidth: 48, imageHeight: 48, label: 'Wohnzimmer', labelColor: 1, labelSpacing: 2 },
          { type: 'signalImage', dp: 'alias.0.Fenster_Keller', images: { true: '/window_open48.jpg', false: '/window_closed48.jpg' }, x: 210, y: 22, imageWidth: 48, imageHeight: 48, label: 'Keller', labelColor: 1, labelSpacing: 2 },
    
          // Reihe 2 von links nach rechts
          { type: 'signalImage', dp: 'alias.0.Fenster_Gäste-WC', images: { true: '/window_open48.jpg', false: '/window_closed48.jpg' }, x: 38, y: 82, imageWidth: 48, imageHeight: 48, label: 'Gäste-WC', labelColor: 1, labelSpacing: 2 },
          { type: 'signalImage', dp: 'alias.0.Tür_Terrasse', images: { true: '/door-open48.jpg', false: '/door-closed48.jpg' }, x: 124, y: 82, imageWidth: 48, imageHeight: 48, label: 'Terrassentür', labelColor: 1, labelSpacing: 2 },
          { type: 'signalImage', dp: 'alias.0.Fenster_Garage', images: { true: '/window_open48.jpg', false: '/window_closed48.jpg' }, x: 210, y: 82, imageWidth: 48, imageHeight: 48, label: 'Garage', labelColor: 1, labelSpacing: 2 }
        ]
      }
    ];
    
    /* ############################################################################
     *  Hilfsfunktionen
     * ############################################################################ */
    
    /**
     * Prüft, ob die aktuelle Zeit im konfigurierten Zeitfenster liegt.
     * @param {string} ZeitFenster - Zeitfenster im Format "HH:mm-HH:mm" (z. B. "07:00-23:00").
     * @returns {boolean} - True, wenn die aktuelle Zeit im Zeitfenster liegt, sonst false.
     */
    function istImZeitFenster(ZeitFenster) {
      const jetzt = DateTime.now().setZone('Europe/Berlin');
      const [von, bis] = ZeitFenster.split('-').map(t => {
        const [h, m] = t.split(':').map(Number);
        return jetzt.set({ hour: h, minute: m, second: 0, millisecond: 0 });
      });
      return bis < von ? (jetzt >= von || jetzt <= bis) : (jetzt >= von && jetzt <= bis);
    }
    
    /**
     * Konvertiert eine Zeitangabe (HH:mm) in Minuten seit Mitternacht für Vergleiche.
     * @param {string} ZeitStr - Zeit im Format "HH:mm".
     * @returns {number} - Minuten seit Mitternacht.
     */
    function zeitZuMinuten(ZeitStr) {
      const [h, m] = ZeitStr.split(':').map(Number);
      return h * 60 + m;
    }
    
    /**
     * Erstellt die JSON-Elemente für die Titelleiste (Hintergrund + Text).
     * @param {string} titelText - Text, der in der Titelleiste erscheinen soll.
     * @returns {Array} - Array mit JSON-Elementen (box + text).
     */
    function titelElementeErzeugen(titelText) {
      const mitteX = Math.floor(BILDSCHIRM_BREITE / 2);
      return [
        { box: [0, 0, BILDSCHIRM_BREITE, TITEL_BALKEN_HÖHE, 1, 1, 1] },
        { text: [mitteX, 3, titelText, `fonts/${TITEL_SCHRIFT}`, 0, 1] }
      ];
    }
    
    /**
     * Verarbeitet ein einzelnes Anzeige-Element und liefert das dafür nötige JSON.
     * Erkannt werden:
     *  – Signalbilder (signalImage)
     *  – normale Bilder (image)
     *  – Texte (dp oder fester text)
     *  – Fortschrittsbalken (progressBar)
     *
     * @param {Object} item - Konfiguration des Elements
     * @returns {Object|Array} - JSON-Objekt oder Array von Objekten für die Darstellung
     */
    async function elementAuflösen(item) {
      /* ---------- Signalbild mit optionalem Label ---------- */
      if (item.type === 'signalImage') {
        const status = await getStateAsync(item.dp);
        const bild   = (status && status.val) ? item.images.true : item.images.false;
        const result = [{ image: [bild, item.x || 0, item.y || 0] }];
        if (item.label) {
          const labelX = (item.x || 0) + Math.floor((item.imageWidth || 48) / 2);
          const labelY = (item.y || 0) + (item.imageHeight || 48) + (item.labelSpacing || 2);
          result.push({ text: [labelX, labelY, item.label, 'tahoma11', item.labelColor || 1, 1] });
        }
        return result;
      }
    
      /* ---------- Normales Bild ---------- */
      if (item.image) {
        const { filename, x = 0, y = 0 } = item.image;
        return { image: [filename, x, y] };
      }
    
      /* ---------- Text & Balken ---------- */
      let wert = '';
      if (item.dp) {
        const state = await getStateAsync(item.dp);
        wert = (state && state.val != null) ? state.val : '';
        if (typeof wert === 'number' || !isNaN(parseFloat(wert))) {
          const stellen = typeof item.decimalPlaces === 'number' ? item.decimalPlaces : 2;
          wert = parseFloat(wert).toLocaleString('de-DE', { minimumFractionDigits: stellen, maximumFractionDigits: stellen });
        } else {
          wert = String(wert);
        }
      } else if (typeof item.text === 'string') {
        wert = item.text;
      }
    
      if (item.type === 'progressBar') {
        const prozent = parseFloat(wert) || 0;
        const breite  = Math.round(Math.max(0, Math.min(100, prozent)) / 100 * BILDSCHIRM_BREITE);
        const yPos    = typeof item.y === 'number' ? item.y : BILDSCHIRM_HÖHE - (item.height || 32);
        return { box: [0, yPos, breite, item.height || 32, item.fillColor || 2, item.fillColor || 2, item.fillColor || 2] };
      }
    
      if (item.prefix) wert = item.prefix + wert;
      if (item.suffix) wert = wert + item.suffix;
    
      const x      = typeof item.x === 'number' ? item.x : 0;
      const y      = typeof item.y === 'number' ? item.y : 0;
      const schrift = item.font || 'bahnschrift20';
      const farbe   = typeof item.color === 'number' ? item.color : 1;
      const textArr = [x, y, wert, schrift, farbe];
      if (typeof item.align === 'number') textArr.push(item.align);
      return { text: textArr };
    }
    
    /* ############################################################################
     *  HTTP-Sender
     * ############################################################################ */
    
    /**
     * Sendet das fertige JSON-Array an den OpenEPaperLink-Access-Point.
     * @param {string} mac        - MAC-Adresse des Tags
     * @param {Array}  jsonArray  - Vollständiges JSON-Array für das Display
     * @param {string} titel      - Nur für Logging/Console-Ausgabe
     * @returns {Promise}         - Resolves bei Erfolg, rejects bei Netzwerkfehler
     */
    function sendeAnTag(mac, jsonArray, titel) {
      const postData = querystring.stringify({ mac, json: JSON.stringify(jsonArray) });
      const options  = {
        hostname: AP_IP,
        port: 80,
        path: '/jsonupload',
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          'Content-Length': Buffer.byteLength(postData)
        }
      };
    
      return new Promise((resolve, reject) => {
        const req = http.request(options, res => {
          let data = '';
          res.on('data', c => data += c);
          res.on('end', () => {
            console.log(`✅ "${titel}" (${mac}) → AP-Antwort: "${data.trim() || 'leer'}"`);
            resolve();
          });
        });
        req.on('error', e => {
          console.error(`❌ "${titel}" (${mac}) → HTTP-Fehler: ${e.message}`);
          reject(e);
        });
        req.write(postData);
        req.end();
      });
    }
    
    /* ############################################################################
     *  Update-Logik
     * ############################################################################ */
    
    /**
     * Aktualisiert ein einzelnes Tag, sofern die aktuelle Zeit im konfigurierten
     * Zeitfenster liegt.  Bei Erfolg wird das fertige JSON an den AP gesendet.
     * @param {Object} tag - Ein Eintrag aus tagConfig
     * @returns {Promise}  - Resolves bei Erfolg oder wenn außerhalb des Zeitfensters
     */
    async function aktualisiereTag(tag) {
      if (!istImZeitFenster(tag.ZeitFenster)) {
        console.log(`⏸️  "${tag.title}" (${tag.mac}) außerhalb des Zeitfensters ${tag.ZeitFenster}`);
        return;
      }
    
      const json = [{ clear: [1] }, ...titelElementeErzeugen(tag.title)];
      for (const item of tag.body) {
        const ergebnis = await elementAuflösen(item);
        Array.isArray(ergebnis) ? json.push(...ergebnis) : json.push(ergebnis);
      }
      await sendeAnTag(tag.mac, json, tag.title);
    }
    
    /**
     * Startet für jeden Tag ein eigenes setInterval und führt sofort ein erstes
     * Update aus.  So kann sich jeder Tag unabhängig aktualisieren.
     */
    function starteTagUpdates() {
      for (const tag of tagConfig) {
        const intervallMs = tag.updateIntervalMinutes * 60 * 1000;
        setInterval(() => aktualisiereTag(tag).catch(e =>
          console.error(`Fehler bei ${tag.title}: ${e.message}`)), intervallMs);
        aktualisiereTag(tag).catch(e =>
          console.error(`Erstes Update fehlgeschlagen bei ${tag.title}: ${e.message}`));
      }
    }
    
    /* ############################################################################
     *  Start
     * ############################################################################ */
    starteTagUpdates();
    
    BertDerKleineB B 2 Antworten Letzte Antwort
    3
    • BertDerKleineB BertDerKleine

      Nachdem die meisten OEPL Steuerungen anscheinend über den Weg der Screenshots mit Puppeteer gehen, hier mal mein Ansatz über JSONs.

      Mein Puppeteer Skript habe ich hierdurch ersetzt und bin sehr zufrieden damit.

      Mittels der Konfigurationsblöcke kann ich sehr simpel umsetzen, was ich mag und jeder Tag hat seine eigene Zeitsteuerung. Ich habe mal ein paar Varianten reinkopiert, die ich durchgespielt habe.

      Vielleicht hilfts ja jemandem. :relaxed:

      /**
       * ############################################################################
       *  OpenEPaperLink – vollautomatische ePaper-Steuerung mit Zeitschaltung
       *  und Signalbildern für Fenster- / Tür-Status, realisiert über JSON-Templates
       * ############################################################################
       *
       *  Im August 2025 von BertDerKleine
       *
       *  Zweck
       *  =====
       *  Dieses Skript versorgt alle registrierten OpenEPaperLink-Tags (WLAN-ePaper-
       *  Displays) zyklisch mit aktuellen Daten aus ioBroker.  Durch konfigurierbare
       *  Zeitfenster und individuelle Update-Intervalle wird die Batterielaufzeit
       *  maximal geschont.  Neu hinzugekommen ist die Möglichkeit, einfache
       *  „Signalbilder“ einzublenden: anhand eines Boolean-Datenpunktes wird
       *  automatisch zwischen zwei Icon-Dateien (z. B. offen / geschlossen)
       *  gewechselt.
       *  Ziel war es, eine Lösung via JSON-Templates hinzubekommen, ohne den Umweg
       *  über Screenshots mit Puppeteer. U.a. umgeht dies das Problem, das Puppeteer
       *  immer nur eine Sache gleichzeitig tun kann, was zu Konflikten führen kann,
       *  wenn man viele Tags aktualisieren muss. 
       *
       *  Ablauf im Überblick
       *  -------------------
       *  1.  Konstanten am Kopf definieren Display-Größe und Access-Point.
       *  2.  In `tagConfig` pro Tag eintragen:
       *        – MAC-Adresse
       *        – Anzeige-Titel
       *        – Update-Intervall in Minuten
       *        – ZeitFenster (z. B. „07:00-23:00“)
       *        – body: Array aller anzuzeigenden Elemente
       *  3.  `starteTagUpdates()` startet für jeden Tag ein eigenes Intervall
       *      und führt sofort ein erstes Update aus.
       *  4.  `aktualisiereTag()` prüft das ZeitFenster, baut das JSON zusammen
       *      und sendet es per HTTP-POST an den Access-Point.
       *
       *  Element-Typen
       *  -------------
       *  •  Text:             { dp: 'pfad/zum/Datenpunkt', font: 'Nolo90', … }
       *  •  Fortschrittsbalken: { type: 'progressBar', dp: '…', fillColor: 2, … }
       *  •  Bild:             { image: { filename: '/Bild.jpg', x: 10, y: 20 } }
       *  •  Signalbild:       { type: 'signalImage',
       *                          dp: 'alias.0.Fenster_Kueche',
       *                          images: { true: '/offen.jpg', false: '/zu.jpg' },
       *                          x, y, imageWidth, imageHeight, label, … }
       *
       *  Voraussetzungen
       *  ---------------
       *  • ioBroker mit JavaScript-Adapter
       *  • Node-Modul „luxon“ (nur für zuverlässige Sommer-/Winterzeit)
       *      npm install luxon
       *  • Bild- und Icon-Dateien liegen im Root des Access-Points als 8bit sRGB JPGs
       *    rein schwarz, weiss und rot.
       *  ############################################################################
       */
      
      const http          = require('http');
      const querystring   = require('querystring');
      const { DateTime }  = require('luxon');
      
      /* --------------------------------------------------------------------------
       *  Globale Konstanten
       * -------------------------------------------------------------------------- */
      const AP_IP               = '192.168.178.233';   // IP des OpenEPaperLink-AP
      const BILDSCHIRM_BREITE   = 296;
      const BILDSCHIRM_HÖHE     = 152;
      const TITEL_BALKEN_HÖHE   = 20;
      const TITEL_SCHRIFT       = 'calibrib16';
      
      /* --------------------------------------------------------------------------
       *  Tag-Konfiguration
       *  Jeder Eintrag definiert exakt ein ePaper-Display.
       * -------------------------------------------------------------------------- */
      // Konfiguration der Tags mit MAC-Adresse, Titel, Update-Intervall, Zeitfenster und Inhalt
      const tagConfig = [
        {
          mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für DG Außentemperatur 
          title: 'Aussentemperatur DG', // Titel, der auf dem Tag angezeigt wird
          updateIntervalMinutes: 30, // Update alle 30 Minuten
          ZeitFenster: '07:00-23:00', // Updates zwischen 07:00 und 23:00 (Europe/Berlin)
          body: [
            // Temperatur mit großer Schrift, linksbündig
            { dp: 'alias.0.Aussentemperatur_kombiniert', font: 'Nolo90', color: 1, suffix: '°C', x: 10, y: 40, decimalPlaces: 1 },
            // Regenmenge pro Tag, unten links
            { dp: 'alias.0.Regen_pro_Tag', font: 'bahnschrift30', color: 2, suffix: 'mm', x: 5, y: 127, decimalPlaces: 1 },
            // Luftfeuchtigkeit, rechtsbündig unten
            { dp: 'alias.0.Luftfeuchtigkeit_Gartenhaus', font: 'bahnschrift30', color: 1, prefix: 'LF ', suffix: '%', x: 296, y: 115, align: 2, decimalPlaces: 0 }
          ]
        },
        
        {
          mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für Waage 
          title: 'Waage', 
          updateIntervalMinutes: 10, 
          ZeitFenster: '06:30-12:00', 
          body: [
            // Körpergewicht mit großer Schrift, linksbündig
            { dp: 'alias.0.Körpergewicht', font: 'Nolo70', color: 1, suffix: ' kg', x: 10, y: 40, decimalPlaces: 2 },
            // Widerstand, unten links
            { dp: 'alias.0.Körperwiderstand', font: 'bahnschrift30', color: 2, suffix: 'Ohm', x: 5, y: 127, decimalPlaces: 0 },
            // BMI, rechtsbündig unten
            { dp: 'alias.0.BMI', font: 'bahnschrift30', color: 1, prefix: 'BMI: ', suffix: '', x: 296, y: 115, align: 2, decimalPlaces: 1 }
          ]
        },
        {
          mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für die Kalendervorschau
          title: 'Kalender',
          updateIntervalMinutes: 120,
          ZeitFenster: '07:00-23:00',
          body: [
            // Kalendertext, oben links
            { dp: 'alias.0.Kalendervorschau', font: 'bahnschrift20', color: 1, x: 0, y: 25 }
          ]
        },
        {
          mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für Akku-Status
          title: 'Akku-Status',
          updateIntervalMinutes: 30,
          ZeitFenster: '07:00-23:00',
          body: [
            // Akku-Prozentsatz, rechtsbündig
            { dp: 'alias.0.Akku_Ladezustand', font: 'Nolo90', color: 1, suffix: '%', x: BILDSCHIRM_BREITE, y: 40, align: 2, decimalPlaces: 0 },
            // Fortschrittsbalken für Akku, unten
            { dp: 'alias.0.Akku_Ladezustand', type: 'progressBar', fillColor: 2, height: 32, y: BILDSCHIRM_HÖHE - 32 }
          ]
        },
        {
          mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für Stromwerte
          title: 'Stromwerte',
          updateIntervalMinutes: 60,
          ZeitFenster: '07:00-23:00',
          body: [
            { image: { filename: '/Sonne.jpg', x: 10, y: 25 } },
            // PV-Erzeugung, rechtsbündig
            { dp: 'sourceanalytix.0.alias__0__PV_Erzeugungszählerstand.currentYear.consumed.01_currentDay', font: 'bahnschrift30', color: 1, prefix: 'PV: ', suffix: ' kWh', x: BILDSCHIRM_BREITE, y: 28, align: 2, decimalPlaces: 1 },
            // Netzbezug, rechtsbündig
            { dp: 'sourceanalytix.0.alias__0__Netzbezugszählerstand.currentYear.consumed.01_currentDay', font: 'bahnschrift30', color: 2, prefix: 'Bezug: ', suffix: ' kWh', x: BILDSCHIRM_BREITE, y: 58, align: 2, decimalPlaces: 1 },
            // Netzeinspeisung, rechtsbündig
            { dp: 'sourceanalytix.0.alias__0__Netzeinspeisezählerstand.currentYear.delivered.01_currentDay', font: 'bahnschrift30', color: 1, prefix: 'Einsp.: ', suffix: ' kWh', x: BILDSCHIRM_BREITE, y: 88, align: 2, decimalPlaces: 1 },
            // Verbrauch, rechtsbündig
            { dp: 'sourceanalytix.0.alias__0__Verbrauchszaehlerstand.currentYear.consumed.01_currentDay', font: 'bahnschrift30', color: 1, prefix: 'Verbr.: ', suffix: ' kWh', x: BILDSCHIRM_BREITE, y: 118, align: 2, decimalPlaces: 1 }
          ]
        },
        {
          mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für Wasserverbrauch
          title: 'Wasserverbrauch',
          updateIntervalMinutes: 45,
          ZeitFenster: '07:00-23:00',
          body: [
            { image: { filename: '/Wasser.jpg', x: 10, y: 25, } },
            { dp: 'alias.0.Tages-Wasserverbrauch', font: 'Nolo90', color: 1, prefix: '', suffix: ' l', x: BILDSCHIRM_BREITE, y: 50, align: 2, decimalPlaces: 0 },
          ]
        },
        {
          mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für offene Fenster/Türen
          title: 'Fensterstatus',
          updateIntervalMinutes: 15,
          ZeitFenster: '07:00-23:00',
          body: [
            // Reihe 1 von links nach rechts; die verwendeten Signalbilder sind 48x48 Pixel gross
            { type: 'signalImage', dp: 'alias.0.Fenster_Küche', images: { true: '/window_open48.jpg', false: '/window_closed48.jpg' }, x: 38, y: 22, imageWidth: 48, imageHeight: 48, label: 'Küche', labelColor: 1, labelSpacing: 2 },
            { type: 'signalImage', dp: 'alias.0.Fenster_Wohnzimmer', images: { true: '/window_open48.jpg', false: '/window_closed48.jpg' }, x: 124, y: 22, imageWidth: 48, imageHeight: 48, label: 'Wohnzimmer', labelColor: 1, labelSpacing: 2 },
            { type: 'signalImage', dp: 'alias.0.Fenster_Keller', images: { true: '/window_open48.jpg', false: '/window_closed48.jpg' }, x: 210, y: 22, imageWidth: 48, imageHeight: 48, label: 'Keller', labelColor: 1, labelSpacing: 2 },
      
            // Reihe 2 von links nach rechts
            { type: 'signalImage', dp: 'alias.0.Fenster_Gäste-WC', images: { true: '/window_open48.jpg', false: '/window_closed48.jpg' }, x: 38, y: 82, imageWidth: 48, imageHeight: 48, label: 'Gäste-WC', labelColor: 1, labelSpacing: 2 },
            { type: 'signalImage', dp: 'alias.0.Tür_Terrasse', images: { true: '/door-open48.jpg', false: '/door-closed48.jpg' }, x: 124, y: 82, imageWidth: 48, imageHeight: 48, label: 'Terrassentür', labelColor: 1, labelSpacing: 2 },
            { type: 'signalImage', dp: 'alias.0.Fenster_Garage', images: { true: '/window_open48.jpg', false: '/window_closed48.jpg' }, x: 210, y: 82, imageWidth: 48, imageHeight: 48, label: 'Garage', labelColor: 1, labelSpacing: 2 }
          ]
        }
      ];
      
      /* ############################################################################
       *  Hilfsfunktionen
       * ############################################################################ */
      
      /**
       * Prüft, ob die aktuelle Zeit im konfigurierten Zeitfenster liegt.
       * @param {string} ZeitFenster - Zeitfenster im Format "HH:mm-HH:mm" (z. B. "07:00-23:00").
       * @returns {boolean} - True, wenn die aktuelle Zeit im Zeitfenster liegt, sonst false.
       */
      function istImZeitFenster(ZeitFenster) {
        const jetzt = DateTime.now().setZone('Europe/Berlin');
        const [von, bis] = ZeitFenster.split('-').map(t => {
          const [h, m] = t.split(':').map(Number);
          return jetzt.set({ hour: h, minute: m, second: 0, millisecond: 0 });
        });
        return bis < von ? (jetzt >= von || jetzt <= bis) : (jetzt >= von && jetzt <= bis);
      }
      
      /**
       * Konvertiert eine Zeitangabe (HH:mm) in Minuten seit Mitternacht für Vergleiche.
       * @param {string} ZeitStr - Zeit im Format "HH:mm".
       * @returns {number} - Minuten seit Mitternacht.
       */
      function zeitZuMinuten(ZeitStr) {
        const [h, m] = ZeitStr.split(':').map(Number);
        return h * 60 + m;
      }
      
      /**
       * Erstellt die JSON-Elemente für die Titelleiste (Hintergrund + Text).
       * @param {string} titelText - Text, der in der Titelleiste erscheinen soll.
       * @returns {Array} - Array mit JSON-Elementen (box + text).
       */
      function titelElementeErzeugen(titelText) {
        const mitteX = Math.floor(BILDSCHIRM_BREITE / 2);
        return [
          { box: [0, 0, BILDSCHIRM_BREITE, TITEL_BALKEN_HÖHE, 1, 1, 1] },
          { text: [mitteX, 3, titelText, `fonts/${TITEL_SCHRIFT}`, 0, 1] }
        ];
      }
      
      /**
       * Verarbeitet ein einzelnes Anzeige-Element und liefert das dafür nötige JSON.
       * Erkannt werden:
       *  – Signalbilder (signalImage)
       *  – normale Bilder (image)
       *  – Texte (dp oder fester text)
       *  – Fortschrittsbalken (progressBar)
       *
       * @param {Object} item - Konfiguration des Elements
       * @returns {Object|Array} - JSON-Objekt oder Array von Objekten für die Darstellung
       */
      async function elementAuflösen(item) {
        /* ---------- Signalbild mit optionalem Label ---------- */
        if (item.type === 'signalImage') {
          const status = await getStateAsync(item.dp);
          const bild   = (status && status.val) ? item.images.true : item.images.false;
          const result = [{ image: [bild, item.x || 0, item.y || 0] }];
          if (item.label) {
            const labelX = (item.x || 0) + Math.floor((item.imageWidth || 48) / 2);
            const labelY = (item.y || 0) + (item.imageHeight || 48) + (item.labelSpacing || 2);
            result.push({ text: [labelX, labelY, item.label, 'tahoma11', item.labelColor || 1, 1] });
          }
          return result;
        }
      
        /* ---------- Normales Bild ---------- */
        if (item.image) {
          const { filename, x = 0, y = 0 } = item.image;
          return { image: [filename, x, y] };
        }
      
        /* ---------- Text & Balken ---------- */
        let wert = '';
        if (item.dp) {
          const state = await getStateAsync(item.dp);
          wert = (state && state.val != null) ? state.val : '';
          if (typeof wert === 'number' || !isNaN(parseFloat(wert))) {
            const stellen = typeof item.decimalPlaces === 'number' ? item.decimalPlaces : 2;
            wert = parseFloat(wert).toLocaleString('de-DE', { minimumFractionDigits: stellen, maximumFractionDigits: stellen });
          } else {
            wert = String(wert);
          }
        } else if (typeof item.text === 'string') {
          wert = item.text;
        }
      
        if (item.type === 'progressBar') {
          const prozent = parseFloat(wert) || 0;
          const breite  = Math.round(Math.max(0, Math.min(100, prozent)) / 100 * BILDSCHIRM_BREITE);
          const yPos    = typeof item.y === 'number' ? item.y : BILDSCHIRM_HÖHE - (item.height || 32);
          return { box: [0, yPos, breite, item.height || 32, item.fillColor || 2, item.fillColor || 2, item.fillColor || 2] };
        }
      
        if (item.prefix) wert = item.prefix + wert;
        if (item.suffix) wert = wert + item.suffix;
      
        const x      = typeof item.x === 'number' ? item.x : 0;
        const y      = typeof item.y === 'number' ? item.y : 0;
        const schrift = item.font || 'bahnschrift20';
        const farbe   = typeof item.color === 'number' ? item.color : 1;
        const textArr = [x, y, wert, schrift, farbe];
        if (typeof item.align === 'number') textArr.push(item.align);
        return { text: textArr };
      }
      
      /* ############################################################################
       *  HTTP-Sender
       * ############################################################################ */
      
      /**
       * Sendet das fertige JSON-Array an den OpenEPaperLink-Access-Point.
       * @param {string} mac        - MAC-Adresse des Tags
       * @param {Array}  jsonArray  - Vollständiges JSON-Array für das Display
       * @param {string} titel      - Nur für Logging/Console-Ausgabe
       * @returns {Promise}         - Resolves bei Erfolg, rejects bei Netzwerkfehler
       */
      function sendeAnTag(mac, jsonArray, titel) {
        const postData = querystring.stringify({ mac, json: JSON.stringify(jsonArray) });
        const options  = {
          hostname: AP_IP,
          port: 80,
          path: '/jsonupload',
          method: 'POST',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Content-Length': Buffer.byteLength(postData)
          }
        };
      
        return new Promise((resolve, reject) => {
          const req = http.request(options, res => {
            let data = '';
            res.on('data', c => data += c);
            res.on('end', () => {
              console.log(`✅ "${titel}" (${mac}) → AP-Antwort: "${data.trim() || 'leer'}"`);
              resolve();
            });
          });
          req.on('error', e => {
            console.error(`❌ "${titel}" (${mac}) → HTTP-Fehler: ${e.message}`);
            reject(e);
          });
          req.write(postData);
          req.end();
        });
      }
      
      /* ############################################################################
       *  Update-Logik
       * ############################################################################ */
      
      /**
       * Aktualisiert ein einzelnes Tag, sofern die aktuelle Zeit im konfigurierten
       * Zeitfenster liegt.  Bei Erfolg wird das fertige JSON an den AP gesendet.
       * @param {Object} tag - Ein Eintrag aus tagConfig
       * @returns {Promise}  - Resolves bei Erfolg oder wenn außerhalb des Zeitfensters
       */
      async function aktualisiereTag(tag) {
        if (!istImZeitFenster(tag.ZeitFenster)) {
          console.log(`⏸️  "${tag.title}" (${tag.mac}) außerhalb des Zeitfensters ${tag.ZeitFenster}`);
          return;
        }
      
        const json = [{ clear: [1] }, ...titelElementeErzeugen(tag.title)];
        for (const item of tag.body) {
          const ergebnis = await elementAuflösen(item);
          Array.isArray(ergebnis) ? json.push(...ergebnis) : json.push(ergebnis);
        }
        await sendeAnTag(tag.mac, json, tag.title);
      }
      
      /**
       * Startet für jeden Tag ein eigenes setInterval und führt sofort ein erstes
       * Update aus.  So kann sich jeder Tag unabhängig aktualisieren.
       */
      function starteTagUpdates() {
        for (const tag of tagConfig) {
          const intervallMs = tag.updateIntervalMinutes * 60 * 1000;
          setInterval(() => aktualisiereTag(tag).catch(e =>
            console.error(`Fehler bei ${tag.title}: ${e.message}`)), intervallMs);
          aktualisiereTag(tag).catch(e =>
            console.error(`Erstes Update fehlgeschlagen bei ${tag.title}: ${e.message}`));
        }
      }
      
      /* ############################################################################
       *  Start
       * ############################################################################ */
      starteTagUpdates();
      
      BertDerKleineB Offline
      BertDerKleineB Offline
      BertDerKleine
      schrieb am zuletzt editiert von
      #2

      P.S.: Ich habe öfter eine Schriftart "Nolo", wie "Nolo90" verwendet in dem Beispielskript. Das ist einfach eine gut lesbare Schriftart, die ich selbst reduziert auf die nötigen Zeichen und dann in das VLW Format konvertiert habe; und anschliessend natürlich auf den AP ins "fonts" Verzeichnis hochgeladen.
      Bei sehr großen Schriften wie "90" ist es wichtig, nicht zu viele Glyphen zu haben, da sonst im Pixelformat die VLW-Datei riesig wird.
      Geht aber alles in 5 min umzusetzen, da die Software dafür ja simpel zu bedienen ist.
      Ich habe mir einfach Nolo20, ... , Nolo80, Nolo90 gebaut und hochgeladen und gut ist. Zusammen knapp 1.050 kB. Im Filesystem sind dann immer noch 4MB frei.

      Rene55R 1 Antwort Letzte Antwort
      0
      • BertDerKleineB BertDerKleine

        P.S.: Ich habe öfter eine Schriftart "Nolo", wie "Nolo90" verwendet in dem Beispielskript. Das ist einfach eine gut lesbare Schriftart, die ich selbst reduziert auf die nötigen Zeichen und dann in das VLW Format konvertiert habe; und anschliessend natürlich auf den AP ins "fonts" Verzeichnis hochgeladen.
        Bei sehr großen Schriften wie "90" ist es wichtig, nicht zu viele Glyphen zu haben, da sonst im Pixelformat die VLW-Datei riesig wird.
        Geht aber alles in 5 min umzusetzen, da die Software dafür ja simpel zu bedienen ist.
        Ich habe mir einfach Nolo20, ... , Nolo80, Nolo90 gebaut und hochgeladen und gut ist. Zusammen knapp 1.050 kB. Im Filesystem sind dann immer noch 4MB frei.

        Rene55R Online
        Rene55R Online
        Rene55
        schrieb am zuletzt editiert von
        #3

        @bertderkleine Danke für den guten Ansatz mit dem Script. Beschreibst du auch noch die Vorgehensweise mit den Schriften?

        Host: Fujitsu Intel(R) Pentium(R) CPU G4560T, 32 GB RAM, Proxmox 8.x + lxc Ubuntu 22.04
        ioBroker (8 GB RAM) Node.js: 20.19.1, NPM: 10.8.2, js-Controller: 7.0.6, Admin: 7.6.3
        Wetterstation: Froggit WH3000SE V1.6.6

        BertDerKleineB 1 Antwort Letzte Antwort
        0
        • Rene55R Rene55

          @bertderkleine Danke für den guten Ansatz mit dem Script. Beschreibst du auch noch die Vorgehensweise mit den Schriften?

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

          @rene55 sagte in E-Ink OpenEpaperLink - JSON-Steuerung per Javascript:

          @bertderkleine Danke für den guten Ansatz mit dem Script. Beschreibst du auch noch die Vorgehensweise mit den Schriften?

          Gerne.
          Zuerst lade und installiere zwei kostenlose Programme:
          FontForge: https://fontforge.org/en-US/downloads/
          und
          Processing: https://processing.org/download

          Schritt 1: Entscheide Dich für irgendeine .ttf Schriftartendatei auf Deinem Rechner, die Dir gefällt für die Ausgabe auf dem Display.

          Schritt 2: Lösche die überflüssigen Glyphen mit Fontforge.

          • Starte FontForge und öffne die TTF-Datei.
          • Wähle im Menü "Codierung / Compact", um leere Plätze auszublenden.
          • Markiere mit Maus Anklicken oder Maus drüberziehen überflüssige Glyphen. Mit "Shift" kann man getrennte Mehrfachauswahlen machen, was sonst unter Windows eher bei "Strg" geht.
          • Wähle Rechtsklick / "Löschen".
          • Wähle im Menü "Element / Schrift - Informationen", um die Schrift mit einem neuen eindeutigen Namen zu versehen, der sie von der Quelle unterscheidet.
          • Wähle im Menü "Datei / Schriften erstellen", um die reduzierte Datei als TrueType zu speichern.

          Schritt 3: Installiere die neue Schriftart zumindest temporär auf Deinem System für den Schritt 4. Danach kannst Du sie wieder löschen. Unter Windows geht das mit Rechtsklick / Installieren über der TTF-Datei.

          Schritt 4: Wandle die TTF in VLW um mit Processing

          • Starte Processing.
          • Wähle im Menü "Tools / Schrift erstellen"
          • Wähle die eben erstellte und installierte Schriftart, die Zielgröße und den Zieldateinamen.
            Drücke "OK"
          • Optional: Diesen Export mit verschiedenen Zielgrößen wiederholen.

          Fertig!

          Nun nur noch ins "fonts" Verzeichnis des AP mit dem Filebrowser des WebUI des AP hochladen.

          1 Antwort Letzte Antwort
          1
          • BertDerKleineB Offline
            BertDerKleineB Offline
            BertDerKleine
            schrieb am zuletzt editiert von
            #5

            Ich teile hier noch mal eine mögliche Ergänzung, die ich bei mir selbst verwende.
            Meist gibt man ja eher Zahlen aus, das ist fein soweit. Wenn man aber längere Textfelder ausgeben will, gibt es das Problem, das der Text zu lang ist und eigentlich umgebrochen werden muss.
            Beispiel: Texte aus dem Kalender.

            Mein erster Lösungsansatz sieht im Code so aus:

            Neu hinzugekommen sind für Textfelder drei Parameter:

            • maxZeilenbreite: Zeichen, die maximal pro Zeile hinpassen auf das Display
            • maxZeilen: maximal auszugebende Zeilen auf dem Display für dieses Textfeld
            • zeilenAbstand: Pixelhöhe pro Zeile inkl. optischem Abstand

            Ersetze im Text-Block (ganz unten in "elementAuflösen" ) die Zeilen

            const textArr = [x, y, wert, schrift, farbe];
            if (typeof item.align === 'number') textArr.push(item.align);
            return { text: textArr };
            

            durch die gepimpte Version:

            // --- automatischer Zeilenumbruch ---
            if (item.maxZeilenbreite && item.maxZeilen) {
              const breite   = item.maxZeilenbreite;
              const zeilen   = item.maxZeilen;
              const abstand  = item.zeilenAbstand || 20;
              const zeilenArr = wert.split('\n');              // erlaubt manuelle \n
            
              const result = [];
              let posY = y;
              let zähler = 0;
            
              for (const rawLine of zeilenArr) {
                const words = rawLine.split(' ');
                let line = '';
                for (const w of words) {
                  const probe = line + (line ? ' ' : '') + w;
                  if (probe.length > breite && line.length) {
                    // neue Zeile
                    result.push({ text: [x, posY, line.trim(), schrift, farbe] });
                    posY += abstand;
                    zähler++;
                    if (zähler >= zeilen) return result;
                    line = w;
                  } else {
                    line = probe;
                  }
                }
                if (line) {
                  result.push({ text: [x, posY, line.trim(), schrift, farbe] });
                  posY += abstand;
                  zähler++;
                  if (zähler >= zeilen) return result;
                }
              }
              return result;
            }
            
            // --- klassischer Einzeiler (wie bisher) ---
            const textArr = [x, y, wert, schrift, farbe];
            if (typeof item.align === 'number') textArr.push(item.align);
            return { text: textArr };
            

            Die relevante Konfigurationszeile in der Konfig sieht dann z.B. so aus:

            { dp: 'alias.0.Kalendervorschau', font: 'calibrib16', color: 1, x: 0, y: 25, maxZeilenbreite: 38, maxZeilen: 7, zeilenAbstand: 17 }
            

            Das setzt zwar etwas Fingerspitzengefühl beim Benutzer voraus, aber gegen eine Vollautomatisierung spricht die Herausforderung, dass jede Schriftart in Pixeln unterschiedlich breit und hoch ist. Dazu mache ich lieber mit drei Parametern per Hand Anpassungen, als zu versuchen, das mit Formlen in den Griff zu bekommen.

            1 Antwort Letzte Antwort
            1
            • B Offline
              B Offline
              Beowolf
              schrieb am zuletzt editiert von
              #6

              Könntest du die kleinen Grafiken hier anzeigen?

              Also window_open48.jpg usw.

              Grüße

              Die Natur braucht nicht unseren Schutz, sie braucht unsere Abwesenheit.

              1 Antwort Letzte Antwort
              0
              • BertDerKleineB BertDerKleine

                Nachdem die meisten OEPL Steuerungen anscheinend über den Weg der Screenshots mit Puppeteer gehen, hier mal mein Ansatz über JSONs.

                Mein Puppeteer Skript habe ich hierdurch ersetzt und bin sehr zufrieden damit.

                Mittels der Konfigurationsblöcke kann ich sehr simpel umsetzen, was ich mag und jeder Tag hat seine eigene Zeitsteuerung. Ich habe mal ein paar Varianten reinkopiert, die ich durchgespielt habe.

                Vielleicht hilfts ja jemandem. :relaxed:

                /**
                 * ############################################################################
                 *  OpenEPaperLink – vollautomatische ePaper-Steuerung mit Zeitschaltung
                 *  und Signalbildern für Fenster- / Tür-Status, realisiert über JSON-Templates
                 * ############################################################################
                 *
                 *  Im August 2025 von BertDerKleine
                 *
                 *  Zweck
                 *  =====
                 *  Dieses Skript versorgt alle registrierten OpenEPaperLink-Tags (WLAN-ePaper-
                 *  Displays) zyklisch mit aktuellen Daten aus ioBroker.  Durch konfigurierbare
                 *  Zeitfenster und individuelle Update-Intervalle wird die Batterielaufzeit
                 *  maximal geschont.  Neu hinzugekommen ist die Möglichkeit, einfache
                 *  „Signalbilder“ einzublenden: anhand eines Boolean-Datenpunktes wird
                 *  automatisch zwischen zwei Icon-Dateien (z. B. offen / geschlossen)
                 *  gewechselt.
                 *  Ziel war es, eine Lösung via JSON-Templates hinzubekommen, ohne den Umweg
                 *  über Screenshots mit Puppeteer. U.a. umgeht dies das Problem, das Puppeteer
                 *  immer nur eine Sache gleichzeitig tun kann, was zu Konflikten führen kann,
                 *  wenn man viele Tags aktualisieren muss. 
                 *
                 *  Ablauf im Überblick
                 *  -------------------
                 *  1.  Konstanten am Kopf definieren Display-Größe und Access-Point.
                 *  2.  In `tagConfig` pro Tag eintragen:
                 *        – MAC-Adresse
                 *        – Anzeige-Titel
                 *        – Update-Intervall in Minuten
                 *        – ZeitFenster (z. B. „07:00-23:00“)
                 *        – body: Array aller anzuzeigenden Elemente
                 *  3.  `starteTagUpdates()` startet für jeden Tag ein eigenes Intervall
                 *      und führt sofort ein erstes Update aus.
                 *  4.  `aktualisiereTag()` prüft das ZeitFenster, baut das JSON zusammen
                 *      und sendet es per HTTP-POST an den Access-Point.
                 *
                 *  Element-Typen
                 *  -------------
                 *  •  Text:             { dp: 'pfad/zum/Datenpunkt', font: 'Nolo90', … }
                 *  •  Fortschrittsbalken: { type: 'progressBar', dp: '…', fillColor: 2, … }
                 *  •  Bild:             { image: { filename: '/Bild.jpg', x: 10, y: 20 } }
                 *  •  Signalbild:       { type: 'signalImage',
                 *                          dp: 'alias.0.Fenster_Kueche',
                 *                          images: { true: '/offen.jpg', false: '/zu.jpg' },
                 *                          x, y, imageWidth, imageHeight, label, … }
                 *
                 *  Voraussetzungen
                 *  ---------------
                 *  • ioBroker mit JavaScript-Adapter
                 *  • Node-Modul „luxon“ (nur für zuverlässige Sommer-/Winterzeit)
                 *      npm install luxon
                 *  • Bild- und Icon-Dateien liegen im Root des Access-Points als 8bit sRGB JPGs
                 *    rein schwarz, weiss und rot.
                 *  ############################################################################
                 */
                
                const http          = require('http');
                const querystring   = require('querystring');
                const { DateTime }  = require('luxon');
                
                /* --------------------------------------------------------------------------
                 *  Globale Konstanten
                 * -------------------------------------------------------------------------- */
                const AP_IP               = '192.168.178.233';   // IP des OpenEPaperLink-AP
                const BILDSCHIRM_BREITE   = 296;
                const BILDSCHIRM_HÖHE     = 152;
                const TITEL_BALKEN_HÖHE   = 20;
                const TITEL_SCHRIFT       = 'calibrib16';
                
                /* --------------------------------------------------------------------------
                 *  Tag-Konfiguration
                 *  Jeder Eintrag definiert exakt ein ePaper-Display.
                 * -------------------------------------------------------------------------- */
                // Konfiguration der Tags mit MAC-Adresse, Titel, Update-Intervall, Zeitfenster und Inhalt
                const tagConfig = [
                  {
                    mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für DG Außentemperatur 
                    title: 'Aussentemperatur DG', // Titel, der auf dem Tag angezeigt wird
                    updateIntervalMinutes: 30, // Update alle 30 Minuten
                    ZeitFenster: '07:00-23:00', // Updates zwischen 07:00 und 23:00 (Europe/Berlin)
                    body: [
                      // Temperatur mit großer Schrift, linksbündig
                      { dp: 'alias.0.Aussentemperatur_kombiniert', font: 'Nolo90', color: 1, suffix: '°C', x: 10, y: 40, decimalPlaces: 1 },
                      // Regenmenge pro Tag, unten links
                      { dp: 'alias.0.Regen_pro_Tag', font: 'bahnschrift30', color: 2, suffix: 'mm', x: 5, y: 127, decimalPlaces: 1 },
                      // Luftfeuchtigkeit, rechtsbündig unten
                      { dp: 'alias.0.Luftfeuchtigkeit_Gartenhaus', font: 'bahnschrift30', color: 1, prefix: 'LF ', suffix: '%', x: 296, y: 115, align: 2, decimalPlaces: 0 }
                    ]
                  },
                  
                  {
                    mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für Waage 
                    title: 'Waage', 
                    updateIntervalMinutes: 10, 
                    ZeitFenster: '06:30-12:00', 
                    body: [
                      // Körpergewicht mit großer Schrift, linksbündig
                      { dp: 'alias.0.Körpergewicht', font: 'Nolo70', color: 1, suffix: ' kg', x: 10, y: 40, decimalPlaces: 2 },
                      // Widerstand, unten links
                      { dp: 'alias.0.Körperwiderstand', font: 'bahnschrift30', color: 2, suffix: 'Ohm', x: 5, y: 127, decimalPlaces: 0 },
                      // BMI, rechtsbündig unten
                      { dp: 'alias.0.BMI', font: 'bahnschrift30', color: 1, prefix: 'BMI: ', suffix: '', x: 296, y: 115, align: 2, decimalPlaces: 1 }
                    ]
                  },
                  {
                    mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für die Kalendervorschau
                    title: 'Kalender',
                    updateIntervalMinutes: 120,
                    ZeitFenster: '07:00-23:00',
                    body: [
                      // Kalendertext, oben links
                      { dp: 'alias.0.Kalendervorschau', font: 'bahnschrift20', color: 1, x: 0, y: 25 }
                    ]
                  },
                  {
                    mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für Akku-Status
                    title: 'Akku-Status',
                    updateIntervalMinutes: 30,
                    ZeitFenster: '07:00-23:00',
                    body: [
                      // Akku-Prozentsatz, rechtsbündig
                      { dp: 'alias.0.Akku_Ladezustand', font: 'Nolo90', color: 1, suffix: '%', x: BILDSCHIRM_BREITE, y: 40, align: 2, decimalPlaces: 0 },
                      // Fortschrittsbalken für Akku, unten
                      { dp: 'alias.0.Akku_Ladezustand', type: 'progressBar', fillColor: 2, height: 32, y: BILDSCHIRM_HÖHE - 32 }
                    ]
                  },
                  {
                    mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für Stromwerte
                    title: 'Stromwerte',
                    updateIntervalMinutes: 60,
                    ZeitFenster: '07:00-23:00',
                    body: [
                      { image: { filename: '/Sonne.jpg', x: 10, y: 25 } },
                      // PV-Erzeugung, rechtsbündig
                      { dp: 'sourceanalytix.0.alias__0__PV_Erzeugungszählerstand.currentYear.consumed.01_currentDay', font: 'bahnschrift30', color: 1, prefix: 'PV: ', suffix: ' kWh', x: BILDSCHIRM_BREITE, y: 28, align: 2, decimalPlaces: 1 },
                      // Netzbezug, rechtsbündig
                      { dp: 'sourceanalytix.0.alias__0__Netzbezugszählerstand.currentYear.consumed.01_currentDay', font: 'bahnschrift30', color: 2, prefix: 'Bezug: ', suffix: ' kWh', x: BILDSCHIRM_BREITE, y: 58, align: 2, decimalPlaces: 1 },
                      // Netzeinspeisung, rechtsbündig
                      { dp: 'sourceanalytix.0.alias__0__Netzeinspeisezählerstand.currentYear.delivered.01_currentDay', font: 'bahnschrift30', color: 1, prefix: 'Einsp.: ', suffix: ' kWh', x: BILDSCHIRM_BREITE, y: 88, align: 2, decimalPlaces: 1 },
                      // Verbrauch, rechtsbündig
                      { dp: 'sourceanalytix.0.alias__0__Verbrauchszaehlerstand.currentYear.consumed.01_currentDay', font: 'bahnschrift30', color: 1, prefix: 'Verbr.: ', suffix: ' kWh', x: BILDSCHIRM_BREITE, y: 118, align: 2, decimalPlaces: 1 }
                    ]
                  },
                  {
                    mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für Wasserverbrauch
                    title: 'Wasserverbrauch',
                    updateIntervalMinutes: 45,
                    ZeitFenster: '07:00-23:00',
                    body: [
                      { image: { filename: '/Wasser.jpg', x: 10, y: 25, } },
                      { dp: 'alias.0.Tages-Wasserverbrauch', font: 'Nolo90', color: 1, prefix: '', suffix: ' l', x: BILDSCHIRM_BREITE, y: 50, align: 2, decimalPlaces: 0 },
                    ]
                  },
                  {
                    mac: '0000AABBCCDDEEFF', // MAC-Adresse des Tags für offene Fenster/Türen
                    title: 'Fensterstatus',
                    updateIntervalMinutes: 15,
                    ZeitFenster: '07:00-23:00',
                    body: [
                      // Reihe 1 von links nach rechts; die verwendeten Signalbilder sind 48x48 Pixel gross
                      { type: 'signalImage', dp: 'alias.0.Fenster_Küche', images: { true: '/window_open48.jpg', false: '/window_closed48.jpg' }, x: 38, y: 22, imageWidth: 48, imageHeight: 48, label: 'Küche', labelColor: 1, labelSpacing: 2 },
                      { type: 'signalImage', dp: 'alias.0.Fenster_Wohnzimmer', images: { true: '/window_open48.jpg', false: '/window_closed48.jpg' }, x: 124, y: 22, imageWidth: 48, imageHeight: 48, label: 'Wohnzimmer', labelColor: 1, labelSpacing: 2 },
                      { type: 'signalImage', dp: 'alias.0.Fenster_Keller', images: { true: '/window_open48.jpg', false: '/window_closed48.jpg' }, x: 210, y: 22, imageWidth: 48, imageHeight: 48, label: 'Keller', labelColor: 1, labelSpacing: 2 },
                
                      // Reihe 2 von links nach rechts
                      { type: 'signalImage', dp: 'alias.0.Fenster_Gäste-WC', images: { true: '/window_open48.jpg', false: '/window_closed48.jpg' }, x: 38, y: 82, imageWidth: 48, imageHeight: 48, label: 'Gäste-WC', labelColor: 1, labelSpacing: 2 },
                      { type: 'signalImage', dp: 'alias.0.Tür_Terrasse', images: { true: '/door-open48.jpg', false: '/door-closed48.jpg' }, x: 124, y: 82, imageWidth: 48, imageHeight: 48, label: 'Terrassentür', labelColor: 1, labelSpacing: 2 },
                      { type: 'signalImage', dp: 'alias.0.Fenster_Garage', images: { true: '/window_open48.jpg', false: '/window_closed48.jpg' }, x: 210, y: 82, imageWidth: 48, imageHeight: 48, label: 'Garage', labelColor: 1, labelSpacing: 2 }
                    ]
                  }
                ];
                
                /* ############################################################################
                 *  Hilfsfunktionen
                 * ############################################################################ */
                
                /**
                 * Prüft, ob die aktuelle Zeit im konfigurierten Zeitfenster liegt.
                 * @param {string} ZeitFenster - Zeitfenster im Format "HH:mm-HH:mm" (z. B. "07:00-23:00").
                 * @returns {boolean} - True, wenn die aktuelle Zeit im Zeitfenster liegt, sonst false.
                 */
                function istImZeitFenster(ZeitFenster) {
                  const jetzt = DateTime.now().setZone('Europe/Berlin');
                  const [von, bis] = ZeitFenster.split('-').map(t => {
                    const [h, m] = t.split(':').map(Number);
                    return jetzt.set({ hour: h, minute: m, second: 0, millisecond: 0 });
                  });
                  return bis < von ? (jetzt >= von || jetzt <= bis) : (jetzt >= von && jetzt <= bis);
                }
                
                /**
                 * Konvertiert eine Zeitangabe (HH:mm) in Minuten seit Mitternacht für Vergleiche.
                 * @param {string} ZeitStr - Zeit im Format "HH:mm".
                 * @returns {number} - Minuten seit Mitternacht.
                 */
                function zeitZuMinuten(ZeitStr) {
                  const [h, m] = ZeitStr.split(':').map(Number);
                  return h * 60 + m;
                }
                
                /**
                 * Erstellt die JSON-Elemente für die Titelleiste (Hintergrund + Text).
                 * @param {string} titelText - Text, der in der Titelleiste erscheinen soll.
                 * @returns {Array} - Array mit JSON-Elementen (box + text).
                 */
                function titelElementeErzeugen(titelText) {
                  const mitteX = Math.floor(BILDSCHIRM_BREITE / 2);
                  return [
                    { box: [0, 0, BILDSCHIRM_BREITE, TITEL_BALKEN_HÖHE, 1, 1, 1] },
                    { text: [mitteX, 3, titelText, `fonts/${TITEL_SCHRIFT}`, 0, 1] }
                  ];
                }
                
                /**
                 * Verarbeitet ein einzelnes Anzeige-Element und liefert das dafür nötige JSON.
                 * Erkannt werden:
                 *  – Signalbilder (signalImage)
                 *  – normale Bilder (image)
                 *  – Texte (dp oder fester text)
                 *  – Fortschrittsbalken (progressBar)
                 *
                 * @param {Object} item - Konfiguration des Elements
                 * @returns {Object|Array} - JSON-Objekt oder Array von Objekten für die Darstellung
                 */
                async function elementAuflösen(item) {
                  /* ---------- Signalbild mit optionalem Label ---------- */
                  if (item.type === 'signalImage') {
                    const status = await getStateAsync(item.dp);
                    const bild   = (status && status.val) ? item.images.true : item.images.false;
                    const result = [{ image: [bild, item.x || 0, item.y || 0] }];
                    if (item.label) {
                      const labelX = (item.x || 0) + Math.floor((item.imageWidth || 48) / 2);
                      const labelY = (item.y || 0) + (item.imageHeight || 48) + (item.labelSpacing || 2);
                      result.push({ text: [labelX, labelY, item.label, 'tahoma11', item.labelColor || 1, 1] });
                    }
                    return result;
                  }
                
                  /* ---------- Normales Bild ---------- */
                  if (item.image) {
                    const { filename, x = 0, y = 0 } = item.image;
                    return { image: [filename, x, y] };
                  }
                
                  /* ---------- Text & Balken ---------- */
                  let wert = '';
                  if (item.dp) {
                    const state = await getStateAsync(item.dp);
                    wert = (state && state.val != null) ? state.val : '';
                    if (typeof wert === 'number' || !isNaN(parseFloat(wert))) {
                      const stellen = typeof item.decimalPlaces === 'number' ? item.decimalPlaces : 2;
                      wert = parseFloat(wert).toLocaleString('de-DE', { minimumFractionDigits: stellen, maximumFractionDigits: stellen });
                    } else {
                      wert = String(wert);
                    }
                  } else if (typeof item.text === 'string') {
                    wert = item.text;
                  }
                
                  if (item.type === 'progressBar') {
                    const prozent = parseFloat(wert) || 0;
                    const breite  = Math.round(Math.max(0, Math.min(100, prozent)) / 100 * BILDSCHIRM_BREITE);
                    const yPos    = typeof item.y === 'number' ? item.y : BILDSCHIRM_HÖHE - (item.height || 32);
                    return { box: [0, yPos, breite, item.height || 32, item.fillColor || 2, item.fillColor || 2, item.fillColor || 2] };
                  }
                
                  if (item.prefix) wert = item.prefix + wert;
                  if (item.suffix) wert = wert + item.suffix;
                
                  const x      = typeof item.x === 'number' ? item.x : 0;
                  const y      = typeof item.y === 'number' ? item.y : 0;
                  const schrift = item.font || 'bahnschrift20';
                  const farbe   = typeof item.color === 'number' ? item.color : 1;
                  const textArr = [x, y, wert, schrift, farbe];
                  if (typeof item.align === 'number') textArr.push(item.align);
                  return { text: textArr };
                }
                
                /* ############################################################################
                 *  HTTP-Sender
                 * ############################################################################ */
                
                /**
                 * Sendet das fertige JSON-Array an den OpenEPaperLink-Access-Point.
                 * @param {string} mac        - MAC-Adresse des Tags
                 * @param {Array}  jsonArray  - Vollständiges JSON-Array für das Display
                 * @param {string} titel      - Nur für Logging/Console-Ausgabe
                 * @returns {Promise}         - Resolves bei Erfolg, rejects bei Netzwerkfehler
                 */
                function sendeAnTag(mac, jsonArray, titel) {
                  const postData = querystring.stringify({ mac, json: JSON.stringify(jsonArray) });
                  const options  = {
                    hostname: AP_IP,
                    port: 80,
                    path: '/jsonupload',
                    method: 'POST',
                    headers: {
                      'Content-Type': 'application/x-www-form-urlencoded',
                      'Content-Length': Buffer.byteLength(postData)
                    }
                  };
                
                  return new Promise((resolve, reject) => {
                    const req = http.request(options, res => {
                      let data = '';
                      res.on('data', c => data += c);
                      res.on('end', () => {
                        console.log(`✅ "${titel}" (${mac}) → AP-Antwort: "${data.trim() || 'leer'}"`);
                        resolve();
                      });
                    });
                    req.on('error', e => {
                      console.error(`❌ "${titel}" (${mac}) → HTTP-Fehler: ${e.message}`);
                      reject(e);
                    });
                    req.write(postData);
                    req.end();
                  });
                }
                
                /* ############################################################################
                 *  Update-Logik
                 * ############################################################################ */
                
                /**
                 * Aktualisiert ein einzelnes Tag, sofern die aktuelle Zeit im konfigurierten
                 * Zeitfenster liegt.  Bei Erfolg wird das fertige JSON an den AP gesendet.
                 * @param {Object} tag - Ein Eintrag aus tagConfig
                 * @returns {Promise}  - Resolves bei Erfolg oder wenn außerhalb des Zeitfensters
                 */
                async function aktualisiereTag(tag) {
                  if (!istImZeitFenster(tag.ZeitFenster)) {
                    console.log(`⏸️  "${tag.title}" (${tag.mac}) außerhalb des Zeitfensters ${tag.ZeitFenster}`);
                    return;
                  }
                
                  const json = [{ clear: [1] }, ...titelElementeErzeugen(tag.title)];
                  for (const item of tag.body) {
                    const ergebnis = await elementAuflösen(item);
                    Array.isArray(ergebnis) ? json.push(...ergebnis) : json.push(ergebnis);
                  }
                  await sendeAnTag(tag.mac, json, tag.title);
                }
                
                /**
                 * Startet für jeden Tag ein eigenes setInterval und führt sofort ein erstes
                 * Update aus.  So kann sich jeder Tag unabhängig aktualisieren.
                 */
                function starteTagUpdates() {
                  for (const tag of tagConfig) {
                    const intervallMs = tag.updateIntervalMinutes * 60 * 1000;
                    setInterval(() => aktualisiereTag(tag).catch(e =>
                      console.error(`Fehler bei ${tag.title}: ${e.message}`)), intervallMs);
                    aktualisiereTag(tag).catch(e =>
                      console.error(`Erstes Update fehlgeschlagen bei ${tag.title}: ${e.message}`));
                  }
                }
                
                /* ############################################################################
                 *  Start
                 * ############################################################################ */
                starteTagUpdates();
                
                B Offline
                B Offline
                Beowolf
                schrieb am zuletzt editiert von
                #7

                @BertDerKleine

                Mal ne Frage. Ich habe hier einge grpße Displays. 800 breit und 480 hoch.

                Gibt es mit deinem Skript eine Möglichkeit, das ich die Displays auch hochkant ansprechen kann?

                Bis jetzt bekomme ich es nur hin, das ich mir alles im Querformat anzeigen lasse. Bei vielen "Info-Displays" wäre mir es aber hochkant lieber.

                Im AP von den TAGs kann ich in den Einstellungen zum einzelnen TAG ja nur um 180° drehen.

                Grüße

                Die Natur braucht nicht unseren Schutz, sie braucht unsere Abwesenheit.

                BertDerKleineB 1 Antwort Letzte Antwort
                0
                • B Beowolf

                  @BertDerKleine

                  Mal ne Frage. Ich habe hier einge grpße Displays. 800 breit und 480 hoch.

                  Gibt es mit deinem Skript eine Möglichkeit, das ich die Displays auch hochkant ansprechen kann?

                  Bis jetzt bekomme ich es nur hin, das ich mir alles im Querformat anzeigen lasse. Bei vielen "Info-Displays" wäre mir es aber hochkant lieber.

                  Im AP von den TAGs kann ich in den Einstellungen zum einzelnen TAG ja nur um 180° drehen.

                  Grüße

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

                  @Beowolf
                  Nein, da ich die Anforderung bisher nicht hatte, kann das Skript aktuell nichts drehen.

                  Ich habe auch keine Idee, wie schwierig das wäre, einzubauen.
                  Ich befürchte, dass OEPL für Drehungen von Texten um 90 Grad auch nichts bereithält.

                  Ich habe mir jüngst noch Features eingebaut, um Temperaturkurven mit gefüllter Fläche zwischen Achse und Kurve ausgeben zu können und das war schwieriger, als ich dachte. Die dynamische Positionierung der X-Achse bei unterschiedlichen Schwankungsbreiten bekomme ich auch immer noch nicht ganz in den Griff.

                  B 1 Antwort Letzte Antwort
                  0
                  • BertDerKleineB BertDerKleine

                    @Beowolf
                    Nein, da ich die Anforderung bisher nicht hatte, kann das Skript aktuell nichts drehen.

                    Ich habe auch keine Idee, wie schwierig das wäre, einzubauen.
                    Ich befürchte, dass OEPL für Drehungen von Texten um 90 Grad auch nichts bereithält.

                    Ich habe mir jüngst noch Features eingebaut, um Temperaturkurven mit gefüllter Fläche zwischen Achse und Kurve ausgeben zu können und das war schwieriger, als ich dachte. Die dynamische Positionierung der X-Achse bei unterschiedlichen Schwankungsbreiten bekomme ich auch immer noch nicht ganz in den Griff.

                    B Offline
                    B Offline
                    Beowolf
                    schrieb am zuletzt editiert von Beowolf
                    #9

                    @BertDerKleine sagte in E-Ink OpenEpaperLink - JSON-Steuerung per Javascript:

                    Ich habe auch keine Idee, wie schwierig das wäre, einzubauen.

                    Hatte auch keine Idee, bis mir das Wiki wieder einfiel. Dort hatte ich in grauer Vorzeit mal was von "Rotate Screen" gelesen.

                    Siehe da - es funktioniert.

                    Ich habe folgendes in deinem Skript geändert:

                    Zeilen 65 und 66

                    const BILDSCHIRM_BREITE   = 480;
                    const BILDSCHIRM_HÖHE     = 800;
                    

                    für mein grosses Display

                    und nach der Zeile 208 das eingefügt

                        { "rotate": 1 },
                    

                    Sieht also nachher so aus:

                    /**
                     * Erstellt die JSON-Elemente für die Titelleiste (Hintergrund + Text).
                     * @param {string} titelText - Text, der in der Titelleiste erscheinen soll.
                     * @returns {Array} - Array mit JSON-Elementen (box + text).
                     */
                    function titelElementeErzeugen(titelText) {
                      const mitteX = Math.floor(BILDSCHIRM_BREITE / 2);
                      return [
                        { "rotate": 1 },
                        { box: [0, 0, BILDSCHIRM_BREITE, TITEL_BALKEN_HÖHE, 1, 1, 1] },
                        { text: [mitteX, 3, titelText, `fonts/${TITEL_SCHRIFT}`, 0, 1] }
                      ];
                    }
                    

                    Ob das dort genau an der richtigen Stelle ist - keine Ahnung, bin nicht so der JavaSkript Held. Es funktioniert aber.

                    Die Natur braucht nicht unseren Schutz, sie braucht unsere Abwesenheit.

                    BertDerKleineB 1 Antwort Letzte Antwort
                    1
                    • B Beowolf

                      @BertDerKleine sagte in E-Ink OpenEpaperLink - JSON-Steuerung per Javascript:

                      Ich habe auch keine Idee, wie schwierig das wäre, einzubauen.

                      Hatte auch keine Idee, bis mir das Wiki wieder einfiel. Dort hatte ich in grauer Vorzeit mal was von "Rotate Screen" gelesen.

                      Siehe da - es funktioniert.

                      Ich habe folgendes in deinem Skript geändert:

                      Zeilen 65 und 66

                      const BILDSCHIRM_BREITE   = 480;
                      const BILDSCHIRM_HÖHE     = 800;
                      

                      für mein grosses Display

                      und nach der Zeile 208 das eingefügt

                          { "rotate": 1 },
                      

                      Sieht also nachher so aus:

                      /**
                       * Erstellt die JSON-Elemente für die Titelleiste (Hintergrund + Text).
                       * @param {string} titelText - Text, der in der Titelleiste erscheinen soll.
                       * @returns {Array} - Array mit JSON-Elementen (box + text).
                       */
                      function titelElementeErzeugen(titelText) {
                        const mitteX = Math.floor(BILDSCHIRM_BREITE / 2);
                        return [
                          { "rotate": 1 },
                          { box: [0, 0, BILDSCHIRM_BREITE, TITEL_BALKEN_HÖHE, 1, 1, 1] },
                          { text: [mitteX, 3, titelText, `fonts/${TITEL_SCHRIFT}`, 0, 1] }
                        ];
                      }
                      

                      Ob das dort genau an der richtigen Stelle ist - keine Ahnung, bin nicht so der JavaSkript Held. Es funktioniert aber.

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

                      @Beowolf sagte in E-Ink OpenEpaperLink - JSON-Steuerung per Javascript:

                      @BertDerKleine sagte in E-Ink OpenEpaperLink - JSON-Steuerung per Javascript:

                      Ich habe auch keine Idee, wie schwierig das wäre, einzubauen.

                      Hatte auch keine Idee, bis mir das Wiki wieder einfiel. Dort hatte ich in grauer Vorzeit mal was von "Rotate Screen" gelesen.

                      Siehe da - es funktioniert.

                      Ich habe folgendes in deinem Skript geändert:

                      Zeilen 65 und 66

                      const BILDSCHIRM_BREITE   = 480;
                      const BILDSCHIRM_HÖHE     = 800;
                      

                      für mein grosses Display

                      und nach der Zeile 208 das eingefügt

                          { "rotate": 1 },
                      

                      Sieht also nachher so aus:

                      /**
                       * Erstellt die JSON-Elemente für die Titelleiste (Hintergrund + Text).
                       * @param {string} titelText - Text, der in der Titelleiste erscheinen soll.
                       * @returns {Array} - Array mit JSON-Elementen (box + text).
                       */
                      function titelElementeErzeugen(titelText) {
                        const mitteX = Math.floor(BILDSCHIRM_BREITE / 2);
                        return [
                          { "rotate": 1 },
                          { box: [0, 0, BILDSCHIRM_BREITE, TITEL_BALKEN_HÖHE, 1, 1, 1] },
                          { text: [mitteX, 3, titelText, `fonts/${TITEL_SCHRIFT}`, 0, 1] }
                        ];
                      }
                      

                      Ob das dort genau an der richtigen Stelle ist - keine Ahnung, bin nicht so der JavaSkript Held. Es funktioniert aber.

                      Spannend Danke!
                      Ich habe eben das Wiki gesucht und auch den rotate Befehl und gefunden. Steht natürlich ganz unten. :-)

                      Wo gibts denn eigentlich 800x480 große Displays für OEPL?

                      1 Antwort Letzte Antwort
                      0
                      • BertDerKleineB Offline
                        BertDerKleineB Offline
                        BertDerKleine
                        schrieb am zuletzt editiert von BertDerKleine
                        #11

                        Kleiner Kommentar am Rande:
                        Die Farben, die die üblichen zweifarbigen Displays mit OEPL erzeugen können, sind anders, als man es im Wiki lesen kann.

                        Hier sieht man die 7 möglichen (Füll-) Farben mit ihrem Farbcode jeweils:

                        Farben.png

                        Hintergrund-Weiss gibts natürlich auch.

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


                        Support us

                        ioBroker
                        Community Adapters
                        Donate

                        602

                        Online

                        32.6k

                        Benutzer

                        82.1k

                        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