Zum Inhalt

ESP32 E-Paper Provisioning System - Technische Analyse

Datum: 23. Oktober 2025
Anforderung: Benutzerfreundliches Setup-System für ESP32 E-Paper Displays


🎯 Anforderungen

Benutzer-Workflow

  1. Ausgangssituation:
  2. Benutzer erhält neues ESP32 E-Paper Display
  3. Laptop ist im gleichen WLAN wie PV-Anlage
  4. Keine komplizierte Konfiguration gewünscht

  5. Setup-Prozess (einmalig):

  6. ESP32 "flashen" mit allen Einstellungen
  7. WLAN-Zugangsdaten (SSID + Passwort)
  8. Inverter-Auswahl und IP-Adresse
  9. Update-Intervalle (Display + API)
  10. Portal-Authentifizierung (UUID + Auth-Token)

  11. Betrieb (dauerhaft):

  12. Automatischer WLAN-Verbindungsaufbau
  13. Polling vom Inverter
  14. Datenübermittlung an Portal (Cloudflare)
  15. Display-Aktualisierung
  16. Status im Portal sichtbar

Technische Anforderungen

  • ESP32-S3 mit E-Paper Display (296×128 oder ähnlich)
  • Kein/Minimale Eingabemöglichkeiten am Gerät selbst
  • Browser-basierte Lösung bevorzugt (keine App-Installation)
  • Maximale Benutzerfreundlichkeit

🔍 Technologie-Analyse

Option 1: ESP32 Provisioning via WiFi AP + WebBrowser ⭐ EMPFOHLEN

Technologie-Stack

  • ESP32 WiFi Manager (z.B. WiFiManager Library)
  • Captive Portal (automatische Browser-Umleitung)
  • Web-basiertes Setup (HTML/JS im ESP32)
  • WebSerial API (optional für Firmware-Upload)

Workflow

1. ESP32 startet im AP-Modus (Access Point)
   └─> SSID: "SolarLog-Setup-XXXX" (XXXX = letzte 4 Stellen MAC)
   └─> Kein Passwort oder Standard-Passwort

2. Benutzer verbindet Laptop mit ESP32-WLAN
   └─> Automatische Captive Portal Umleitung
   └─> Browser öffnet Setup-Seite automatisch

3. Setup-Webseite im Browser (gehostet auf ESP32)
   ┌─────────────────────────────────────────┐
   │  🔌 SolarLog ESP32 Setup                │
   ├─────────────────────────────────────────┤
   │  Schritt 1: WLAN Konfiguration          │
   │  ├─ Verfügbare Netzwerke scannen        │
   │  ├─ SSID auswählen                      │
   │  └─ Passwort eingeben                   │
   │                                          │
   │  Schritt 2: Portal-Verbindung           │
   │  ├─ Portal-URL eingeben oder Standard   │
   │  │   (solarlog-api.karma.organic)       │
   │  └─ Login mit Portal-Account            │
   │      (erstellt automatisch Geräte-Token)│
   │                                          │
   │  Schritt 3: Inverter-Konfiguration      │
   │  ├─ Auto-Scan im lokalen Netzwerk       │
   │  ├─ Inverter aus Liste auswählen        │
   │  └─ Test-Verbindung                     │
   │                                          │
   │  Schritt 4: Einstellungen               │
   │  ├─ Display Update: 30 Sek (Standard)   │
   │  ├─ API Upload: 60 Sek (Standard)       │
   │  └─ Zeitzone: Europe/Berlin             │
   │                                          │
   │  [✓ Konfiguration speichern & starten]  │
   └─────────────────────────────────────────┘

4. ESP32 speichert Konfiguration in Flash
   └─> Verbindet sich mit konfigurierten WLAN
   └─> Registriert sich automatisch im Portal
   └─> Startet Normalbetrieb

5. Bei Fehler: Automatischer AP-Modus nach 3 Versuchen
   └─> Benutzer kann Konfiguration korrigieren

