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. Einfache Solarleistungsanzeige

NEWS

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

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

  • Weihnachtsangebot 2025! 🎄
    BluefoxB
    Bluefox
    25
    1
    2.1k

Einfache Solarleistungsanzeige

Geplant Angeheftet Gesperrt Verschoben Praktische Anwendungen (Showcase)
4 Beiträge 2 Kommentatoren 546 Aufrufe 3 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.
  • B Offline
    B Offline
    Beowolf
    schrieb am zuletzt editiert von Beowolf
    #1

    Ich habe mal eine einfache Anzeige der derzeitigen Solarleistung gebastelt. Meine Frau hatte immer gefragt, ob jetzt ein guter Zeitpunkt sei um dieses oder jenes laufen zu lassen. Also Waschmaschine, Kochen usw..

    Die Werte holt sich das Skript von OpenDTU und einem Hichi Lesekopf.

    Programmübersicht
    Dieses Programm läuft auf einem ESP8266-Mikrocontroller (z. B. NodeMCU oder Wemos D1 mini) und dient als Energie-Monitoring- und Anzeige-System.
    Es verbindet sich mit einem WLAN, liest Energiedaten von zwei Quellen (OpenDTU und HiChi/Tasmota-Stromzähler), zeigt die Werte auf einem TFT-Display an und stellt zusätzlich ein Web-Dashboard mit Live-Daten zur Verfügung.
    Über eine Weboberfläche können Einstellungen zur Display-Hintergrundbeleuchtung (An/Aus/Dimmen) vorgenommen und im EEPROM dauerhaft gespeichert werden.
    Zusätzlich unterstützt das Programm Over-the-Air Updates (OTA) und mDNS, damit das Gerät leicht im Netzwerk auffindbar ist.


    Hauptfunktionen des Programms

    1. WLAN-Verbindung
      • Das Gerät verbindet sich mit den im Code hinterlegten WLAN-Zugangsdaten (ssid, password).
      • Nach erfolgreicher Verbindung ist es über die lokale IP-Adresse erreichbar.
      • Die IP wird beim Start für kurze Zeit auf dem TFT-Display angezeigt.

    1. Zeitverwaltung
      • Die lokale Zeit wird über NTP-Server synchronisiert (pool.ntp.org, time.nist.gov).
      • Die Funktion isDST() berücksichtigt Sommerzeit (DST) und stellt sicher, dass die Uhrzeit korrekt angezeigt und für Beleuchtungssteuerung verwendet wird.

    1. Hintergrundbeleuchtung des Displays
      • Die TFT-Hintergrundbeleuchtung wird je nach Uhrzeit automatisch gesteuert:
      o Voll an: tagsüber
      o Gedimmt: ab einer festgelegten Uhrzeit am Abend
      o Aus: nachts
      • Diese Zeiten sind in Variablen gespeichert (stunde_an, stunde_dimmen, stunde_aus) und können über die Weboberfläche geändert werden.
      • Werte werden im EEPROM gespeichert, damit sie auch nach einem Neustart erhalten bleiben.

    1. Anzeige auf dem TFT-Display
      • Das Display zeigt nach dem Start zunächst die IP-Adresse für 5 Sekunden.
      • Danach wird ein Hauptbildschirm angezeigt:
      o Solarleistung (Watt) von der OpenDTU
      o Netzleistung (Watt) vom Zähler
      o Einspeisung (kWh)
      o Bezug (kWh)
      • Unten wird ein farbiges Feld angezeigt:
      o Rot mit "Netzbezug", wenn Strom aus dem Netz gezogen wird.
      o Grün mit "Einspeisung", wenn Strom ins Netz eingespeist wird.

    1. Abruf von Energiedaten
      • OpenDTU-API (opendtu_url): liefert die aktuelle Solarleistung.
      • HiChi/Tasmota-Zähler (hichi_url): liefert Netzleistung, Einspeisung und Bezug.
      • Die Abfrage erfolgt regelmäßig alle 5 Sekunden (updateInterval).
      • Daten werden per HTTP GET abgerufen und mit ArduinoJson geparst.

    1. Webserver-Funktionen
      Das Programm startet einen lokalen Webserver (Port 80), erreichbar über die IP-Adresse oder per mDNS („esp.local“).
      • / (Root):
      Dashboard-Seite im Browser mit Anzeige von Solar, Netz, Einspeisung, Bezug und der aktuellen Uhrzeit.
      Die Daten werden per AJAX (fetch /data) alle 5 Sekunden aktualisiert.
      • /data:
      Liefert die aktuellen Messwerte als JSON (für die Live-Aktualisierung im Browser).
      • /set:
      Ermöglicht das Ändern der Zeiten für An, Dimmen, Aus und des Dimmwertes.
      Werte werden geprüft, gespeichert und ins EEPROM geschrieben.

    1. OTA-Updates
      • Mit ArduinoOTA kann das Programm im Netzwerk neu geflasht werden, ohne den ESP8266 über USB anschließen zu müssen.
      • Das Gerät ist durch Hostname und Passwort abgesichert.

    Ablauf im loop()
    Die Endlosschleife (loop()) kümmert sich um:

    1. Abwicklung von OTA-Updates (ArduinoOTA.handle()).
    2. Beantwortung von Webserver-Anfragen (server.handleClient()).
    3. Anzeige der IP-Adresse beim Start für 5 Sekunden.
    4. Regelmäßige Abfrage der Energiedaten (alle 5 Sekunden).
    5. Aktualisierung der TFT-Anzeige mit den neuen Werten.
    6. Steuerung der Hintergrundbeleuchtung in Abhängigkeit von Uhrzeit und gespeicherten Einstellungen.

    Zusammenfassung
    Das Programm realisiert ein komplettes Energie-Monitoring-System mit:
    • Automatischer Abfrage von Solar- und Netzleistungen.
    • Anzeige der Werte auf einem TFT-Farbdisplay.
    • Steuerung der Display-Beleuchtung nach Uhrzeit.
    • Bereitstellung einer Weboberfläche mit Live-Daten und Einstellmöglichkeiten.
    • Speicherung von Benutzer-Einstellungen im EEPROM.
    • OTA-Update-Funktion für einfache Wartung.

    Skript:

    /*
      Ausführlich kommentierte Version des bereitgestellten ESP8266-Skripts.
      Dieses Skript verbindet sich mit einem WLAN, holt LIVE-Daten von zwei HTTP-APIs,
      zeigt Werte auf einem TFT-Display an, bietet ein kleines Web-Dashboard zum Lesen
      und Einstellen von Hintergrundlicht-Zeiten und unterstützt OTA-Updates.
    */
    
    #include <ESP8266WiFi.h>        // WiFi-Funktionalität für den ESP8266
    #include <ESP8266HTTPClient.h>  // HTTP-Client, um GET/POST-Requests zu machen
    #include <ESP8266WebServer.h>   // Einfacher HTTP-Server auf dem Gerät
    #include <ESP8266mDNS.h>        // mDNS (Multicast DNS) zur Namensauflösung im LAN
    #include <TFT_eSPI.h>           // Treiber für das TFT-Display (ILI9341/ILI9488 etc.)
    #include <ArduinoOTA.h>         // Over-the-Air Firmware-Updates (OTA)
    #include <ArduinoJson.h>        // JSON-Parsing für Antworten der APIs
    #include <time.h>               // Zeitfunktionen (time_t, struct tm, localtime...)
    #include <EEPROM.h>             // EEPROM zum dauerhaften Speichern kleiner Einstellungen
    
    #define EEPROM_SIZE 64         // Größe des verwendeten EEPROM-Bereichs
    
    // ---------- Netzwerk- und Service-Konfiguration (statisch im Sketch) ----------
    const char* ssid = "xxxxx";                  // WLAN-SSID
    const char* password = "xxxxxxxxxxxxxxx"; // WLAN-Passwort
    
    // URLs und Zugangsdaten für die entfernten Dienste
    const char* opendtu_url = "http://192.168.xxx.xxx/api/livedata/status"; // API 1
    const char* opendtu_user = "xxxxxxxxxx";                                      // Basic-Auth Benutzer
    const char* opendtu_password = "xxxxxxxxxxxxx";                         // Basic-Auth Passwort
    
    const char* hichi_url = "http://192.168.xxx.xxx/cm?cmnd=status%208";     // API 2 (z.B. Tasmota-Gerät)
    
    // OTA (Over-The-Air) Einstellungen
    String otaHostname = "ESP-Solar-1"; // Hostname für OTA
    String otaPassword = "xxxxx";      // Passwort für OTA (einfacher Schutz)
    
    // Objekte für Netzwerk, HTTP, Display und Webserver
    WiFiClient client;                     // Client für HTTP-Verbindungen
    HTTPClient http;                       // HTTP-Client-Wrapper
    TFT_eSPI tft = TFT_eSPI();             // TFT-Display-Objekt (Bibliothek TFT_eSPI)
    ESP8266WebServer server(80);           // HTTP-Server lauscht auf Port 80
    
    // ---------- Anzuzeigende Werte (globale Variablen) ----------
    float solarPower = 0;  // Aktuelle Solarleistung (W)
    float gridPower = 0;   // Aktuelle Netzleistung (positiv: Bezug, negativ: Einspeisung) (W)
    float einspeisung = 0; // kWh Ausgehende Energie (Einspeisung)
    float bezug = 0;       // kWh Eingehende Energie (Bezug)
    
    // Timing / Update-Intervall
    unsigned long lastUpdate = 0;                  // letzer Zeitstempel der Aktualisierung (millis)
    const unsigned long updateInterval = 5000;    // Aktualisierungsintervall in ms (5s)
    
    // Hintergrundbeleuchtung / PWM
    const int backlightPin = 3;                    // Pin für Hintergrundbeleuchtung (hier RX/PWM)
    int brightness = 1023;                         // Voller PWM-Wert (für analoge Ausgabe auf ESP8266)
    int brightnessDim = 200;                       // Gedimmter PWM-Wert (kann per Webinterface gesetzt werden)
    
    // Zeitpunkte für Hintergrundbeleuchtung (jeweils hh/mm)
    int stunde_an = 7;    // Zeitpunkt: Hintergrundlicht an (Stunde)
    int minute_an = 0;    // Zeitpunkt: Hintergrundlicht an (Minute)
    int stunde_dimmen = 21; // Ab wann dimmen
    int minute_dimmen = 0;
    int stunde_aus = 23;  // Ab wann ganz aus
    int minute_aus = 0;
    
    // UI/IP Anzeige Steuerung
    bool showIP = true;                           // Flag: zeigt beim Start die IP-Adresse an
    unsigned long ipDisplayStart = 0;             // millis() Zeitpunkt des Starts der IP-Anzeige
    const unsigned long ipDisplayDuration = 5000; // Zeitdauer der IP-Anzeige in ms (5s)
    
    // ------------------ Hilfsfunktionen für Zeitvergleiche ------------------
    
    // Vergleicht zwei Zeiten (h1:m1) >= (h2:m2)
    // Rückgabe: true, wenn (h1:m1) gleich oder nach (h2:m2) liegt.
    bool isTimeAfter(int h1, int m1, int h2, int m2) {
      if (h1 > h2) return true;
      if (h1 == h2 && m1 >= m2) return true;
      return false;
    }
    
    // Vergleicht zwei Zeiten (h1:m1) < (h2:m2)
    // Rückgabe: true, wenn (h1:m1) strikt vor (h2:m2) liegt.
    bool isTimeBefore(int h1, int m1, int h2, int m2) {
      if (h1 < h2) return true;
      if (h1 == h2 && m1 < m2) return true;
      return false;
    }
    
    // DST (Daylight Saving Time) Bestimmung für Mitteleuropa (MEZ/CEST)
    // Die Funktion prüft, ob die gegebene lokale Zeit in der Sommerzeit (DST) liegt.
    // Implementierung orientiert sich an: DST ist von letztem Sonntag im März (02:00) bis
    // letztem Sonntag im Oktober (03:00) gültig. Hier wird aus tm-Struktur (lokale Zeit)
    // entschieden. (Die Berechnung verwendet tm_mon, tm_mday, tm_wday, tm_hour.)
    bool isDST(struct tm* timeinfo) {
      int month = timeinfo->tm_mon + 1; // tm_mon: 0-11, darum +1
      int day = timeinfo->tm_mday;
      int wday = timeinfo->tm_wday;     // 0=Sonntag..6=Samstag
      int hour = timeinfo->tm_hour;
    
      // Monate außerhalb April-Oktober sind sicher keine DST-Monate
      if (month < 3 || month > 10) return false; // Jan, Feb, Nov, Dez -> kein DST
    
      // Apr-Sep sind immer DST
      if (month > 3 && month < 10) return true;  // Apr bis Sep -> DST
    
      // März: DST beginnt am letzten Sonntag im März um 2:00 Uhr
      if (month == 3) {
        // Berechnung des letzten Sonntags im Monat
        int lastSunday = 31 - ((wday + (31 - day)) % 7);
        if ((day > lastSunday) || (day == lastSunday && hour >= 2)) return true;
        else return false;
      }
    
      // Oktober: DST endet am letzten Sonntag im Oktober um 3:00 Uhr
      if (month == 10) {
        int lastSunday = 31 - ((wday + (31 - day)) % 7);
        if ((day < lastSunday) || (day == lastSunday && hour < 3)) return true;
        else return false;
      }
    
      return false; // Fallback
    }
    
    // ------------------ Anzeige-Funktionen (TFT) ------------------
    
    // Zeichnet einen einfachen Rahmen / Layout-Linien auf dem Display
    void drawFrame() {
      tft.drawRect(0, 0, tft.width(), tft.height(), TFT_WHITE);
      tft.drawLine(0, 28, tft.width(), 28, TFT_WHITE);   // obere Trennlinie
      tft.drawLine(0, 160, tft.width(), 160, TFT_WHITE); // mittlere Trennlinie
    }
    
    // Setzt die Hintergrundbeleuchtung je nach eingestellten Zeiten
    // - während der "aus"-Periode wird auf 0 gesetzt (aus)
    // - während der "dimmen"-Periode wird brightnessDim gesetzt
    // - sonst volle Helligkeit (brightness)
    void updateBacklight() {
      time_t now = time(nullptr);
      struct tm* timeinfo = localtime(&now);
      int curr_h = timeinfo->tm_hour;
      int curr_m = timeinfo->tm_min;
    
      // Aus: wenn aktuelle Zeit >= stunde_aus oder < stunde_an
      if (isTimeAfter(curr_h, curr_m, stunde_aus, minute_aus) || isTimeBefore(curr_h, curr_m, stunde_an, minute_an)) {
        analogWrite(backlightPin, 0); // ganz aus
      }
      // Dimmen: wenn aktuelle Zeit >= stunde_dimmen
      else if (isTimeAfter(curr_h, curr_m, stunde_dimmen, minute_dimmen)) {
        analogWrite(backlightPin, brightnessDim); // gedimmt
      }
      // Voll an: sonst
      else {
        analogWrite(backlightPin, brightness); // volle Helligkeit
      }
    }
    
    // Zeigt bei Start die IP-Adresse mittig auf dem Bildschirm an
    void showIPOnDisplay() {
      tft.fillScreen(TFT_BLACK);
      tft.setTextColor(TFT_YELLOW);
      tft.setTextDatum(MC_DATUM); // zentriert
      tft.setTextSize(3);
      tft.drawString("IP-Adresse:", tft.width() / 2, tft.height() / 3);
      tft.drawString(WiFi.localIP().toString(), tft.width() / 2, tft.height() / 2);
    }
    
    // Initiale (leere) Hauptanzeige (wird später bei Updates gefüllt)
    void showMainDisplay() {
      tft.fillScreen(TFT_BLACK);
      drawFrame();
      tft.setTextSize(2);
      tft.setTextColor(TFT_YELLOW);
      tft.setTextDatum(MC_DATUM);
      tft.drawString("Energieanzeige", tft.width() / 2, 14);
      // Die numerischen Werte werden regelmäßig in loop() gezeichnet/aktualisiert
    }
    
    // ------------------ Webserver-Handler ------------------
    
    // Handler für die Root-Seite: baut ein HTML-Dashboard als String zusammen
    // - Verwendet ein Raw-String-Literal (R"rawliteral(... )rawliteral") für das HTML-Template
    // - Fügt dynamische Werte (Hostname, Zeit, Messwerte, Formulareingaben) ein
    void handleRoot() {
      time_t now = time(nullptr);
      struct tm* timeinfo = localtime(&now);
      char timeStr[30];
      strftime(timeStr, sizeof(timeStr), "%d.%m.%Y %H:%M:%S", timeinfo);
    
      // Der HTML-Block ist ein Raw-String-Literal, damit Anführungszeichen im HTML nicht
      // escaped werden müssen. Danach werden dynamische Teile (Hostname, Werte, Formulare)
      // als String-Konkatenationen angehängt.
      String html = R"rawliteral(
    <!DOCTYPE html>
    <html lang="de">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>ESP Energie Dashboard</title>
      <style>
        /* Einfache CSS-Stile für ein dunkel gehaltenes Dashboard */
        body { background-color: #0b1120; color: #e2e8f0; font-family: "Segoe UI", sans-serif; margin: 0; padding: 20px; box-sizing: border-box; }
        h1 { text-align: center; color: #facc15; font-size: 2em; margin-bottom: 5px; }
        .subtitle { text-align: center; font-size: 0.9em; color: #94a3b8; margin-bottom: 20px; }
        .card { background-color: #1e293b; padding: 20px; margin: 20px auto; border-radius: 16px; max-width: 500px; width: 100%; box-shadow: 0 0 10px rgba(255, 255, 255, 0.05); }
        .label { color: #94a3b8; font-size: 0.9em; margin-top: 10px; }
        .value { font-size: 1.3em; margin-bottom: 12px; color: #f1f5f9; }
        form { display: flex; flex-direction: column; }
        form input[type='text'], form input[type='number'] { width: 100%; padding: 8px; margin: 6px 0; border-radius: 4px; border: none; background-color: #334155; color: white; box-sizing: border-box; }
        form input[type='submit'] { background-color: #4ade80; padding: 10px; margin-top: 12px; cursor: pointer; font-weight: bold; border: none; border-radius: 6px; transition: background 0.2s; }
        form input[type='submit']:hover { background-color: #22c55e; }
        @media (max-width: 600px) { h1 { font-size: 1.5em; } .card { margin: 10px; padding: 15px; } .value { font-size: 1.1em; } .label { font-size: 0.8em; } }
      </style>
      <script>
        // JavaScript-Funktion, die /data periodisch abfragt und Felder im HTML aktualisiert
        function updateData() {
          fetch('/data')
            .then(response => response.json())
            .then(data => {
              document.getElementById('zeit').textContent = data.zeit;
              document.getElementById('solar').textContent = data.solar + " W";
              document.getElementById('netz').textContent = data.netz + " W";
              document.getElementById('einspeisung').textContent = data.einspeisung + " kWh";
              document.getElementById('bezug').textContent = data.bezug + " kWh";
            })
            .catch(err => console.error("Fehler beim Datenabruf:", err));
        }
        setInterval(updateData, 5000); // alle 5 Sekunden aktualisieren
        window.onload = updateData;
      </script>
    </head>
    <body>
      <h1>ESP Energie Dashboard</h1>
      <div class="subtitle">Gerätename: )rawliteral";
    
      // Hier wird der Gerätename (OTA-Hostname) eingefügt
      html += otaHostname;
      html += R"rawliteral(</div>
      <div class="card">
        <div class="label">Zeit:</div>
        <div class="value" id="zeit">)rawliteral";
    
      // Aktuelle Zeit (Server-seitig) als initialer Wert
      html += String(timeStr);
      html += R"rawliteral(</div>
        <div class="label">Solar:</div>
        <div class="value" id="solar">)rawliteral";
    
      // Aktuelle Solarleistung als initialer Wert
      html += String(solarPower);
      html += R"rawliteral( W</div>
        <div class="label">Netz:</div>
        <div class="value" id="netz">)rawliteral";
    
      // Aktuelle Netzleistung
      html += String(gridPower);
      html += R"rawliteral( W</div>
        <div class="label">Einspeisung:</div>
        <div class="value" id="einspeisung">)rawliteral";
    
      // Aktuelle Einspeisung (kWh)
      html += String(einspeisung);
      html += R"rawliteral( kWh</div>
        <div class="label">Bezug:</div>
        <div class="value" id="bezug">)rawliteral";
    
      // Aktueller Bezug (kWh)
      html += String(bezug);
      html += R"rawliteral( kWh</div>
      </div>
    
      <div class="card">
        <form method='POST' action='/set'>
          <div class="label">Hintergrundlicht an (hh:mm):</div>
      )rawliteral";
    
      // Formularfelder: Werte werden aus den gespeicherten Variablen eingefügt
      html += "<input name='an_h' value=\"" + String(stunde_an) + "\" type='number' min='0' max='23'> : ";
      html += "<input name='an_m' value=\"" + String(minute_an) + "\" type='number' min='0' max='59'><br>";
      html += "<div class='label'>Dimmen ab (hh:mm):</div>";
      html += "<input name='dimmen_h' value=\"" + String(stunde_dimmen) + "\" type='number' min='0' max='23'> : ";
      html += "<input name='dimmen_m' value=\"" + String(minute_dimmen) + "\" type='number' min='0' max='59'><br>";
      html += "<div class='label'>Aus ab (hh:mm):</div>";
      html += "<input name='aus_h' value=\"" + String(stunde_aus) + "\" type='number' min='0' max='23'> : ";
      html += "<input name='aus_m' value=\"" + String(minute_aus) + "\" type='number' min='0' max='59'><br>";
      html += "<div class='label'>Dimmwert (0–1023):</div>";
      html += "<input name='dimValue' value=\"" + String(brightnessDim) + "\" type='number' min='0' max='1023'><br>";
      html += "<input type='submit' value='Speichern'>";
      html += "</form></div></body></html>";
    
      // Antwort an den Browser
      server.send(200, "text/html", html);
    }
    
    // Handler für das Formular /set (POST)
    // Liest die Formularfelder, validiert sie grob und schreibt sie ins EEPROM
    void handleSet() {
      if (server.method() == HTTP_POST) {
        // Formulardaten einlesen
        int an_h = server.arg("an_h").toInt();
        int an_m = server.arg("an_m").toInt();
        int dimmen_h = server.arg("dimmen_h").toInt();
        int dimmen_m = server.arg("dimmen_m").toInt();
        int aus_h = server.arg("aus_h").toInt();
        int aus_m = server.arg("aus_m").toInt();
        int dimValue = server.arg("dimValue").toInt();
    
        // Einfache Validierungschecks (nur Bereichsprüfung)
        if (an_h >= 0 && an_h < 24) stunde_an = an_h;
        if (an_m >= 0 && an_m < 60) minute_an = an_m;
        if (dimmen_h >= 0 && dimmen_h < 24) stunde_dimmen = dimmen_h;
        if (dimmen_m >= 0 && dimmen_m < 60) minute_dimmen = dimmen_m;
        if (aus_h >= 0 && aus_h < 24) stunde_aus = aus_h;
        if (aus_m >= 0 && aus_m < 60) minute_aus = aus_m;
        if (dimValue >= 0 && dimValue <= 1023) brightnessDim = dimValue;
    
        // Persistente Speicherung: einzelne Bytes ins EEPROM schreiben
        EEPROM.write(0, stunde_an);
        EEPROM.write(1, minute_an);
        EEPROM.write(2, stunde_dimmen);
        EEPROM.write(3, minute_dimmen);
        EEPROM.write(4, stunde_aus);
        EEPROM.write(5, minute_aus);
        EEPROM.write(6, brightnessDim & 0xFF);          // LSB des 16-bit Werts
        EEPROM.write(7, (brightnessDim >> 8) & 0xFF);   // MSB
        EEPROM.commit(); // Wichtig: Änderungen wirklich speichern
    
        // Bestätigungsseite zurück an den Browser
        String html = "<html><body style='font-family:sans-serif; background:#0b1120; color:#e2e8f0; padding:20px;'>";
        html += "<h2>✅ Einstellungen gespeichert</h2>";
        html += "<p><a href=\"/\">Zurück zum Dashboard</a></p>";
        html += "</body></html>";
        server.send(200, "text/html", html);
      } else {
        // Nur POST ist erlaubt
        server.send(405, "text/plain", "Method Not Allowed");
      }
    }
    
    // ------------------ Setup-Funktion ------------------
    void setup() {
      Serial.begin(115200); // Serielle Ausgabe für Debug
    
      // PWM / Backlight Pin vorbereiten
      pinMode(backlightPin, OUTPUT);
      analogWriteFreq(500); // PWM-Frequenz setzen (ESP8266 spezifisch)
    
      // Mit WLAN verbinden
      WiFi.begin(ssid, password);
      while (WiFi.status() != WL_CONNECTED) delay(500); // Blockiert bis Verbindung steht
    
      // Zeitkonfiguration: Zeitzone und NTP-Server
      // configTzTime nutzt die POSIX-Zeitzonenbeschreibung. Hier CET/CEST wird gesetzt.
      configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "pool.ntp.org", "time.nist.gov");
    
      // OTA initialisieren
      ArduinoOTA.setHostname(otaHostname.c_str());
      ArduinoOTA.setPassword(otaPassword.c_str());
      ArduinoOTA.begin();
    
      // TFT initialisieren
      tft.init();
      tft.invertDisplay(1); // Manche Displays benötigen invertiert beim ersten Einschalten
      tft.setRotation(3);   // Ausrichtung des Displays
    
      // IP-Anzeige beim Start initialisieren
      showIP = true;
      ipDisplayStart = millis();
    
      // mDNS: Gerät im lokalen Netzwerk unter "esp.local" verfügbar machen (falls mdns funktioniert)
      if (MDNS.begin("esp")) MDNS.addService("http", "tcp", 80);
    
      // Webserver Routen registrieren
      server.on("/", handleRoot);
      server.on("/set", HTTP_POST, handleSet);
      server.on("/data", handleData);
      server.begin();
    
      // Background initial: Hintergrundbeleuchtung auf vollen Wert setzen
      pinMode(backlightPin, OUTPUT);
      analogWrite(backlightPin, brightness);
    
      // EEPROM initialisieren und gespeicherte Einstellungen laden
      EEPROM.begin(EEPROM_SIZE);
      stunde_an = EEPROM.read(0);
      minute_an = EEPROM.read(1);
      stunde_dimmen = EEPROM.read(2);
      minute_dimmen = EEPROM.read(3);
      stunde_aus = EEPROM.read(4);
      minute_aus = EEPROM.read(5);
      brightnessDim = EEPROM.read(6) + (EEPROM.read(7) << 8); // 16-bit Wert aus zwei Bytes zusammensetzen
    
      // Gültigkeit der geladenen Werte prüfen, ggf. Standardwerte setzen
      bool invalid = false;
      if (stunde_an > 23) { stunde_an = 7; invalid = true; }
      if (minute_an > 59) { minute_an = 0; invalid = true; }
      if (stunde_dimmen > 23) { stunde_dimmen = 21; invalid = true; }
      if (minute_dimmen > 59) { minute_dimmen = 0; invalid = true; }
      if (stunde_aus > 23) { stunde_aus = 23; invalid = true; }
      if (minute_aus > 59) { minute_aus = 0; invalid = true; }
      if (brightnessDim > 1023) { brightnessDim = 200; invalid = true; }
    
      // Wenn ungültige Werte gefunden wurden, die korrigierten Standardwerte zurückschreiben
      if (invalid) {
        EEPROM.write(0, stunde_an);
        EEPROM.write(1, minute_an);
        EEPROM.write(2, stunde_dimmen);
        EEPROM.write(3, minute_dimmen);
        EEPROM.write(4, stunde_aus);
        EEPROM.write(5, minute_aus);
        EEPROM.write(6, brightnessDim & 0xFF);
        EEPROM.write(7, (brightnessDim >> 8) & 0xFF);
        EEPROM.commit();
      }
    }
    
    // ------------------ /data Handler: liefert JSON mit den aktuellen Werten ------------------
    void handleData() {
      time_t now = time(nullptr);
      struct tm* timeinfo = localtime(&now);
      char timeStr[30];
      strftime(timeStr, sizeof(timeStr), "%d.%m.%Y %H:%M:%S", timeinfo);
    
      // Einfacher JSON-String (manuell zusammengesetzt) mit den aktuellen Werten
      String json = "{";
      json += "\"zeit\":\"" + String(timeStr) + "\",";
      json += "\"solar\":" + String(solarPower) + ",";
      json += "\"netz\":" + String(gridPower) + ",";
      json += "\"einspeisung\":" + String(einspeisung) + ",";
      json += "\"bezug\":" + String(bezug);
      json += "}";
    
      server.send(200, "application/json", json);
    }
    
    // ------------------ Hauptschleife ------------------
    void loop() {
      ArduinoOTA.handle();   // OTA-Ereignisse verarbeiten (falls ein Update hereinkommt)
      server.handleClient(); // HTTP-Server-Anfragen verarbeiten
    
      // Beim Start die IP ein paar Sekunden zeigen
      if (showIP) {
        showIPOnDisplay();
        delay(5000); // 5 Sekunden warten während die IP angezeigt wird (blockierend)
        if (millis() - ipDisplayStart > ipDisplayDuration) {
          showIP = false;
          showMainDisplay();
        }
      } else {
        // Hintergrundbeleuchtung anpassen (je nach aktuellem Zeitpunkt)
        updateBacklight();
    
        unsigned long now = millis();
        if (now - lastUpdate > updateInterval) {
          lastUpdate = now;
    
          // Nur wenn WLAN verbunden ist, Daten abfragen
          if (WiFi.status() == WL_CONNECTED) {
            // --- 1) Anfrage an opendtu_url ---
            http.begin(client, opendtu_url);
            http.setAuthorization(opendtu_user, opendtu_password); // Basic Auth
            int httpCode = http.GET();
            if (httpCode == 200) {
              // JSON-Antwort parsen
              StaticJsonDocument<2048> doc;
              DeserializationError err = deserializeJson(doc, http.getString());
              if (!err)
                solarPower = doc["total"]["Power"]["v"] | 0.0; // Pfad gemäß API (falls vorhanden)
            }
            http.end(); // Verbindung schließen
    
            // --- 2) Anfrage an hichi_url (z.B. Tasmota Status) ---
            http.begin(client, hichi_url);
            httpCode = http.GET();
            if (httpCode == 200) {
              StaticJsonDocument<1024> doc;
              DeserializationError err = deserializeJson(doc, http.getString());
              if (!err) {
                // Werte (Pfad entsprechend der Tasmota-Status-Antwort)
                gridPower = doc["StatusSNS"]["Zaehler"]["Power"] | 0.0;
                einspeisung = doc["StatusSNS"]["Zaehler"]["E_out"] | 0.0;
                bezug = doc["StatusSNS"]["Zaehler"]["E_in"] | 0.0;
              }
            }
            http.end();
    
            // ------------------ TFT-Display aktualisieren ------------------
            tft.fillRect(0, 30, tft.width(), 130, TFT_BLACK); // Bereich löschen
    
            // Solarleistung: große Schrift
            tft.setTextSize(3);
            tft.setTextColor(TFT_GREEN);
            tft.setTextDatum(MC_DATUM);
            char solarText[30];
            snprintf(solarText, sizeof(solarText), "Solar: %.0f W", solarPower);
            tft.drawString(solarText, tft.width() / 2, 50);
    
            // Einspeisung / Ausgang (kWh)
            tft.setTextSize(2);
            tft.setTextColor(TFT_LIGHTGREY);
            char einspeisungText[30];
            snprintf(einspeisungText, sizeof(einspeisungText), "Ausgang: %.2f kWh", einspeisung);
            tft.drawString(einspeisungText, tft.width() / 2, 80);
    
            // Netzleistung: Farbe abhängig vom Vorzeichen
            tft.setTextSize(3);
            uint16_t netzfarbe = (gridPower >= 0) ? TFT_RED : TFT_GREEN; // positiv=rot (Bezug), negativ=grün (Einspeisung)
            tft.setTextColor(netzfarbe);
            char gridText[30];
            snprintf(gridText, sizeof(gridText), "Netz: %.0f W", gridPower);
            tft.drawString(gridText, tft.width() / 2, 110);
    
            // Bezug (kWh)
            tft.setTextSize(2);
            tft.setTextColor(TFT_LIGHTGREY);
            char bezugText[30];
            snprintf(bezugText, sizeof(bezugText), "Eingang: %.2f kWh", bezug);
            tft.drawString(bezugText, tft.width() / 2, 140);
    
            // Unterer Balken: färbt die Fläche nach Netzstatus und schreibt Text
            uint16_t bottomColor = gridPower >= 0 ? TFT_RED : TFT_GREEN;
            tft.fillRect(0, 161, tft.width(), tft.height() - 161, bottomColor);
            tft.setTextSize(3);
            tft.setTextColor(TFT_BLACK);
            const char* statusText = gridPower >= 0 ? "Netzbezug" : "Einspeisung";
            int centerY = 161 + (tft.height() - 161) / 2;
            tft.drawString(statusText, tft.width() / 2, centerY);
          }
        }
      }
    }
    

    Technische Dokumentation – ESP8266 Energieanzeige

    1. Einleitung
      Das Projekt implementiert ein Energie-Monitoring-System auf einem ESP8266-Mikrocontroller.
      Es verbindet sich mit dem WLAN, ruft Energiedaten von einer OpenDTU und einem Tasmota-basierten Stromzähler ab, zeigt die Werte auf einem TFT-Display an und stellt zusätzlich ein Web-Dashboard zur Verfügung.
      Die Benutzer können Einstellungen für die Display-Hintergrundbeleuchtung über eine Weboberfläche vornehmen und im EEPROM speichern.
      Zusätzlich unterstützt das System Over-the-Air Updates (OTA) und ist per mDNS im Netzwerk auffindbar.

    1. Systemübersicht
      Hardware
      • ESP8266 (z. B. NodeMCU / Wemos D1 mini)
      • TFT-Display (angesteuert über TFT_eSPI-Library)
      • Stromzähler (HiChi/Tasmota) – liefert Netzwerte per HTTP
      • OpenDTU – liefert Solarleistungswerte per HTTP
      • Hintergrundbeleuchtung des TFT über PWM (Pin D3/RX)
      Software
      • Arduino Core for ESP8266
      • Wichtige Bibliotheken:
      o ESP8266WiFi (WLAN-Verbindung)
      o ESP8266WebServer (Webserver)
      o ArduinoJson (Parsing von API-Daten)
      o ArduinoOTA (Over-the-Air Updates)
      o TFT_eSPI (Displaysteuerung)
      o EEPROM (Speichern von Benutzer-Einstellungen)
      o time.h (NTP-Synchronisation, Sommerzeit)

    1. Programmablauf
      Setup (setup())
    2. WLAN-Verbindung mit gespeicherten SSID/Passwort.
    3. Zeitkonfiguration via NTP mit Sommerzeitregelung.
    4. OTA-Konfiguration (Hostname, Passwort, Start).
    5. Display-Initialisierung (Rotation, invertieren, Startanzeige).
    6. mDNS starten (esp.local).
    7. Webserver einrichten:
      o / → Dashboard
      o /data → JSON-Daten
      o /set → Einstellungsformular
    8. EEPROM laden und Werte prüfen/korrigieren.

    Loop (loop())
    • OTA-Requests bearbeiten (ArduinoOTA.handle()).
    • Webserver-Anfragen bedienen (server.handleClient()).
    • Display-Anzeige:
    o Start: IP-Adresse 5 Sekunden lang.
    o Danach: Hauptanzeige mit Solar, Netz, Einspeisung, Bezug.
    • Hintergrundbeleuchtung nach Uhrzeit an/aus/dimmen.
    • Alle 5 Sekunden:
    o Daten von OpenDTU (Solarleistung) und HiChi-Zähler (Netz, Einspeisung, Bezug) abrufen.
    o Display mit neuen Werten aktualisieren.


    1. Webinterface
      Dashboard (/)
      • Anzeige von:
      o Solarleistung (Watt)
      o Netzleistung (Watt)
      o Einspeisung (kWh)
      o Bezug (kWh)
      o Aktuelle Uhrzeit
      • Automatische Aktualisierung alle 5 Sekunden per AJAX.
      Einstellungen (/set)
      • Einstellen von:
      o An-Zeit (hh:mm)
      o Dimm-Zeit (hh:mm)
      o Aus-Zeit (hh:mm)
      o Dimmwert (PWM, 0–1023)
      • Werte werden im EEPROM gespeichert und beim Neustart geladen.
      Daten-Endpunkt (/data)
      • JSON-Ausgabe mit aktuellen Messwerten, z. B.:
      {
      "zeit": "10.09.2025 12:34:56",
      "solar": 450.0,
      "netz": -120.0,
      "einspeisung": 5.32,
      "bezug": 2.45
      }

    2. EEPROM-Nutzung
      Die Benutzer-Einstellungen werden im EEPROM gespeichert:
      • Adresse 0 → Stunde An
      • Adresse 1 → Minute An
      • Adresse 2 → Stunde Dimmen
      • Adresse 3 → Minute Dimmen
      • Adresse 4 → Stunde Aus
      • Adresse 5 → Minute Aus
      • Adresse 6 → Dimmwert LSB
      • Adresse 7 → Dimmwert MSB
      Damit bleiben die Werte auch nach einem Neustart erhalten.


    1. Besondere Funktionen
      • Sommerzeitberechnung: Automatische Umstellung gemäß Mitteleuropäischer Zeit (MEZ/MEZ+1).
      • Farbanzeige im Display:
      o Rot: Netzbezug
      o Grün: Einspeisung
      • OTA-Update: Programm kann per WLAN aktualisiert werden, ohne USB.
      • mDNS: Zugriff über http://esp.local/.

    1. Einsatzszenario
      Dieses System eignet sich besonders für Heimanwendungen, bei denen ein Balkonkraftwerk (Solar-Inverter) und ein Haushaltszähler überwacht werden sollen.
      Es bietet:
      • Schnelle Übersicht am Display (lokal, ohne PC/Smartphone).
      • Browser-Dashboard für Remote-Zugriff im Netzwerk.
      • Benutzerfreundliche Steuerung der Anzeige (automatische Dimmung).

    1. Grenzen und Anforderungen
      • Läuft ausschließlich auf ESP8266-Boards.
      • Erfordert ein funktionierendes WLAN.
      • Abhängigkeit von OpenDTU- und Tasmota-Geräten für Messdaten.
      • EEPROM hat begrenzte Schreibzyklen → sollte nicht zu oft überschrieben werden.

    Als Gehäuse habe ich dieses genommen:

    https://www.thingiverse.com/thing:4570437

    Display (2,4" TFT LCD Display Modul ILI9341 240x320) ist solch eines:

    https://www.roboter-bausatz.de/p/2-4-tft-lcd-display-modul-ili9341-240x320
    Es scheint, das es zwei verschiedene Arten von diesem Display gibt. Im Skript ist dafür eine Einstellung vorgesehen um die Farben zu invertieren.

    und natürlich ein Wemos D1 mini.

    Für das Display gibt es auch eine direkt Adapterplatine um den Wemos D1 mini ohne Kabelgefummel anzuschliessen.

    sm_red_top.png

    KiCAD-master.zip

    Hier mal ein Bild:

    innen.jpg
    anzeige.jpg
    einspeisung.jpg

    Bei mir hat es sich gezeigt, das der "WAF" sehr hoch ist. :blush:

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

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

      Neue Version mit DPL Steuerung inkl. Rückmeldung.

      Auch bitte hier lesen.
      https://forum.iobroker.net/topic/80855/opendtu-onbattery-blockly-dpl-ein-aus

      #include <ESP8266WiFi.h>
      #include <ESP8266HTTPClient.h>
      #include <ESP8266WebServer.h>
      #include <ESP8266mDNS.h>
      #include <TFT_eSPI.h>
      #include <ArduinoOTA.h>
      #include <ArduinoJson.h>
      #include <time.h>
      #include <EEPROM.h>
      #include <PubSubClient.h>
      #include <XPT2046_Touchscreen.h>
      
      #define EEPROM_SIZE 64
      
      // WLAN
      const char* ssidList[] = { "msg2", "msg2og", "msg2dg" };
      const char* passwordList[] = { "111111", "222222", "33333" };
      const int numNetworks = sizeof(ssidList) / sizeof(ssidList[0]);
      
      // OTA
      String otaHostname = "ESP-Solar-DPL";   // bleibt gleich auf beiden Geräten
      String clientId = otaHostname + "-" + String(ESP.getChipId(), HEX);
      String otaPassword = "503588";
      
      // MQTT
      const char* mqttServer = "192.168.11.1x1";
      const int mqttPort = 1886;
      const char* mqttUser = "xxxxx";
      const char* mqttPassword = "xxxxxxx";
      
      // Getrennte Topics für Befehl & Rückmeldung
      const char* mqttTopicCmd = "mqtt.0.solar.dpl.dplCmd";      // wird beim Touch gesendet
      const char* mqttTopicState = "mqtt.0.solar.dpl.dplState";  // kommt als Rückmeldung
      
      WiFiClient espClient;
      PubSubClient mqttClient(espClient);
      
      // HTTP
      WiFiClient client;
      HTTPClient http;
      const char* opendtu_url = "http://192.168.xx.xxxx/api/livedata/status";
      const char* opendtu_user = "ddddd";
      const char* opendtu_password = "xxxx";
      const char* hichi_url = "http://192.168.11.111/cm?cmnd=status%208";
      
      // TFT Display
      TFT_eSPI tft = TFT_eSPI();
      XPT2046_Touchscreen ts(D3); // CS-Pin D3
      
      // Backlight
      const int backlightPin = 3;
      int brightness = 1023;
      int brightnessDim = 200;
      
      // Zeit für Backlight
      int stunde_an = 7;
      int minute_an = 0;
      int stunde_dimmen = 21;
      int minute_dimmen = 0;
      int stunde_aus = 23;
      int minute_aus = 0;
      
      // Anzeige IP
      bool showIP = true;
      unsigned long ipDisplayStart = 0;
      const unsigned long ipDisplayDuration = 5000;
      
      // Webserver
      ESP8266WebServer server(80);
      
      // Daten
      float solarPower = 0;
      float gridPower = 0;
      float einspeisung = 0;
      float bezug = 0;
      unsigned long lastUpdate = 0;
      const unsigned long updateInterval = 5000;
      
      // DPL Status
      bool dplStatus = false;
      bool dplWaitingForAck = false;   // zeigt an, dass auf Rückmeldung gewartet wird
      
      // *** Hilfsfunktionen ***
      void drawFrame() {
        tft.drawRect(0, 0, tft.width(), tft.height(), TFT_WHITE);
        tft.drawLine(0, 28, tft.width(), 28, TFT_WHITE);
        tft.drawLine(0, 160, tft.width(), 160, TFT_WHITE);
      }
      
      void updateBacklight() {
        time_t now = time(nullptr);
        struct tm* timeinfo = localtime(&now);
        int curr_h = timeinfo->tm_hour;
        int curr_m = timeinfo->tm_min;
      
        if ((curr_h > stunde_aus || (curr_h == stunde_aus && curr_m >= minute_aus)) ||
            (curr_h < stunde_an || (curr_h == stunde_an && curr_m < minute_an))) {
          analogWrite(backlightPin, 0);
        } else if (curr_h > stunde_dimmen || (curr_h == stunde_dimmen && curr_m >= minute_dimmen)) {
          analogWrite(backlightPin, brightnessDim);
        } else {
          analogWrite(backlightPin, brightness);
        }
      }
      
      void showIPOnDisplay() {
        tft.fillScreen(TFT_BLACK);
        tft.setTextColor(TFT_YELLOW);
        tft.setTextDatum(MC_DATUM);
        tft.setTextSize(3);
        tft.drawString("IP-Adresse:", tft.width() / 2, tft.height() / 3);
        tft.drawString(WiFi.localIP().toString(), tft.width() / 2, tft.height() / 2);
      }
      
      void showMainDisplay() {
        static float lastSolar = -9999;
        static float lastEinspeisung = -9999;
        static float lastGrid = -9999;
        static float lastBezug = -9999;
        static bool lastDplStatus = false;
      
        // beim ersten Aufruf Rahmen und Überschrift zeichnen
        static bool firstRun = true;
        if (firstRun) {
          tft.fillScreen(TFT_BLACK);
          drawFrame();
      
          // Überschrift
          tft.setTextSize(2);
          tft.setTextColor(TFT_YELLOW, TFT_BLACK);
          tft.setTextDatum(MC_DATUM);
          tft.setTextPadding(200);   // Bereich freiräumen
          tft.drawString("Energieanzeige", tft.width() / 2, 14);
          firstRun = false;
        }
      
      // DPL Anzeige rechts oben
      tft.setTextSize(2);
      tft.setTextDatum(TR_DATUM);
      tft.setTextPadding(60);  // etwas kleinerer Bereich reicht
      
      if (dplWaitingForAck) {
        // Warten auf Rückmeldung → Gelb
        tft.setTextColor(TFT_YELLOW, TFT_BLACK);
        tft.drawString("DPL", tft.width() - 5, 7);
      }
      else if (dplStatus) {
        // Aktiv → Blau
        tft.setTextColor(TFT_BLUE, TFT_BLACK);
        tft.drawString("DPL", tft.width() - 5, 7);
      }
      else {
        // Inaktiv → Grau
        tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
        tft.drawString("DPL", tft.width() - 5, 7);
      }
      
        // Solar
        if (abs(solarPower - lastSolar) > 0.5) {
          tft.setTextSize(3);
          tft.setTextColor(TFT_GREEN, TFT_BLACK);
          tft.setTextDatum(MC_DATUM);
          tft.setTextPadding(310);
          char solarText[30];
          snprintf(solarText, sizeof(solarText), "Solar: %.0f W", solarPower);
          tft.drawString(solarText, tft.width() / 2, 50);
          lastSolar = solarPower;
        }
      
        // Einspeisung
        if (abs(einspeisung - lastEinspeisung) > 0.01) {
          tft.setTextSize(2);
          tft.setTextColor(TFT_LIGHTGREY, TFT_BLACK);
          tft.setTextDatum(MC_DATUM);
          tft.setTextPadding(310);
          char einspeisungText[30];
          snprintf(einspeisungText, sizeof(einspeisungText), "Ausgang: %.2f kWh", einspeisung);
          tft.drawString(einspeisungText, tft.width() / 2, 80);
          lastEinspeisung = einspeisung;
        }
      
        // Netz
        if (abs(gridPower - lastGrid) > 0.5) {
          tft.setTextSize(3);
          uint16_t netzfarbe = (gridPower >= 0) ? TFT_RED : TFT_GREEN;
          tft.setTextColor(netzfarbe, TFT_BLACK);
          tft.setTextDatum(MC_DATUM);
          tft.setTextPadding(310);
          char gridText[30];
          snprintf(gridText, sizeof(gridText), "Netz: %.0f W", gridPower);
          tft.drawString(gridText, tft.width() / 2, 110);
          lastGrid = gridPower;
        }
      
        // Bezug
        if (abs(bezug - lastBezug) > 0.01) {
          tft.setTextSize(2);
          tft.setTextColor(TFT_LIGHTGREY, TFT_BLACK);
          tft.setTextDatum(MC_DATUM);
          tft.setTextPadding(310);
          char bezugText[30];
          snprintf(bezugText, sizeof(bezugText), "Eingang: %.2f kWh", bezug);
          tft.drawString(bezugText, tft.width() / 2, 140);
          lastBezug = bezug;
        }
      
        // Statusbereich unten (immer neu, weil Fläche sich ändern kann)
        uint16_t bottomColor = gridPower >= 0 ? TFT_RED : TFT_GREEN;
        tft.fillRect(0, 161, tft.width(), tft.height() - 161, bottomColor);
        tft.setTextSize(3);
        tft.setTextColor(TFT_BLACK, bottomColor);
        const char* statusText = gridPower >= 0 ? "Netzbezug" : "Einspeisung";
        int centerY = 161 + (tft.height() - 161) / 2;
        tft.setTextDatum(MC_DATUM);
        tft.drawString(statusText, tft.width() / 2, centerY);
      }
      
      void mqttCallback(char* topic, byte* payload, unsigned int length) {
        String msg;
        for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
        msg.trim();
        msg.toLowerCase();
      
        Serial.printf("MQTT Nachricht empfangen auf Topic: %s -> %s\n", topic, msg.c_str());
      
        bool newStatus = (msg == "1" || msg == "true" || msg == "on");
      
        if (String(topic) == mqttTopicState) {
          dplWaitingForAck = false;  // Rückmeldung erhalten → Wartezustand beenden
      
          if (newStatus != dplStatus) {
            dplStatus = newStatus;
            Serial.printf("DPL Status geändert auf: %s\n", dplStatus ? "EIN" : "AUS");
          }
      
          // Anzeige immer aktualisieren
          showMainDisplay();
        }
      }
      
      void reconnectMQTT() {
        while (!mqttClient.connected()) {
          String clientId = otaHostname + "-" + String(ESP.getChipId(), HEX);
      
          if (mqttClient.connect(clientId.c_str(), mqttUser, mqttPassword)) {
            // Beide Topics abonnieren
            mqttClient.subscribe(mqttTopicCmd);
            mqttClient.subscribe(mqttTopicState);
            Serial.println("Mit MQTT verbunden als " + clientId);
          } else {
            Serial.print("MQTT Verbindungsfehler, rc=");
            Serial.print(mqttClient.state());
            Serial.println(" -> versuche erneut in 2s");
            delay(2000);
          }
        }
      }
      
      // Webserver Daten
      void handleData() {
        time_t now = time(nullptr);
        struct tm* timeinfo = localtime(&now);
        char timeStr[30];
        strftime(timeStr, sizeof(timeStr), "%d.%m.%Y %H:%M:%S", timeinfo);
      
        String json = "{";
        json += "\"zeit\":\"" + String(timeStr) + "\",";
        json += "\"solar\":" + String(solarPower) + ",";
        json += "\"netz\":" + String(gridPower) + ",";
        json += "\"einspeisung\":" + String(einspeisung) + ",";
        json += "\"bezug\":" + String(bezug);
        json += "}";
        server.send(200, "application/json", json);
      }
      
      // ----------------- Webserver Routen -----------------
      
      void handleSet() {
        if (server.hasArg("an_h")) stunde_an = server.arg("an_h").toInt();
        if (server.hasArg("an_m")) minute_an = server.arg("an_m").toInt();
        if (server.hasArg("dimmen_h")) stunde_dimmen = server.arg("dimmen_h").toInt();
        if (server.hasArg("dimmen_m")) minute_dimmen = server.arg("dimmen_m").toInt();
        if (server.hasArg("aus_h")) stunde_aus = server.arg("aus_h").toInt();
        if (server.hasArg("aus_m")) minute_aus = server.arg("aus_m").toInt();
        if (server.hasArg("dimValue")) brightnessDim = server.arg("dimValue").toInt();
        server.sendHeader("Location", "/");
        server.send(303);
      }
      
      void handleRoot() {
        time_t now = time(nullptr);
        struct tm* timeinfo = localtime(&now);
        char timeStr[30];
        strftime(timeStr, sizeof(timeStr), "%d.%m.%Y %H:%M:%S", timeinfo);
      
        String html = R"rawliteral(
      <!DOCTYPE html>
      <html lang="de">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>ESP Solar Dashboard</title>
        <style>
          body { background-color:#0b1120; color:#e2e8f0; font-family:"Segoe UI",sans-serif; margin:0; padding:20px;}
          h1 { text-align:center; color:#facc15; font-size:2em; margin-bottom:5px;}
          .subtitle { text-align:center; font-size:0.9em; color:#94a3b8; margin-bottom:20px;}
          .card { background:#1e293b; padding:20px; margin:20px auto; border-radius:16px; max-width:300px; box-shadow:0 0 10px rgba(255,255,255,0.05);}
          .label { color:#94a3b8; font-size:0.9em; margin-top:10px;}
          .value { font-size:1.3em; margin-bottom:12px; color:#f1f5f9;}
          .time-row { display:flex; gap:6px; margin-bottom:12px;}
          .time-row input[type='number']{ width:50px; padding:6px 4px; text-align:center;}
          form input[type='number']{ width:100%; padding:8px; margin:6px 0; border:none; border-radius:4px; background:#334155; color:white;}
          form input[type='submit']{ background:#4ade80; padding:10px; margin-top:12px; cursor:pointer; font-weight:bold; border:none; border-radius:6px;}
          form input[type='submit']:hover{ background:#22c55e;}
        </style>
        <script>
          function updateData(){
            fetch('/data').then(r=>r.json()).then(d=>{
              document.getElementById('zeit').textContent=d.zeit;
              document.getElementById('solar').textContent=d.solar+" W";
              document.getElementById('netz').textContent=d.netz+" W";
              document.getElementById('einspeisung').textContent=d.einspeisung+" kWh";
              document.getElementById('bezug').textContent=d.bezug+" kWh";
            });
          }
          setInterval(updateData,10000);
          window.onload=updateData;
        </script>
      </head>
      <body>
        <h1>ESP Solar Dashboard</h1>
        <div class="subtitle">Gerätename: )rawliteral";
        html += clientId;
        html += R"rawliteral(</div>
      
        <div class="card">
          <div class="label">Zeit:</div>
          <div class="value" id="zeit">)rawliteral" + String(timeStr) + R"rawliteral(</div>
          <div class="label">Solarleistung:</div>
          <div class="value" id="solar">--</div>
          <div class="label">Netzleistung:</div>
          <div class="value" id="netz">--</div>
          <div class="label">Einspeisung:</div>
          <div class="value" id="einspeisung">--</div>
          <div class="label">Bezug:</div>
          <div class="value" id="bezug">--</div>
        </div>
      
        <div class="card">
          <form method="POST" action="/set">
            <div class="label">Hintergrundlicht an (hh:mm):</div>
            <div class="time-row">
              <input name='an_h' value=')rawliteral" + String(stunde_an) + R"rawliteral(' type='number' min='0' max='23'><span>:</span>
              <input name='an_m' value=')rawliteral" + String(minute_an) + R"rawliteral(' type='number' min='0' max='59'>
            </div>
            <div class="label">Dimmen ab (hh:mm):</div>
            <div class="time-row">
              <input name='dimmen_h' value=')rawliteral" + String(stunde_dimmen) + R"rawliteral(' type='number' min='0' max='23'><span>:</span>
              <input name='dimmen_m' value=')rawliteral" + String(minute_dimmen) + R"rawliteral(' type='number' min='0' max='59'>
            </div>
            <div class="label">Aus ab (hh:mm):</div>
            <div class="time-row">
              <input name='aus_h' value=')rawliteral" + String(stunde_aus) + R"rawliteral(' type='number' min='0' max='23'><span>:</span>
              <input name='aus_m' value=')rawliteral" + String(minute_aus) + R"rawliteral(' type='number' min='0' max='59'>
            </div>
            <div class="label">Dimmwert (0–1023):</div>
            <input name='dimValue' value=')rawliteral" + String(brightnessDim) + R"rawliteral(' type='number' min='0' max='1023'>
            <input type='submit' value='Speichern'>
          </form>
        </div>
      </body>
      </html>
      )rawliteral";
      
        server.send(200,"text/html",html);
      }
      
      void setup() {
        Serial.begin(115200);
        pinMode(backlightPin, OUTPUT);
        analogWriteFreq(500);
        bool connected = false;
        showIP = true;
        ipDisplayStart = millis();
      
        // WLAN
      for (int i = 0; i < numNetworks; i++) {
          WiFi.begin(ssidList[i], passwordList[i]);
          int attempts = 0;
          while (WiFi.status() != WL_CONNECTED && attempts < 20) {
            delay(500);
            attempts++;
          }
          if (WiFi.status() == WL_CONNECTED) {
            connected = true;
            break;
          }
        }
      
        // Zeit
        configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "pool.ntp.org", "time.nist.gov");
      
        // OTA
        ArduinoOTA.setHostname(otaHostname.c_str());
        ArduinoOTA.setPassword(otaPassword.c_str());
        ArduinoOTA.begin();
      
        // Display
        tft.init();
        tft.invertDisplay(1);
        tft.setRotation(3);
      
        // IP-Anzeige
        showIP = true;
        ipDisplayStart = millis();
      
      // Webserver
      if(MDNS.begin("esp")) MDNS.addService("http","tcp",80);
      server.on("/", handleRoot);
      server.on("/set", HTTP_POST, handleSet);
      server.on("/data", handleData);
      server.begin();
      
        // MQTT
        mqttClient.setServer(mqttServer, mqttPort);
        mqttClient.setCallback(mqttCallback);
        reconnectMQTT();
      
        // Touchscreen initialisieren
        ts.begin();
        ts.setRotation(1);
      }
      
      void loop() {
        ArduinoOTA.handle();
        server.handleClient();
      
        if(!mqttClient.connected()) reconnectMQTT();
        mqttClient.loop();
      
      // ----------------- IP-Anzeige im Loop -----------------
      
        updateBacklight();
        
      if (showIP) {
          static unsigned long lastIPUpdate = 0;
      
          // alle 1 Sekunde aktualisieren
          if (millis() - lastIPUpdate > 1000) {
              lastIPUpdate = millis();
      
              // nur anzeigen, wenn WLAN verbunden
              if (WiFi.status() == WL_CONNECTED) {
                  showIPOnDisplay();
              }
          }
      
          // Dauer prüfen, nach Ablauf die Hauptanzeige zeigen
          if (millis() - ipDisplayStart > ipDisplayDuration) {
              showIP = false;
              showMainDisplay();
          }
        } else {
          updateBacklight();
      
      if (ts.touched()) {
        TS_Point p = ts.getPoint();
        (void)p; // gesamte Fläche reagiert
      
        // Nur Befehl senden – Anzeige ändert sich erst bei Rückmeldung
        bool newCmd = !dplStatus; 
        mqttClient.publish(mqttTopicCmd, newCmd ? "1" : "0", true);
      
        // Wartezustand aktivieren
        dplWaitingForAck = true;
        showMainDisplay();  // damit „Warten...“ sichtbar wird
      
        Serial.printf("Touch erkannt -> Sende DPL-CMD: %s (warte auf Bestätigung)\n", newCmd ? "EIN" : "AUS");
        delay(300); // Entprellen
      }
      
          // Datenaktualisierung
          unsigned long nowMillis = millis();
          if(nowMillis - lastUpdate > updateInterval){
            lastUpdate = nowMillis;
      
            // opendtu
            if(WiFi.status() == WL_CONNECTED){
              http.begin(client, opendtu_url);
              http.setAuthorization(opendtu_user, opendtu_password);
              int httpCode = http.GET();
              if(httpCode == 200){
                StaticJsonDocument<2048> doc;
                DeserializationError err = deserializeJson(doc, http.getString());
                if(!err) solarPower = doc["total"]["Power"]["v"] | 0.0;
              }
              http.end();
      
              // hichi
              http.begin(client, hichi_url);
              httpCode = http.GET();
              if(httpCode == 200){
                StaticJsonDocument<1024> doc;
                DeserializationError err = deserializeJson(doc, http.getString());
                if(!err){
                  gridPower = doc["StatusSNS"]["Zaehler"]["Power"] | 0.0;
                  einspeisung = doc["StatusSNS"]["Zaehler"]["E_out"] | 0.0;
                  bezug = doc["StatusSNS"]["Zaehler"]["E_in"] | 0.0;
                }
              }
              http.end();
            }
      
            showMainDisplay();
          }
        }
      }
      
      

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

      1 Antwort Letzte Antwort
      1
      • arteckA Offline
        arteckA Offline
        arteck
        Developer Most Active
        schrieb am zuletzt editiert von
        #3

        schau dir mal das nspanel an.. mal so neben bei

        zigbee hab ich, zwave auch, nuc's genauso und HA auch

        B 1 Antwort Letzte Antwort
        0
        • arteckA arteck

          schau dir mal das nspanel an.. mal so neben bei

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

          @arteck
          Ich kenne das NSPanel.

          Nur, das Display was ich hier habe kostet beim netten Chinesen ca. 5 €. Dann noch den wemos d1 mini fü 2 € und das 3D gedruckte Gehäuse. Zusammen sind wir dann bei unter 10 €.

          Das NSPanel liegt bei 50 € plus Tischgehäuse.

          Da bleibe ich bei meiner Version.:face_with_rolling_eyes:

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

          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

          767

          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