// Versionsverlauf: // V1.0 - 01.01.2025 - Initiale Version mit grundlegender Funktionalität // V1.1 - 15.01.2025 - Hinzufügung von Preferences für persistente Speicherung // V1.2 - 01.02.2025 - Implementierung des Fallback-AP-Modus // V1.3 - 15.02.2025 - Automatische Sommer-/Winterzeit-Umstellung // V1.4 - 01.03.2025 - Erweiterte Weboberfläche mit Statusanzeige // V1.5 - 31.03.2025 - Hinzufügung der Backlight-Steuerung // V1.6 - 31.03.2025 - Anpassbare Display-Anzeige hinzugefügt // V1.7 - 31.03.2025 - Versionsnummer in Willkommensnachricht hinzugefügt // V1.8 - 31.03.2025 - Display-Vorschau im Webinterface hinzugefügt // V1.9 - 31.03.2025 - Firmware-Version im Webinterface hinzugefügt // V2.0 - 02.04.2025 - Aus/Einschaltfunktion nach Zeitplan implementiert // V2.1 - 02.04.2025 - DeepSleep wärend Display Aus // V2.2b - 02.04.2025 - Innentemperatur implementiert // V2.3 - 04.04.2025 - Innentemperatur offset implementiert // V2.4 - 04.04.2025 - Webinterface kompakter gestaltet // V2.5 - 06.04.2025 - Fehlerbehebungen und Modus für "kein BME280" hinzugefügt. Stromausfall färend DeepSleep berücksichtigt // # Projektdokumentation: ESP32 Wetterstation mit Webinterface // // ## 1. Projektübersicht // // ### 1.1 Beschreibung // Die ESP32-Wetterstation ist ein IoT-Projekt, das Wetterdaten (Temperatur und Luftfeuchtigkeit) sowohl lokal (Innenraum über BME280-Sensor) als auch extern (über eine API) anzeigt. Sie verfügt über ein konfigurierbares Webinterface, persistente Einstellungsspeicherung, automatische Zeitumstellung und einen Zeitplan für das Display. Die aktuelle Version (V2.3) beinhaltet ein einstellbares Temperatur-Offset für den Innenraum-Sensor. // // ### 1.2 Hauptfunktionen // - Anzeige von Datum, Uhrzeit, Innen- und Außentemperatur sowie Luftfeuchtigkeit auf einem 16x2 LCD // - Konfiguration über ein Webinterface // - WiFi-Verbindung mit Fallback-AP-Modus // - Persistente Speicherung von Einstellungen // - Automatische Sommer-/Winterzeitumstellung // - Deep-Sleep-Modus nach Zeitplan // - Temperatur-Offset für den BME280-Sensor // // ### 1.3 Version // - **Aktuelle Version**: V2.3 (Stand: 04.04.2025) // - **Versionsverlauf**: Siehe Kommentar im Code (oben) // // ## 2. Hardware // // ### 2.1 Verwendete Komponenten // - **ESP32-DevKit-V1** (z. B. NodeMCU-32S oder ähnliches Modul) // - **LiquidCrystal_I2C Display** (16x2 Zeichen, I²C-Interface, Adresse: 0x27) // - **BME280 Sensor** (Temperatur und Luftfeuchtigkeit, I²C, Adresse: 0x76 oder 0x77) // - **Stromversorgung**: 5V über USB oder 3.3V extern (mindestens 500mA empfohlen) // - **Verbindungskabel**: Dupont-Kabel oder Breadboard // // ### 2.2 Anschlussplan // Die folgende Tabelle beschreibt die Verkabelung der Komponenten mit dem ESP32: // // | **Komponente** | **Pin am ESP32** | **Pin an der Komponente** | **Beschreibung** | // |------------------------|------------------|---------------------------|------------------------------------------| // | **LCD (I²C)** | | | | // | - SDA | GPIO 21 (SDA) | SDA | I²C-Datenleitung | // | - SCL | GPIO 22 (SCL) | SCL | I²C-Taktleitung | // | - VCC | 5V | VCC | Stromversorgung (5V) | // | - GND | GND | GND | Masse | // | **BME280 (I²C)** | | | | // | - SDA | GPIO 21 (SDA) | SDA | I²C-Datenleitung (gemeinsam mit LCD) | // | - SCL | GPIO 22 (SCL) | SCL | I²C-Taktleitung (gemeinsam mit LCD) | // | - VCC | 3.3V | VIN | Stromversorgung (3.3V empfohlen) | // | - GND | GND | GND | Masse | // | **ESP32 Stromversorgung** | | | | // | - USB | 5V (USB) | - | Strom über USB-Port | // | - GND | GND | - | Masse | // // #### Hinweise zum Anschluss: // - **I²C-Bus**: Sowohl das LCD als auch der BME280 nutzen den gleichen I²C-Bus (GPIO 21 und 22). Stelle sicher, dass die Adressen unterschiedlich sind (LCD: 0x27, BME280: 0x76 oder 0x77). // - **Pull-Up-Widerstände**: Der I²C-Bus benötigt typischerweise 4.7kΩ Pull-Up-Widerstände zwischen SDA/SCL und VCC (oft bereits im Modul integriert). // - **Spannung**: Der BME280 sollte mit 3.3V betrieben werden, das LCD kann mit 5V arbeiten (prüfe die Spezifikation deines Moduls). // // ## 3. Software // // ### 3.1 Verwendete Bibliotheken // - **Wire.h**: Für I²C-Kommunikation // - **LiquidCrystal_I2C.h**: Steuerung des LCD-Displays // - **WiFi.h**: WiFi-Funktionalität // - **WebServer.h**: Webserver für Konfiguration // - **HTTPClient.h**: Abruf von Wetterdaten über API // - **ArduinoJson.h**: Parsen von JSON-Daten // - **time.h**: Zeit- und Datumssynchronisation // - **Preferences.h**: Persistente Speicherung // - **Adafruit_Sensor.h**: Basis für BME280 // - **Adafruit_BME280.h**: BME280-Sensorsteuerung // // ### 3.2 Funktionsübersicht // - **setup()**: Initialisiert Hardware, lädt Einstellungen, verbindet mit WiFi, startet Webserver // - **loop()**: Verwaltet Display-Updates, Wetterabrufe, Zeitplan und Webserver-Anfragen // - **handleRoot()**: Generiert das Webinterface // - **handleSet()**: Verarbeitet Einstellungsänderungen // - **displayBME280Data()**: Zeigt Innentemperatur und Luftfeuchtigkeit an // - **displayTimeDateWeather()**: Zeigt Datum, Uhrzeit und Außenwetter an // - **updateWeather()**: Holt Wetterdaten von der API // - **checkDisplaySchedule()**: Steuert Deep-Sleep nach Zeitplan // - **reconnectWiFi()**: Verwaltet WiFi-Verbindung und Fallback-AP // // ### 3.3 Konfiguration // Einstellungen werden über das Webinterface vorgenommen und in den Preferences gespeichert: // - WLAN-Zugangsdaten // - Station-ID und API-URL // - Zeit- und Wetter-Update-Intervalle // - Display-Einstellungen (Helligkeit, Anzeigeoptionen, Positionen) // - Temperatur-Offset (-3 bis +3°C) // - Zeitplan für Display-Ein/Aus // // ## 4. Installation und Inbetriebnahme // // ### 4.1 Hardware-Vorbereitung // 1. Verbinde die Komponenten gemäß dem Anschlussplan. // 2. Stelle sicher, dass alle Verbindungen fest sitzen und die Spannungen korrekt sind. // 3. Schließe den ESP32 über USB an einen Computer an. // // ### 4.2 Software-Installation // 1. **Arduino IDE** installieren (oder eine andere kompatible IDE). // 2. **Bibliotheken installieren**: // - Öffne den Bibliotheks-Manager in der Arduino IDE. // - Suche und installiere die oben genannten Bibliotheken. // 3. **Board konfigurieren**: // - Wähle "ESP32 Dev Module" als Board aus. // - Setze die Upload-Geschwindigkeit auf 115200 Baud. // 4. **Code hochladen**: // - Kopiere den vollständigen Code (siehe vorherige Antworten). // - Lade den Sketch über USB auf den ESP32 hoch. // // ### 4.3 Erste Inbetriebnahme // 1. Nach dem Upload zeigt das LCD "Verbinde WLAN..." an. // 2. Bei erfolgreicher Verbindung erscheint die IP-Adresse. // 3. Öffne die IP-Adresse im Browser, um das Webinterface zu konfigurieren. // 4. Falls keine Verbindung möglich ist, startet der Fallback-AP (SSID: ESP32Fallback, Passwort: 123456). // 5. Im Webinterface den API-Link durch ändern der Postleitzahl anpassen: http://pottschrauber.ddnss.de/projekte/wetter-api/index.php?plz=45897 // // ## 5. Bedienung // // ### 5.1 Webinterface // - **URL**: IP-Adresse des ESP32 (z. B. 192.168.x.x) // - **Abschnitte**: // - **Status**: Zeigt WLAN, NTP, Zeitplan und Firmware-Status. // - **Einstellungen**: Konfiguriert WLAN, API, Intervalle und Offset. // - **Display-Anzeige**: Passt die Anzeigeoptionen und Positionen an. // - **Display-Zeitplan**: Legt Ein-/Ausschaltzeiten fest. // - **Vorschau**: Zeigt eine Live-Vorschau des Displays. // - **Buttons**: "Speichern", "Wetter aktualisieren", "Neustart" // // ### 5.2 Display // - Wechselt zwischen Innen- (BME280) und Außenwetter (API) basierend auf `displaySwitchInterval`. // - Zeigt Willkommensnachricht und Version beim Start. // // ## 6. Erweiterungsmöglichkeiten // // - **Bluetooth-Unterstützung**: OTA-Updates über Bluetooth hinzufügen (siehe vorherige Antwort). // - **Zusätzliche Sensoren**: Integration eines Lichtsensors (z. B. BH1750) für automatische Helligkeit. // - **Farbiges Display**: Austausch des LCD gegen ein TFT-Display für mehr Informationen. // // ## 7. Fehlerbehebung // // | **Problem** | **Mögliche Ursache** | **Lösung** | // |--------------------------------|-----------------------------------|--------------------------------------| // | Keine WLAN-Verbindung | Falsche Zugangsdaten | Überprüfe SSID und Passwort im Webinterface | // | LCD bleibt leer | Falsche I²C-Adresse | Scanne I²C-Adressen mit einem Scanner-Sketch | // | BME280-Daten fehlerhaft | Anschlussproblem | Verkabelung prüfen, 3.3V sicherstellen | // | Webinterface nicht erreichbar | ESP32 im Deep-Sleep | Warte bis Zeitplan das Display aktiviert | // // ## 8. Schlusswort // Dieses Projekt bietet eine flexible und erweiterbare Wetterstation mit modernen IoT-Funktionen. Die Dokumentation und der Anschlussplan sollten eine einfache Nachbaubarkeit gewährleisten. Für Fragen oder Verbesserungsvorschläge kontaktiere den Entwickler (hypothetisch: support@xai.com). #include #include #include #include #include #include #include #include #include #include // Aktuelle Version als Konstante const char* VERSION = "V2.5"; // WLAN-Zugangsdaten String wifiSSID = "WLAN SSID"; String wifiPassword = "WLAN Passwort"; // Fallback Access Point Daten const char* fallbackSSID = "ESP32Fallback"; const char* fallbackPassword = "123456"; // Station-ID int stationId = 1; String url = "http://p4sf4qeavfus9j7u.myfritz.net/data.php?station=45897"; // NTP-Server const char* ntpServer = "pool.ntp.org"; long gmtOffset_sec = 3600; int daylightOffset_sec = 0; bool autoDST = true; // Webserver auf Port 80 WebServer server(80); // LCD-Objekt LiquidCrystal_I2C lcd(0x27, 16, 2); uint8_t lcdAddress = 0x27; // BME280-Objekt und Status Adafruit_BME280 bme; // I²C-Adresse standardmäßig 0x76 oder 0x77 bool hasBME280 = false; // Neue Variable zur Erkennung des Sensors // Preferences Preferences preferences; // Konfigurierbare Intervalle unsigned long timeUpdateInterval = 1000; unsigned long weatherUpdateInterval = 60000; unsigned long displaySwitchInterval = 5000; int displayBrightness = 1; String welcomeMessage = "Willkommen!"; unsigned long welcomeDuration = 3000; unsigned long lastWeatherUpdateTime = 0; bool inFallbackMode = false; float tempOffset = 0.0; // Display-Zeitplan bool displayScheduleActive = false; int displayOnHour = 7; int displayOnMinute = 0; int displayOffHour = 22; int displayOffMinute = 0; // RTC-Daten für Deep Sleep Persistenz RTC_DATA_ATTR bool wasInDeepSleep = false; // Display-Konfigurationsvariablen struct DisplayConfig { bool showYear = true; bool showTime = true; bool showTemp = true; bool showHumid = true; uint8_t datePosX = 1; uint8_t datePosY = 0; uint8_t timePosX = 10; uint8_t timePosY = 0; uint8_t tempPosX = 1; uint8_t tempPosY = 1; uint8_t humidPosX = 10; uint8_t humidPosY = 1; } displayConfig; void setLCDBacklight(uint8_t brightness) { Wire.beginTransmission(lcdAddress); Wire.write(0x00); Wire.write(brightness == 0 ? 0x00 : 0x08); Wire.endTransmission(); } void setup() { Serial.begin(115200); lcd.init(); lcd.backlight(); setLCDBacklight(displayBrightness); lcd.setCursor(0, 0); lcd.print("Verbinde WLAN..."); // Prüfe Wakeup-Grund esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause(); if (wakeup_reason == ESP_SLEEP_WAKEUP_TIMER) { Serial.println("Aufgewacht aus Deep Sleep durch Timer"); lcd.clear(); lcd.setCursor(0, 0); lcd.print("Deep Sleep Ende"); delay(2000); } else if (wasInDeepSleep) { Serial.println("Neustart nach Deep Sleep Unterbrechung (z. B. Stromausfall)"); lcd.clear(); lcd.setCursor(0, 0); lcd.print("Stromausfall?"); lcd.setCursor(0, 1); lcd.print("Deep Sleep war aktiv"); delay(5000); wasInDeepSleep = false; } else { Serial.println("Normaler Start"); } // BME280 Initialisierung mit Statusprüfung hasBME280 = bme.begin(0x76) || bme.begin(0x77); if (!hasBME280) { Serial.println("BME280 nicht gefunden! Fahre ohne Sensor fort."); lcd.clear(); lcd.print("Kein BME280"); delay(2000); } else { Serial.println("BME280 erfolgreich initialisiert."); } preferences.begin("settings", false); wifiSSID = preferences.getString("wifiSSID", "WLAN Name"); wifiPassword = preferences.getString("wifiPassword", "WLAN Password"); autoDST = preferences.getBool("autoDST", true); daylightOffset_sec = preferences.getInt("dst", 0); timeUpdateInterval = preferences.getULong("timeInterval", 1000); weatherUpdateInterval = preferences.getULong("weatherInterval", 60000); displaySwitchInterval = preferences.getULong("switchInterval", 5000); stationId = preferences.getInt("stationId", 1); displayBrightness = preferences.getInt("brightness", 255); welcomeMessage = preferences.getString("welcomeMsg", "Willkommen!"); welcomeDuration = preferences.getULong("welcomeDur", 3000); url = preferences.getString("apiUrl", "http://p4sf4qeavfus9j7u.myfritz.net/data.php?station=1"); tempOffset = preferences.getFloat("tempOffset", 0.0); displayScheduleActive = preferences.getBool("schedActive", false); displayOnHour = preferences.getInt("onHour", 7); displayOnMinute = preferences.getInt("onMinute", 0); displayOffHour = preferences.getInt("offHour", 22); displayOffMinute = preferences.getInt("offMinute", 0); displayConfig.showYear = preferences.getBool("showYear", true); displayConfig.showTime = preferences.getBool("showTime", true); displayConfig.showTemp = preferences.getBool("showTemp", true); displayConfig.showHumid = preferences.getBool("showHumid", true); displayConfig.datePosX = preferences.getUChar("datePosX", 0); displayConfig.datePosY = preferences.getUChar("datePosY", 0); displayConfig.timePosX = preferences.getUChar("timePosX", 11); displayConfig.timePosY = preferences.getUChar("timePosY", 0); displayConfig.tempPosX = preferences.getUChar("tempPosX", 0); displayConfig.tempPosY = preferences.getUChar("tempPosY", 1); displayConfig.humidPosX = preferences.getUChar("humidPosX", 8); displayConfig.humidPosY = preferences.getUChar("humidPosY", 1); WiFi.disconnect(true); WiFi.mode(WIFI_STA); reconnectWiFi(); setupWebServer(); configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); if (autoDST) updateDST(); lcd.clear(); lcd.setCursor(0, 0); if (welcomeMessage.length() > 16) { lcd.print(welcomeMessage.substring(0, 16)); lcd.setCursor(0, 1); String secondLine = welcomeMessage.substring(16, welcomeMessage.length() > 32 ? 32 : welcomeMessage.length()); lcd.print(secondLine.substring(0, 16 - strlen(VERSION))); lcd.setCursor(16 - strlen(VERSION), 1); lcd.print(VERSION); } else { lcd.print(welcomeMessage); lcd.setCursor(16 - strlen(VERSION), 1); lcd.print(VERSION); } delay(welcomeDuration); if (displayScheduleActive) { lcd.clear(); lcd.setCursor(0, 0); lcd.print("Ein: "); lcd.print(displayOnHour < 10 ? "0" : ""); lcd.print(displayOnHour); lcd.print(":"); lcd.print(displayOnMinute < 10 ? "0" : ""); lcd.print(displayOnMinute); lcd.setCursor(0, 1); lcd.print("Aus: "); lcd.print(displayOffHour < 10 ? "0" : ""); lcd.print(displayOffHour); lcd.print(":"); lcd.print(displayOffMinute < 10 ? "0" : ""); lcd.print(displayOffMinute); delay(3000); } lcd.clear(); lcd.setCursor(0, 0); lcd.print("Wetterstation:"); lcd.setCursor(0, 1); lcd.print("ID " + String(stationId) + " verbunden"); delay(3000); lcd.clear(); } void setupWebServer() { server.on("/", handleRoot); server.on("/set", handleSet); server.on("/updateWeather", handleUpdateWeather); server.on("/restart", handleRestart); server.begin(); } void handleRoot() { struct tm timeinfo; String lastWeatherStr = "Noch nicht aktualisiert"; if (lastWeatherUpdateTime > 0 && getLocalTime(&timeinfo)) { char timeStr[20]; strftime(timeStr, sizeof(timeStr), "%d.%m.%Y %H:%M", &timeinfo); lastWeatherStr = String(timeStr); } String html = ""; html += ""; html += "ESP32 Wetterdisplay"; html += ""; html += "