Vorteile ✅

  • Browser-basiert: Keine App-Installation nötig
  • Plattform-unabhängig: Windows, Mac, Linux, Smartphone
  • Captive Portal: Automatische Browser-Umleitung (bekannt von Hotel-WLAN)
  • Netzwerk-Scan: Zeigt verfügbare WLANs und Inverter an
  • Inverter Auto-Discovery: Scannt lokales Netzwerk nach Modbus/HTTP
  • Portal-Integration: Direkter Login mit bestehendem Account
  • Fail-Safe: Automatischer AP-Modus bei Verbindungsproblemen
  • Keine Kabel nötig: Alles über WLAN

Nachteile ⚠️

  • Benutzer muss WLAN wechseln (Laptop → ESP32 → PV-WLAN)
  • Captive Portal funktioniert nicht auf allen Geräten perfekt
  • ESP32 muss Web-Server hosten (Ressourcen)

Option 2: Web Bluetooth API (Chrome-basiert)

Technologie-Stack

  • Web Bluetooth API (Chrome/Edge Browser)
  • ESP32 BLE (Bluetooth Low Energy)
  • Portal-Webseite hostet Setup-Tool

Workflow

1. Benutzer öffnet Portal-Webseite
   └─> https://solarlog.karma.organic/setup

2. Klickt "Neues Gerät einrichten"
   └─> Browser fragt nach Bluetooth-Berechtigung
   └─> Zeigt verfügbare ESP32 Geräte

3. ESP32 wird ausgewählt
   └─> Bluetooth-Verbindung aufgebaut
   └─> Alle Einstellungen über BLE übertragen

4. ESP32 startet mit neuer Konfiguration

Vorteile ✅

  • Kein WLAN-Wechsel nötig
  • Direkt aus Portal startbar
  • Elegant und modern
  • Weniger Schritte für Benutzer

Nachteile ⚠️

  • Nur Chrome/Edge Browser (Safari/Firefox nicht unterstützt)
  • Bluetooth-Reichweite begrenzt (~10m)
  • iOS Einschränkungen (Web Bluetooth nicht verfügbar)
  • Komplexere Implementierung

Option 3: WebSerial API + USB-Kabel

Technologie-Stack

  • WebSerial API (Chrome/Edge)
  • ESP-IDF Flashing-Tool im Browser
  • USB-Verbindung zum ESP32

Workflow

1. ESP32 per USB mit Laptop verbinden
2. Portal-Webseite öffnen: /flash
3. Firmware + Konfiguration hochladen
4. Automatisches Flashen über Browser

Vorteile ✅

  • Zuverlässig: Direkte USB-Verbindung
  • Schnell: Upload über USB
  • Keine WLAN-Probleme während Setup

Nachteile ⚠️

  • USB-Kabel erforderlich
  • Nicht wireless
  • Benutzer muss ESP32 physisch anschließen

🏆 Empfehlung: Option 3 (WebSerial API + USB-Kabel) ⭐ PROFESSIONELLE INSTALLATION

Begründung für Servicetechniker vor Ort

  1. Zuverlässigkeit: Direkte USB-Verbindung = Keine WLAN-Probleme während Setup
  2. Schnelligkeit: USB-Upload in Sekunden, kein Warten auf WLAN-Scans
  3. Sicherheit: Kein temporäres offenes WLAN, keine Fehlkonfiguration möglich
  4. Professionell: Servicetechniker hat Laptop mit USB-Kabel dabei
  5. Eindeutigkeit: Eine Methode, ein Workflow - kein Wireless-Troubleshooting
  6. Vor-Ort-Ansatz: Installation findet ohnehin am Wechselrichter statt
  7. Änderungen selten: Bei Änderungen kommt Techniker erneut vor Ort

Warum die "Nachteile" irrelevant sind

USB-Kabel erforderlich → ✅ Techniker hat Laptop & USB-Kabel dabei
Nicht wireless → ✅ Techniker steht direkt am ESP32, kein Wireless nötig
Physischer Anschluss → ✅ Professionelle Installation, kein End-User-Setup

Zusätzliche Vorteile für professionelle Installation

  • Kein Support-Aufwand: End-User kann nichts falsch machen
  • Kein WLAN-Troubleshooting: "Warum verbindet sich ESP32 nicht?" entfällt
  • Schneller: 2 Minuten statt 10 Minuten Setup-Zeit
  • Dokumentiert: Techniker dokumentiert Installation im System
  • Garantie: Bei Problemen kommt Techniker zurück (mit USB-Kabel)

📋 Implementierungsplan (WebSerial + USB)

Phase 1: Web-Tool für Servicetechniker

Technologien: - Web Serial API (Chrome/Edge Browser) - ESP-IDF Flashing Protocol (esptool.js) - React/TypeScript Frontend im Portal - Konfigurationsdatei-Generator (JSON → ESP32 Flash)

Setup-Tool im Portal: /setup/flash-device

// Portal-Webseite: https://solarlog.karma.organic/setup/flash-device

import { ESPLoader, Transport } from 'esptool-js';

async function flashESP32() {
  // 1. Servicetechniker öffnet Portal auf Laptop
  // 2. Navigiert zu "Neues Gerät installieren"
  // 3. ESP32 per USB anschließen
  // 4. Button "Mit ESP32 verbinden" klicken

  const port = await navigator.serial.requestPort();
  await port.open({ baudRate: 115200 });

  const transport = new Transport(port);
  const loader = new ESPLoader(transport);

  // Flash Firmware + Konfiguration in einem Schritt
  await loader.flashId();
  await loader.write_flash(...);
}

Phase 1a: ESP32 Firmware (Vereinfacht)

Technologien: - ESP-IDF oder Arduino Framework - ArduinoJson (Konfigurationsverwaltung) - SPIFFS/LittleFS (Persistente Speicherung) - KEINE WiFiManager/WebServer mehr nötig!

Komponenten:

// 1. WiFi Manager mit Captive Portal
class ProvisioningManager {
    - startAPMode()              // Access Point starten
    - startWebServer()           // Setup-Webseite hosten
    - scanNetworks()             // Verfügbare WLANs listen
    - scanInverters()            // Modbus/HTTP Inverter finden
    - saveConfiguration()        // Config in Flash speichern
    - validateAndConnect()       // WLAN-Verbindung testen
}

// 2. Portal-Client
class PortalClient {
    - registerDevice()           // Gerät am Portal registrieren
    - authenticate()             // Login mit Token
    - uploadData()               // Produktionsdaten senden
    - receiveCommands()          // Remote-Befehle empfangen
    - heartbeat()                // Status-Updates
}

// 3. Inverter-Client
class InverterClient {
    - autoDetect()               // Inverter-Typ erkennen
    - connect()                  // Modbus/HTTP Verbindung
    - readData()                 // Produktionsdaten lesen
    - reconnect()                // Auto-Reconnect bei Fehler
}

// 4. Display-Manager
class DisplayManager {
    - initialize()               // E-Paper initialisieren
    - updateDisplay()            // Daten anzeigen
    - showStatus()               // Verbindungsstatus
    - partialUpdate()            // Schnelle Updates
}

Phase 2: Backend-Erweiterungen

Neue API-Endpoints:

# backend/app/api/v1/devices.py

POST   /api/v1/devices/register
    - Registriert neues ESP32-Gerät
    - Erstellt Device-Token
    - Input: {mac_address, model, user_id}
    - Output: {device_id, auth_token, mqtt_credentials}

POST   /api/v1/devices/{device_id}/data
    - Empfängt Produktionsdaten vom ESP32
    - Input: {current_power, daily_energy, ...}
    - Authentifizierung via Device-Token

GET    /api/v1/devices/{device_id}/config
    - Liefert Konfiguration an ESP32
    - Output: {poll_interval, display_settings, ...}

POST   /api/v1/devices/{device_id}/heartbeat
    - Status-Update vom ESP32
    - Input: {rssi, uptime, memory, last_error}

GET    /api/v1/devices/{device_id}/logs
    - Debug-Logs für Support
    - Letzten 100 Log-Einträge

Neue Datenbank-Models:

# backend/app/models/device.py