ESP32 Wetterdisplay Konfiguration

"; html += "

Status

"; html += "

WLAN-Status: " + String(WiFi.status() == WL_CONNECTED ? "Verbunden" : "Nicht verbunden") + "

"; html += "

Verbundenes WLAN: " + String(WiFi.SSID()) + "

"; html += "

Letzte Wetteraktualisierung: " + lastWeatherStr + "

"; html += "

NTP-Sync: " + String(getLocalTime(&timeinfo) ? "Synchronisiert" : "Nicht synchronisiert") + "

"; html += "

Zeitmodus: " + String(autoDST ? "Automatisch" : (daylightOffset_sec == 3600 ? "Sommerzeit (MESZ)" : "Winterzeit (MEZ)")) + "

"; html += "

Firmware-Version: " + String(VERSION) + "

"; html += "

Display-Zeitplan: " + String(displayScheduleActive ? "Aktiv" : "Inaktiv") + "

"; html += "

BME280-Sensor: " + String(hasBME280 ? "Vorhanden" : "Nicht vorhanden") + "

"; // Neuer Status if (displayScheduleActive) { html += "

Einschaltzeit: " + String(displayOnHour < 10 ? "0" : "") + String(displayOnHour) + ":" + String(displayOnMinute < 10 ? "0" : "") + String(displayOnMinute) + "

"; html += "

Ausschaltzeit: " + String(displayOffHour < 10 ? "0" : "") + String(displayOffHour) + ":" + String(displayOffMinute < 10 ? "0" : "") + String(displayOffMinute) + "

"; } html += "
"; html += "

Einstellungen

"; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; if (hasBME280) { // Nur anzeigen, wenn BME280 vorhanden html += ""; } html += "

Display-Anzeige

"; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += "

Display-Zeitplan

"; html += ""; html += ""; html += ""; html += ""; html += ""; html += "

Display-Vorschau

"; html += ""; html += ""; html += "
"; html += "
"; html += "
"; html += "
"; html += "
"; server.send(200, "text/html", html); } void handleSet() { bool wifiChanged = false; if (server.hasArg("wifiSSID") && server.arg("wifiSSID") != wifiSSID) { wifiSSID = server.arg("wifiSSID"); wifiChanged = true; } if (server.hasArg("wifiPassword") && server.arg("wifiPassword") != wifiPassword) { wifiPassword = server.arg("wifiPassword"); wifiChanged = true; } if (server.hasArg("stationId")) { stationId = server.arg("stationId").toInt(); if (stationId < 1) stationId = 1; if (stationId > 99) stationId = 99; } if (server.hasArg("apiUrl")) { url = server.arg("apiUrl"); if (!url.startsWith("http")) url = "http://" + url; } if (server.hasArg("dst")) { int dstValue = server.arg("dst").toInt(); if (dstValue == -1) { autoDST = true; updateDST(); } else { autoDST = false; daylightOffset_sec = (dstValue == 1) ? 3600 : 0; gmtOffset_sec = 3600; configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); } preferences.putBool("autoDST", autoDST); preferences.putInt("dst", daylightOffset_sec); } if (server.hasArg("timeInterval")) { timeUpdateInterval = server.arg("timeInterval").toInt() * 1000; if (timeUpdateInterval < 1000) timeUpdateInterval = 1000; if (timeUpdateInterval > 60000) timeUpdateInterval = 60000; } if (server.hasArg("weatherInterval")) { weatherUpdateInterval = server.arg("weatherInterval").toInt() * 1000; if (weatherUpdateInterval < 5000) weatherUpdateInterval = 5000; if (weatherUpdateInterval > 3600000) weatherUpdateInterval = 3600000; } if (server.hasArg("switchInterval")) { displaySwitchInterval = server.arg("switchInterval").toInt() * 1000; if (displaySwitchInterval < 1000) displaySwitchInterval = 1000; if (displaySwitchInterval > 60000) displaySwitchInterval = 60000; } if (server.hasArg("brightness")) { displayBrightness = server.arg("brightness").toInt(); if (displayBrightness < 0) displayBrightness = 0; if (displayBrightness > 255) displayBrightness = 255; setLCDBacklight(displayBrightness); } if (server.hasArg("welcomeMsg")) { welcomeMessage = server.arg("welcomeMsg"); if (welcomeMessage.length() > 32) welcomeMessage = welcomeMessage.substring(0, 32); } if (server.hasArg("welcomeDur")) { welcomeDuration = server.arg("welcomeDur").toInt() * 1000; if (welcomeDuration < 1000) welcomeDuration = 1000; if (welcomeDuration > 30000) welcomeDuration = 30000; } if (hasBME280 && server.hasArg("tempOffset")) { // Nur wenn BME280 vorhanden tempOffset = server.arg("tempOffset").toFloat(); if (tempOffset < -5.0) tempOffset = -5.0; if (tempOffset > 5.0) tempOffset = 5.0; } displayConfig.showYear = server.hasArg("showYear"); displayConfig.showTime = server.hasArg("showTime"); displayConfig.showTemp = server.hasArg("showTemp"); displayConfig.showHumid = server.hasArg("showHumid"); if (server.hasArg("datePosX")) displayConfig.datePosX = constrain(server.arg("datePosX").toInt(), 0, 15); if (server.hasArg("datePosY")) displayConfig.datePosY = constrain(server.arg("datePosY").toInt(), 0, 1); if (server.hasArg("timePosX")) displayConfig.timePosX = constrain(server.arg("timePosX").toInt(), 0, 15); if (server.hasArg("timePosY")) displayConfig.timePosY = constrain(server.arg("timePosY").toInt(), 0, 1); if (server.hasArg("tempPosX")) displayConfig.tempPosX = constrain(server.arg("tempPosX").toInt(), 0, 15); if (server.hasArg("tempPosY")) displayConfig.tempPosY = constrain(server.arg("tempPosY").toInt(), 0, 1); if (server.hasArg("humidPosX")) displayConfig.humidPosX = constrain(server.arg("humidPosX").toInt(), 0, 15); if (server.hasArg("humidPosY")) displayConfig.humidPosY = constrain(server.arg("humidPosY").toInt(), 0, 1); displayScheduleActive = server.hasArg("schedActive"); if (server.hasArg("onHour")) displayOnHour = constrain(server.arg("onHour").toInt(), 0, 23); if (server.hasArg("onMinute")) displayOnMinute = constrain(server.arg("onMinute").toInt(), 0, 59); if (server.hasArg("offHour")) displayOffHour = constrain(server.arg("offHour").toInt(), 0, 23); if (server.hasArg("offMinute")) displayOffMinute = constrain(server.arg("offMinute").toInt(), 0, 59); preferences.putString("wifiSSID", wifiSSID); preferences.putString("wifiPassword", wifiPassword); preferences.putInt("stationId", stationId); preferences.putString("apiUrl", url); preferences.putULong("timeInterval", timeUpdateInterval); preferences.putULong("weatherInterval", weatherUpdateInterval); preferences.putULong("switchInterval", displaySwitchInterval); preferences.putInt("brightness", displayBrightness); preferences.putString("welcomeMsg", welcomeMessage); preferences.putULong("welcomeDur", welcomeDuration); if (hasBME280) preferences.putFloat("tempOffset", tempOffset); // Nur speichern, wenn BME280 vorhanden preferences.putBool("showYear", displayConfig.showYear); preferences.putBool("showTime", displayConfig.showTime); preferences.putBool("showTemp", displayConfig.showTemp); preferences.putBool("showHumid", displayConfig.showHumid); preferences.putUChar("datePosX", displayConfig.datePosX); preferences.putUChar("datePosY", displayConfig.datePosY); preferences.putUChar("timePosX", displayConfig.timePosX); preferences.putUChar("timePosY", displayConfig.timePosY); preferences.putUChar("tempPosX", displayConfig.tempPosX); preferences.putUChar("tempPosY", displayConfig.tempPosY); preferences.putUChar("humidPosX", displayConfig.humidPosX); preferences.putUChar("humidPosY", displayConfig.humidPosY); preferences.putBool("schedActive", displayScheduleActive); preferences.putInt("onHour", displayOnHour); preferences.putInt("onMinute", displayOnMinute); preferences.putInt("offHour", displayOffHour); preferences.putInt("offMinute", displayOffMinute); if (wifiChanged) { WiFi.disconnect(true); inFallbackMode = false; reconnectWiFi(); } server.sendHeader("Location", "/"); server.send(303); } void handleUpdateWeather() { updateWeather(); server.sendHeader("Location", "/"); server.send(303); } void handleRestart() { server.sendHeader("Location", "/"); server.send(303); delay(1000); ESP.restart(); } void reconnectWiFi() { if (WiFi.status() != WL_CONNECTED) { lcd.clear(); lcd.print("WLAN verbinden..."); Serial.print("Versuche Verbindung mit SSID: "); Serial.println(wifiSSID); WiFi.disconnect(true); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID.c_str(), wifiPassword.c_str()); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 20) { delay(500); attempts++; Serial.print("."); } Serial.println(); if (WiFi.status() == WL_CONNECTED) { lcd.clear(); lcd.print("WLAN verbunden"); lcd.setCursor(0, 1); lcd.print(WiFi.localIP().toString()); Serial.print("Verbunden mit IP: "); Serial.println(WiFi.localIP()); delay(2000); inFallbackMode = false; } else { lcd.clear(); lcd.print("WLAN Fehler"); Serial.println("Verbindung fehlgeschlagen, starte Fallback AP..."); delay(2000); lcd.clear(); lcd.setCursor(0, 0); lcd.print("Fallback WLAN aktiv"); lcd.setCursor(0, 1); lcd.print("192.168.4.1"); WiFi.disconnect(true); WiFi.mode(WIFI_OFF); delay(1000); WiFi.mode(WIFI_AP); if (WiFi.softAP(fallbackSSID, fallbackPassword)) { IPAddress IP = WiFi.softAPIP(); Serial.print("Fallback AP gestartet, SSID: "); Serial.print(fallbackSSID); Serial.print(", IP: "); Serial.println(IP); } else { lcd.clear(); lcd.print("AP Fehler"); Serial.println("Fehler beim Starten des Fallback AP!"); } inFallbackMode = true; } } } void displayTimeDateWeather(float temp = -999, float humid = -999) { lcd.clear(); struct tm timeinfo; if (!getLocalTime(&timeinfo)) { lcd.setCursor(0, 0); lcd.print("Zeit Fehler"); return; } if (displayConfig.showYear || displayConfig.showTime) { char dateStr[11]; if (displayConfig.showYear) { strftime(dateStr, sizeof(dateStr), "%d.%m.%y", &timeinfo); } else { strftime(dateStr, sizeof(dateStr), "%d.%m.", &timeinfo); } lcd.setCursor(displayConfig.datePosX, displayConfig.datePosY); lcd.print(dateStr); } if (displayConfig.showTime) { char timeStr[6]; strftime(timeStr, sizeof(timeStr), "%H:%M", &timeinfo); lcd.setCursor(displayConfig.timePosX, displayConfig.timePosY); lcd.print(timeStr); } if (temp != -999 && displayConfig.showTemp) { lcd.setCursor(displayConfig.tempPosX, displayConfig.tempPosY); lcd.print("T:"); lcd.print(temp, 1); lcd.write(0xDF); lcd.print("C"); } if (humid != -999 && displayConfig.showHumid) { lcd.setCursor(displayConfig.humidPosX, displayConfig.humidPosY); lcd.print("H:"); lcd.print(humid, 0); lcd.print("%"); } } void displayBME280Data() { lcd.clear(); if (hasBME280) { float temp = bme.readTemperature() + tempOffset; float humid = bme.readHumidity(); lcd.setCursor(0, 0); lcd.print(" Innen:"); lcd.setCursor(0, 1); lcd.print(" T:"); lcd.print(temp, 1); lcd.write(0xDF); lcd.print("C H:"); lcd.print(humid, 0); lcd.print("%"); } else { lcd.setCursor(0, 0); lcd.print("Kein BME280"); lcd.setCursor(0, 1); lcd.print("Nur Aussenwetter"); } } void updateWeather() { if (WiFi.status() == WL_CONNECTED) { HTTPClient http; http.begin(url); int httpCode = http.GET(); if (httpCode > 0) { String payload = http.getString(); DynamicJsonDocument doc(2048); DeserializationError error = deserializeJson(doc, payload); if (error) { lcd.clear(); lcd.setCursor(0, 0); lcd.print("JSON Fehler"); } else { float temp = doc["aktuelle_temperatur"].as(); float humid = doc["aktuelle_luftfeuchtigkeit"].as(); displayTimeDateWeather(temp, humid); lastWeatherUpdateTime = millis(); } } else { lcd.clear(); lcd.setCursor(0, 0); lcd.print("Server Fehler"); } http.end(); } else { lcd.clear(); lcd.setCursor(0, 0); lcd.print("Kein WLAN"); } } int getLastSunday(int year, int month) { struct tm timeinfo = {0}; timeinfo.tm_year = year - 1900; timeinfo.tm_mon = month - 1; timeinfo.tm_mday = 31; mktime(&timeinfo); while (timeinfo.tm_mday > 24) { timeinfo.tm_mday--; mktime(&timeinfo); if (timeinfo.tm_wday == 0) break; } return timeinfo.tm_mday; } void updateDST() { if (!autoDST) return; struct tm timeinfo; if (!getLocalTime(&timeinfo)) { Serial.println("Zeit konnte nicht abgerufen werden"); return; } int lastSundayMarch = getLastSunday(timeinfo.tm_year + 1900, 3); int lastSundayOctober = getLastSunday(timeinfo.tm_year + 1900, 10); bool isDST = false; if (timeinfo.tm_mon > 2 && timeinfo.tm_mon < 9) { isDST = true; } else if (timeinfo.tm_mon == 2 && timeinfo.tm_mday > lastSundayMarch) { isDST = true; } else if (timeinfo.tm_mon == 2 && timeinfo.tm_mday == lastSundayMarch && timeinfo.tm_hour >= 2) { isDST = true; } else if (timeinfo.tm_mon == 9 && timeinfo.tm_mday < lastSundayOctober) { isDST = true; } else if (timeinfo.tm_mon == 9 && timeinfo.tm_mday == lastSundayOctober && timeinfo.tm_hour < 3) { isDST = true; } int newDaylightOffset = isDST ? 3600 : 0; if (newDaylightOffset != daylightOffset_sec) { daylightOffset_sec = newDaylightOffset; gmtOffset_sec = 3600; configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); preferences.putInt("dst", daylightOffset_sec); Serial.println(isDST ? "Sommerzeit aktiviert" : "Winterzeit aktiviert"); } } uint64_t calculateSleepTime() { struct tm timeinfo; if (!getLocalTime(&timeinfo)) return 60000000; int currentMinutes = timeinfo.tm_hour * 60 + timeinfo.tm_min; int onMinutes = displayOnHour * 60 + displayOnMinute; int offMinutes = displayOffHour * 60 + displayOffMinute; uint64_t sleepTimeSeconds; if (onMinutes <= offMinutes) { if (currentMinutes < onMinutes) { sleepTimeSeconds = (onMinutes - currentMinutes) * 60 - timeinfo.tm_sec; } else if (currentMinutes >= offMinutes) { sleepTimeSeconds = ((1440 - currentMinutes) + onMinutes) * 60 - timeinfo.tm_sec; } else { return 0; } } else { if (currentMinutes >= offMinutes && currentMinutes < onMinutes) { sleepTimeSeconds = (onMinutes - currentMinutes) * 60 - timeinfo.tm_sec; } else { return 0; } } return sleepTimeSeconds * 1000000; } void enterDeepSleep() { lcd.clear(); lcd.setCursor(0, 0); lcd.print("Deep Sleep..."); setLCDBacklight(0); WiFi.disconnect(true); WiFi.mode(WIFI_OFF); delay(1000); uint64_t sleepTime = calculateSleepTime(); if (sleepTime > 0) { wasInDeepSleep = true; esp_sleep_enable_timer_wakeup(sleepTime); Serial.println("Enter Deep Sleep für " + String(sleepTime/1000000) + " Sekunden"); esp_deep_sleep_start(); } } void checkDisplaySchedule() { if (!displayScheduleActive) { setLCDBacklight(displayBrightness); return; } struct tm timeinfo; if (!getLocalTime(&timeinfo)) return; int currentMinutes = timeinfo.tm_hour * 60 + timeinfo.tm_min; int onMinutes = displayOnHour * 60 + displayOnMinute; int offMinutes = displayOffHour * 60 + displayOffMinute; if (onMinutes <= offMinutes) { if (currentMinutes >= onMinutes && currentMinutes < offMinutes) { setLCDBacklight(displayBrightness); } else { enterDeepSleep(); } } else { if (currentMinutes >= onMinutes || currentMinutes < offMinutes) { setLCDBacklight(displayBrightness); } else { enterDeepSleep(); } } } void loop() { server.handleClient(); static unsigned long lastTimeUpdate = 0; static unsigned long lastWeatherUpdate = 0; static unsigned long lastDSTCheck = 0; static unsigned long lastScheduleCheck = 0; static unsigned long lastDisplaySwitch = 0; static bool showTimeWeather = true; static float lastTemp = -999; static float lastHumid = -999; if (millis() - lastScheduleCheck >= 30000) { checkDisplaySchedule(); lastScheduleCheck = millis(); } if (inFallbackMode) { lcd.setCursor(0, 0); lcd.print("Fallback WLAN aktiv"); lcd.setCursor(0, 1); lcd.print("192.168.4.1"); } else { if (millis() - lastTimeUpdate >= timeUpdateInterval) { lastTimeUpdate = millis(); } if (millis() - lastWeatherUpdate >= weatherUpdateInterval) { if (WiFi.status() != WL_CONNECTED) { reconnectWiFi(); } else { HTTPClient http; http.begin(url); int httpCode = http.GET(); if (httpCode > 0) { String payload = http.getString(); DynamicJsonDocument doc(2048); DeserializationError error = deserializeJson(doc, payload); if (!error) { lastTemp = doc["aktuelle_temperatur"].as(); lastHumid = doc["aktuelle_luftfeuchtigkeit"].as(); lastWeatherUpdate = millis(); } } http.end(); } } if (millis() - lastDisplaySwitch >= displaySwitchInterval) { if (hasBME280) { showTimeWeather = !showTimeWeather; // Wechsel nur bei BME280 } else { showTimeWeather = true; // Immer Außenwetter ohne BME280 } if (showTimeWeather) { displayTimeDateWeather(lastTemp, lastHumid); } else { displayBME280Data(); } lastDisplaySwitch = millis(); } if (autoDST && millis() - lastDSTCheck >= 60000) { updateDST(); lastDSTCheck = millis(); } } }