class Device(Base):
    __tablename__ = "devices"

    id: int                          # Primary Key
    uuid: str                        # Eindeutige Geräte-UUID
    mac_address: str                 # MAC-Adresse
    device_token: str                # Auth-Token (hashed)
    user_id: int                     # Besitzer
    inverter_id: int                 # Verknüpfter Inverter

    # Status
    status: str                      # online, offline, error
    last_seen: datetime              # Letzter Heartbeat
    firmware_version: str            # ESP32 Firmware
    rssi: int                        # WLAN-Signalstärke
    uptime: int                      # Sekunden seit Start

    # Konfiguration
    poll_interval: int = 60          # Sekunden
    display_update_interval: int = 30
    timezone: str = "Europe/Berlin"

    # Debug
    last_error: str
    total_errors: int
    total_uploads: int

    created_at: datetime
    updated_at: datetime

Phase 3: Frontend-Erweiterungen

Neue React-Komponenten:

// src/components/DeviceSetup.tsx
// Zeigt Anleitung für ESP32-Setup
// QR-Code für einfachen WLAN-Zugang zum ESP32-AP

// src/components/DeviceList.tsx
// Liste aller registrierten ESP32-Geräte
// Status-Anzeige (online/offline/error)

// src/components/DeviceDetails.tsx
// Detailansicht eines Geräts
// - Live-Daten
// - Debug-Logs
// - Konfiguration
// - Remote-Neustart

// src/components/DeviceProvisioning.tsx
// Optional: WebBluetooth-Alternative
// Nur für Chrome/Edge Browser

Workflow im Portal:

1. Benutzer: "Gerät hinzufügen" → Anleitung öffnet sich
   ├─> QR-Code wird generiert (WLAN-Zugangsdaten ESP32-AP)
   ├─> Temporäres Setup-Token wird erstellt
   └─> Schritt-für-Schritt Anleitung

2. Benutzer scannt QR-Code mit Smartphone (optional)
   └─> Oder: Manuelle Verbindung zu "SolarLog-Setup-XXXX"

3. ESP32 Captive Portal öffnet Setup-Seite
   ├─> Login mit Portal-Account (OAuth2)
   ├─> Setup-Token wird validiert
   └─> Gerät wird automatisch dem Account zugeordnet

4. Nach erfolgreichem Setup:
   ├─> Gerät erscheint in Portal-Geräteliste
   ├─> Status: "Registriert - Warte auf erste Daten..."
   └─> Nach 60 Sek: Erste Daten treffen ein → Status: "Online ✓"


🔐 Sicherheitskonzept

Device-Authentifizierung

# Registrierung
1. ESP32  Portal: "Ich möchte mich registrieren"
   - Sendet: MAC-Adresse, Setup-Token (vom Portal-Login)

2. Portal validiert Setup-Token
   - Token nur 15 Minuten gültig
   - Kann nur einmal verwendet werden

3. Portal generiert Device-Token (JWT)
   - 256-bit Zufallsstring
   - Gespeichert als Hash in DB
   - Wird an ESP32 zurückgegeben

4. ESP32 speichert Device-Token in Flash
   - Für alle zukünftigen API-Calls
   - Automatische Token-Rotation alle 30 Tage

# Datenkommunikation
ESP32  Portal: Authorization: Bearer <device_token>
Portal validiert Token  Akzeptiert Daten

Failover-Strategie

1. Primär: HTTPS REST API
   └─> POST /api/v1/devices/{id}/data
   └─> Intervall: 60 Sekunden

2. Fallback: MQTT (optional)
   └─> Weniger Overhead
   └─> Bessere Offline-Toleranz
   └─> QoS 1 (mindestens einmal zugestellt)

3. Bei Verbindungsausfall:
   └─> Lokales Buffering (letzte 100 Datenpunkte)
   └─> Automatische Synchronisation bei Reconnect
   └─> Exponential Backoff (1s, 2s, 4s, ..., max 60s)

🛠️ Setup-Webseite (im ESP32 gehostet)

HTML/JavaScript (Minimalistisch)

<!DOCTYPE html>
<html>
<head>
    <title>SolarLog ESP32 Setup</title>
    <style>
        /* Responsive Design, funktioniert auf allen Geräten */
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: Arial; background: #f5f5f5; padding: 20px; }
        .container { max-width: 500px; margin: 0 auto; background: white; 
                     padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        h1 { color: #ff9800; margin-bottom: 20px; }
        .step { display: none; }
        .step.active { display: block; }
        input, select { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; border-radius: 5px; }
        button { width: 100%; padding: 12px; background: #ff9800; color: white; border: none; 
                 border-radius: 5px; cursor: pointer; font-size: 16px; margin-top: 10px; }
        button:hover { background: #f57c00; }
        .network-item { padding: 10px; border: 1px solid #ddd; margin: 5px 0; 
                       cursor: pointer; border-radius: 5px; }
        .network-item:hover { background: #f5f5f5; }
        .status { padding: 10px; margin: 10px 0; border-radius: 5px; }
        .status.success { background: #4caf50; color: white; }
        .status.error { background: #f44336; color: white; }
        .spinner { border: 3px solid #f3f3f3; border-top: 3px solid #ff9800; 
                   border-radius: 50%; width: 30px; height: 30px; 
                   animation: spin 1s linear infinite; margin: 20px auto; }
        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
    </style>
</head>
<body>
    <div class="container">
        <h1>🔌 SolarLog Setup</h1>

        <!-- Schritt 1: WLAN -->
        <div class="step active" id="step1">
            <h2>Schritt 1: WLAN Konfiguration</h2>
            <p>Wähle das WLAN deiner PV-Anlage:</p>
            <button onclick="scanNetworks()">📡 Netzwerke scannen</button>
            <div id="networks"></div>
            <br>
            <label>Oder manuell eingeben:</label>
            <input type="text" id="ssid" placeholder="WLAN Name (SSID)">
            <input type="password" id="password" placeholder="WLAN Passwort">
            <button onclick="nextStep(2)">Weiter →</button>
        </div>

        <!-- Schritt 2: Portal-Login -->
        <div class="step" id="step2">
            <h2>Schritt 2: Portal-Verbindung</h2>
            <p>Melde dich mit deinem SolarLog-Account an:</p>
            <input type="text" id="portalUrl" value="https://solarlog-api.karma.organic" readonly>
            <input type="email" id="email" placeholder="E-Mail Adresse">
            <input type="password" id="portalPassword" placeholder="Passwort">
            <button onclick="loginPortal()">Login & Gerät registrieren</button>
            <button onclick="nextStep(1)">← Zurück</button>
        </div>

        <!-- Schritt 3: Inverter -->
        <div class="step" id="step3">
            <h2>Schritt 3: Inverter-Auswahl</h2>
            <p>Suche nach verfügbaren Invertern:</p>
            <button onclick="scanInverters()">🔍 Inverter scannen</button>
            <div id="inverters"></div>
            <br>
            <label>Oder manuell eingeben:</label>
            <select id="inverterType">
                <option value="SMA">SMA</option>
                <option value="Kostal">Kostal</option>
                <option value="Fronius">Fronius</option>
                <option value="Huawei">Huawei</option>
                <option value="Delta">Delta</option>
                <option value="SolarEdge">SolarEdge</option>
            </select>
            <input type="text" id="inverterIp" placeholder="IP-Adresse (z.B. 192.168.1.100)">
            <button onclick="testInverter()">🧪 Verbindung testen</button>
            <button onclick="nextStep(4)">Weiter →</button>
            <button onclick="nextStep(2)">← Zurück</button>
        </div>

        <!-- Schritt 4: Einstellungen -->
        <div class="step" id="step4">
            <h2>Schritt 4: Einstellungen</h2>
            <label>Display Update Intervall:</label>
            <select id="displayInterval">
                <option value="10">10 Sekunden</option>
                <option value="30" selected>30 Sekunden</option>
                <option value="60">60 Sekunden</option>
            </select>

            <label>API Upload Intervall:</label>
            <select id="apiInterval">
                <option value="30">30 Sekunden</option>
                <option value="60" selected>60 Sekunden</option>
                <option value="300">5 Minuten</option>
            </select>

            <label>Zeitzone:</label>
            <select id="timezone">
                <option value="Europe/Berlin" selected>Europa/Berlin (MEZ)</option>
                <option value="Europe/Vienna">Europa/Wien</option>
                <option value="Europe/Zurich">Europa/Zürich</option>
            </select>

            <button onclick="saveAndFinish()">✓ Speichern & Starten</button>
            <button onclick="nextStep(3)">← Zurück</button>
        </div>

        <!-- Schritt 5: Fertig -->
        <div class="step" id="step5">
            <h2>✅ Setup abgeschlossen!</h2>
            <div class="status success">
                <p>Dein ESP32 wurde erfolgreich konfiguriert.</p>
                <p>Das Gerät startet jetzt neu und verbindet sich automatisch mit deinem WLAN.</p>
            </div>
            <p><strong>Nächste Schritte:</strong></p>
            <ol>
                <li>Verbinde deinen Laptop wieder mit deinem normalen WLAN</li>
                <li>Öffne das Portal: <a href="https://solarlog.karma.organic">solarlog.karma.organic</a></li>
                <li>Dein Gerät sollte nach ~30 Sekunden online sein</li>
            </ol>
        </div>

        <div id="status"></div>
    </div>

    <script>
        let config = {};

        function nextStep(step) {
            document.querySelectorAll('.step').forEach(s => s.classList.remove('active'));
            document.getElementById('step' + step).classList.add('active');
        }

        function showStatus(message, type) {
            const status = document.getElementById('status');
            status.innerHTML = `<div class="status ${type}">${message}</div>`;
        }

        async function scanNetworks() {
            showStatus('Scanne WLAN-Netzwerke...', 'info');
            const resp = await fetch('/api/scan-wifi');
            const networks = await resp.json();

            const container = document.getElementById('networks');
            container.innerHTML = networks.map(net => 
                `<div class="network-item" onclick="selectNetwork('${net.ssid}')">
                    📶 ${net.ssid} (${net.rssi} dBm) ${net.secure ? '🔒' : ''}
                </div>`
            ).join('');
        }

        function selectNetwork(ssid) {
            document.getElementById('ssid').value = ssid;
            document.getElementById('password').focus();
        }

        async function loginPortal() {
            const email = document.getElementById('email').value;
            const password = document.getElementById('portalPassword').value;

            if (!email || !password) {
                showStatus('Bitte E-Mail und Passwort eingeben', 'error');
                return;
            }

            showStatus('Melde an...', 'info');

            // Login am Portal
            const resp = await fetch(document.getElementById('portalUrl').value + '/api/v1/auth/login', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({email, password})
            });

            if (resp.ok) {
                const data = await resp.json();
                config.portalToken = data.access_token;

                // Gerät registrieren
                const macResp = await fetch('/api/mac-address');
                const macData = await macResp.json();

                const deviceResp = await fetch(document.getElementById('portalUrl').value + '/api/v1/devices/register', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': `Bearer ${data.access_token}`
                    },
                    body: JSON.stringify({
                        mac_address: macData.mac,
                        model: 'ESP32-S3 E-Paper'
                    })
                });

                const deviceData = await deviceResp.json();
                config.deviceId = deviceData.device_id;
                config.deviceToken = deviceData.auth_token;

                showStatus('✓ Erfolgreich angemeldet & registriert', 'success');
                setTimeout(() => nextStep(3), 1500);
            } else {
                showStatus('Login fehlgeschlagen. Bitte Zugangsdaten prüfen.', 'error');
            }
        }

        async function scanInverters() {
            showStatus('Scanne lokales Netzwerk nach Invertern...', 'info');
            const resp = await fetch('/api/scan-inverters');
            const inverters = await resp.json();

            const container = document.getElementById('inverters');
            container.innerHTML = inverters.map(inv => 
                `<div class="network-item" onclick="selectInverter('${inv.ip}', '${inv.type}')">
${inv.type} @ ${inv.ip} (${inv.model})
                </div>`
            ).join('');
        }

        function selectInverter(ip, type) {
            document.getElementById('inverterIp').value = ip;
            document.getElementById('inverterType').value = type;
        }

        async function testInverter() {
            const ip = document.getElementById('inverterIp').value;
            const type = document.getElementById('inverterType').value;

            if (!ip) {
                showStatus('Bitte IP-Adresse eingeben', 'error');
                return;
            }

            showStatus('Teste Verbindung...', 'info');

            const resp = await fetch('/api/test-inverter', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({ip, type})
            });

            const result = await resp.json();

            if (result.success) {
                showStatus(`✓ Verbindung erfolgreich! Aktuell: ${result.current_power}W`, 'success');
            } else {
                showStatus('Verbindung fehlgeschlagen: ' + result.error, 'error');
            }
        }

        async function saveAndFinish() {
            const ssid = document.getElementById('ssid').value;
            const password = document.getElementById('password').value;
            const inverterIp = document.getElementById('inverterIp').value;
            const inverterType = document.getElementById('inverterType').value;

            if (!ssid || !password || !inverterIp) {
                showStatus('Bitte alle Felder ausfüllen', 'error');
                return;
            }

            const configuration = {
                wifi: {
                    ssid: ssid,
                    password: password
                },
                portal: {
                    url: document.getElementById('portalUrl').value,
                    device_id: config.deviceId,
                    device_token: config.deviceToken
                },
                inverter: {
                    type: inverterType,
                    ip: inverterIp
                },
                intervals: {
                    display: parseInt(document.getElementById('displayInterval').value),
                    api: parseInt(document.getElementById('apiInterval').value)
                },
                timezone: document.getElementById('timezone').value
            };

            showStatus('Speichere Konfiguration...', 'info');

            const resp = await fetch('/api/save-config', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify(configuration)
            });

            if (resp.ok) {
                nextStep(5);
                // ESP32 startet in 3 Sekunden neu
                setTimeout(() => {
                    fetch('/api/restart');
                }, 3000);
            } else {
                showStatus('Fehler beim Speichern', 'error');
            }
        }
    </script>
</body>
</html>

📊 Kosten-Nutzen-Analyse

Entwicklungsaufwand

Komponente Aufwand Priorität
ESP32 Firmware (Provisioning) 5-7 Tage Hoch
ESP32 Firmware (Inverter Clients) 3-5 Tage Hoch
Backend API-Erweiterungen 2-3 Tage Hoch
Frontend Device Management 2-3 Tage Mittel
Testing & Debugging 3-4 Tage Hoch
Dokumentation 1-2 Tage Mittel
GESAMT 16-24 Tage

Hardware-Kosten (pro Gerät)

  • ESP32-S3 DevKit: ~8-12 EUR
  • E-Paper Display 2.9" (296×128): ~15-25 EUR
  • Gehäuse (3D-Druck oder Kaufteil): ~5-10 EUR
  • Netzteil 5V/1A: ~3-5 EUR
  • GESAMT pro Gerät: ~31-52 EUR

Skalierbarkeit

  • Backend kann tausende Geräte parallel verwalten
  • MQTT-Broker für bessere Performance bei >100 Geräten empfohlen
  • Cloudflare Tunnel skaliert automatisch

🚀 Alternativen & Erweiterungen

Phase 2 Features (später)

  1. Over-The-Air (OTA) Updates
  2. Firmware-Updates über Portal
  3. Automatische Updates bei neuen Versionen

  4. Remote-Konfiguration

  5. Einstellungen nachträglich im Portal ändern
  6. ESP32 lädt neue Config beim nächsten Heartbeat

  7. Multi-Inverter Support

  8. Ein ESP32 liest mehrere Inverter aus
  9. Aggregierte Anzeige auf Display

  10. Offline-Modus

  11. ESP32 funktioniert auch ohne Internet
  12. Lokales Dashboard im WLAN verfügbar
  13. Automatische Sync wenn Internet wieder da

  14. Smart Home Integration

  15. MQTT Broker für Home Assistant
  16. EVCC Integration für dynamisches Laden

✅ Nächste Schritte

  1. Entscheidung: WiFi AP + Captive Portal Ansatz bestätigen
  2. Hardware: ESP32-S3 Board + E-Paper Display bestellen
  3. MVP Entwicklung:
  4. Woche 1: ESP32 Provisioning System
  5. Woche 2: Backend Device Management
  6. Woche 3: Frontend Integration
  7. Woche 4: Testing & Dokumentation

  8. Pilotphase: Erste 3-5 Geräte testen

  9. Produktion: Skalierung & Optimierung