Update: 2023-11-13

Esp32 Zeitschaltuhr Dual

Zeitschaltuhr mit NTP Zeitsynchronisation

Automatischer Wechsel zwischen Sommer und Normalzeit

Eins vorweg, ich übernehme keinerlei Verantwortung falls ihr diesen Sketch nutzt um mittels Relais oder SSR Netzspannung zu schalten. Ich stelle bewusst keine Schaltpläne dazu zur Verfügung. Wendet euch dafür an eine Elektrofachkraft mit entsprechender Ausbildung.

Getestet habe ich mit den, bei Arduino-Jüngern, beliebten blauen Relais Modulen, einem Mosfet "IRF3708" und einem Solid State Relais "Fotek SSR-40 DA".

Esp32 Zeitschaltuhr

Login Manager

Der Sketch Zeitschaltuhrdual ist für LOW und HIGH aktive Relais, Solid State Relais oder Mosfet geeignet. Dies muss beim Login eingestellt werden.

Esp32 Zeitschaltuhr

Highlight

Für beide Ausgänge gibt es einen integrierten Betriebsstundenzähler. Die optische Schaltzustandsanzeige ist gleichzeitig der Button zum manuellen Ein-/Ausschalten der Ausgänge.

Esp32 Zeitschaltuhr

Funktionen

Durch Klick/Touch auf die Tabs kannst du zwischen beiden Relais navigieren. Es wird die zugehörige Registerkarte mit den Ein- und Ausschaltzeiten eingeblendet.

Esp32 Zeitschaltuhr

Die Schaltzeiten können mittels Schaltfläche ON/OFF aktiviert oder deaktiviert werden. Ein- und Ausschaltzeiten werden im Flash des Esp32 gespeichert. Eingegebene Zeitperioden werden durch Klick/Touch des Speichern Buttons zum ESP... gesendet. Ein erfolgreiches speichern der Schaltzeiten auf dem Esp32 Webserver wird im Webinterface für 5 Sekunden signalisiert.

Esp32 Zeitschaltuhr

Beim Klick/Touch auf die Uhrzeit wird der dritte Tab mit dem Betriebsstundenzähler angezeigt. Hie kannst den Zähler der einzelnen Ausgänge zurücksetzen. Erneut den Login Manager aufrufen oder alle gespeicherten Daten löschen. Bei beiden zuletzt genannten wird der Esp32 neu gestartet.

Esp32 Zeitschaltuhr

Anleitung zur Inbetriebnahme

Eventuell den NTP Zeitserver und die Zeitzone, für deinen Standort, in der "Lokalzeit.ino" ändern. Anschließend den Sketch hochladen. Es wird ein Access-Point mit dem Namen "EspConfig" erstellt. Verbinde dich mit diesem Netzwerk gib die URL 192.168.4.1 in deinen Browser ein. Anschließend im Webinterface deine Zugangsdaten eingeben und auswählen wie du schalten möchtest. Alle Daten werden im Flash des Esp32 gespeichert.
Im Seriellen Monitor wird die IP des ESP.. angezeigt. Kopiere diese URL in die Adresszeile deines Browsers und verbinde dich mit deinem ESP32.
Falls sich im Spiffs (Speicher) des Esp32 noch keine "spiffs.html" befindet wird ein kleiner Helfer zu deinem Browser gesendet. Mit diesem kannst du die "spiffs.html" und die "style.css" hochladen. Jetzt wird der Spiffs Manager angezeigt, mit dem du noch die "admin.html" , das Favicon und die "index.html" in den Speicher deines Esp... uploaden musst. Klick/Touch nun auf die index.html um zur Zeitschaltuhr zu kommen.

Wer möchte kann sich unten den Code ansehen und selbst in Dateien kopieren oder aber das Ganze als Archiv downloaden.

Esp8266 Favicon ZeitschaltuhrDownload Projekt

Zeitschaltuhrdual32.ino


// ****************************************************************
// Sketch Esp32 Webserver Modular(Tab)
// created: Jens Fleischer, 2018-07-06
// last mod: Jens Fleischer, 2023-11-13
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp32
// Software: Esp32 Arduino Core 1.0.0 - 2.0.14
// Getestet auf: ESP32 NodeMCU-32s
/******************************************************************
  Copyright (c) 2018 Jens Fleischer. All rights reserved.

  This file is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.
  This file is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.
*******************************************************************/
// Der WebServermodular stellt den Haupt Tab dar.
// #include "SPIFFS.h" #include <WebServer.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP32 Webservers ist erforderlich.
// "server.onNotFound()" darf nicht im Setup des ESP32 Webserver stehen.
// Inklusive Arduino OTA-Updates
/**************************************************************************************/

#include <WebServer.h>
#include <ArduinoOTA.h>
#include <SPIFFS.h>
#include "time.h"
#include <Preferences.h>

#define DEBUGGING             // Auskommentieren wenn keine Serielle Ausgabe erforderlich ist

#ifdef DEBUGGING
#define DEBUG(...) Serial.println(__VA_ARGS__)
#define DEBUG_F(...) Serial.printf("Funktion: %s meldet in Zeile: %d -> ", __PRETTY_FUNCTION__, __LINE__); Serial.println(__VA_ARGS__)
#else
#define DEBUG(...)
#define DEBUG_F(...)
#endif

struct tm tm;
Preferences preferences;
WebServer server(80);

void setup() {
  Serial.begin(115200);
  preferences.begin("config", false);
  DEBUG((String)"\nSketchname: " + (__FILE__) + "\nBuild: " + (__TIMESTAMP__) + "\n" );
  setupFS();
  admin();
  Connect();
  setupTime();
  setupSchaltUhr();
  setupHobbsMeter();
  ArduinoOTA.onStart([]() {
    save();                  // vor dem Sketch Update Betriebsstunden speichern
  });
  ArduinoOTA.begin();
  server.begin();
  DEBUG("HTTP Server gestartet\n\n");
}

void loop() {
  ArduinoOTA.handle();
  server.handleClient();
  if (millis() < 0x2FFF || millis() > 0xFFFFF0FF) runtime();
  getLocalTime(&tm);        // Aktuallisiert die Uhrzeit
  dualSchaltuhr();
  reStation();
}

Admin.ino

// ****************************************************************
// Sketch Esp32 Admin Modular(Tab)
// created: Jens Fleischer, 2018-07-05
// last mod: Jens Fleischer, 2023-11-13
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp32
// Software: Esp32 Arduino Core 1.0.0 - 2.0.14
// Geprüft: bei 4MB Flash
// Getestet auf: ESP32 NodeMCU-32s
/******************************************************************
  Copyright (c) 2018 Jens Fleischer. All rights reserved.

  This file is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.
  This file is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Admin sollte als Tab eingebunden werden.
// #include "SPIFFS.h" #include <WebServer.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP32 Webservers ist erforderlich.
// Die Spiffs.ino muss im ESP32 Webserver enthalten sein
// Funktion "admin();" muss im setup() nach spiffs() aber vor dem Verbindungsaufbau aufgerufen werden.
// Die Funktion "runtime();" muss mindestens zweimal innerhalb 49 Tage aufgerufen werden.
// Entweder durch den Client(Webseite) oder zur Sicherheit im "loop();"
/**************************************************************************************/

#include <rom/rtc.h>

const char* const PROGMEM flashChipMode[] = {"QIO", "QOUT", "DIO", "DOUT", "Unbekannt"};
const char* const PROGMEM resetReason[] = {"ERR", "Power on", "Unknown", "Software", "Watch dog", "Deep Sleep", "SLC module", "Timer Group 0", "Timer Group 1",
                                           "RTC Watch dog", "Instrusion", "Time Group CPU", "Software CPU", "RTC Watch dog CPU", "Extern CPU",
                                           "Voltage not stable", "RTC Watch dog RTC"
                                          };

void admin() {                          // Funktionsaufruf "admin();" muss im Setup eingebunden werden
  File file = SPIFFS.open("/config.json");
  if (file) {
    String Hostname = file.readStringUntil('\n');
    if (Hostname != "") {
      WiFi.setHostname(Hostname.substring(2, Hostname.length() - 2).c_str());
      ArduinoOTA.setHostname(WiFi.getHostname());
    }
  }
  file.close();
  server.on("/admin/renew", handlerenew);
  server.on("/admin/once", handleonce);
  server.on("/reconnect", []() {
    server.send(204, "");
    WiFi.reconnect();
  });
  server.on("/restart", []() {
    server.send(204, "");
    //save();          //Einkommentieren wenn Werte vor dem Neustart gesichert werden sollen
    ESP.restart();
  });
}

void handlerenew() {
  server.send(200, "application/json", R"([")" + runtime() + R"(",")" + temperatureRead() + R"(",")" + WiFi.RSSI() + R"("])");     // Json als Array
}

void handleonce() {
  if (server.arg(0) != "") {
    WiFi.setHostname(server.arg(0).c_str());
    File file = SPIFFS.open("/config.json", FILE_WRITE);
    file.printf("[\"%s\"]", WiFi.getHostname());
    file.close();
  }
  String fname = String(__FILE__).substring( 3, String(__FILE__).lastIndexOf ('\\'));
  String temp = R"({"File":")" + fname.substring(fname.lastIndexOf ('\\') + 1, fname.length()) + R"(", "Build":")" +  (String)__DATE__ + " " + (String)__TIME__ +
                R"(", "LocalIP":")" +  WiFi.localIP().toString() + R"(", "Hostname":")" + WiFi.getHostname() + R"(", "SSID":")" + WiFi.SSID() +
                R"(", "GatewayIP":")" +  WiFi.gatewayIP().toString() +  R"(", "Channel":")" +  WiFi.channel() + R"(", "MacAddress":")" +  WiFi.macAddress() +
                R"(", "SubnetMask":")" +  WiFi.subnetMask().toString() + R"(", "BSSID":")" +  WiFi.BSSIDstr() + R"(", "ClientIP":")" + server.client().remoteIP().toString() +
                R"(", "DnsIP":")" + WiFi.dnsIP().toString() + R"(", "Reset1":")" + resetReason[rtc_get_reset_reason(0)] +
                R"(", "Reset2":")" + resetReason[rtc_get_reset_reason(1)] + R"(", "CpuFreqMHz":")" + ESP.getCpuFreqMHz() +
                R"(", "FreeHeap":")" + formatBytes(ESP.getFreeHeap()) + R"(", "ChipSize":")" +  formatBytes(ESP.getFlashChipSize()) +
                R"(", "ChipSpeed":")" + ESP.getFlashChipSpeed() / 1000000 + R"(", "ChipMode":")" + flashChipMode[ESP.getFlashChipMode()] +
                R"(", "IdeVersion":")" + ARDUINO + R"(", "SdkVersion":")" + ESP.getSdkVersion() + R"("})";
  server.send(200, "application/json", temp);     // Json als Objekt
}

String runtime() {
  static uint8_t rolloverCounter = 0;
  static auto letzteMillis = 0;
  auto aktuelleMillis = millis();
  if (aktuelleMillis < letzteMillis) {       // prüft Millis Überlauf
    rolloverCounter++;
    DEBUG_F("Millis Überlauf");
  }
  letzteMillis = aktuelleMillis;
  auto sek = (0xFFFFFFFF / 1000 ) * rolloverCounter + (aktuelleMillis / 1000);
  char buf[20];
  snprintf(buf, sizeof(buf), sek < 86400 || sek > 172800 ? "%ld Tage %02ld:%02ld:%02ld" : "%ld Tag %02ld:%02ld:%02ld", sek / 86400, sek / 3600 % 24, sek / 60 % 60, sek % 60);
  return buf;
}

Connect.ino

// ****************************************************************
// Sketch Esp32 Login Manager Modular(Tab) mit optischer Anzeige
// created: Jens Fleischer, 2018-12-06
// last mod: Jens Fleischer, 2019-01-04
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp32
// Software: Esp32 Arduino Core 1.0.0 - 2.0.14
// Getestet auf: ESP32 NodeMCU-32s
/******************************************************************
  Copyright (c) 2018 Jens Fleischer. All rights reserved.

  This file is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.
  This file is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Login Manager sollte als Tab eingebunden werden.
// #include <Preferences.h> #include <WebServer.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP32 Webservers ist erforderlich.
// Die Funktion "Connect();" muss im Setup eingebunden werden.
// Die Oneboard LED leuchtet im AP Modus dauerhaft.
// Die Funktion "reStation()" sollte im "loop" aufgerufen werden.
/**************************************************************************************/

const char HTML1[] PROGMEM = R"(<!DOCTYPE HTML><html lang="de"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<style>button,input{padding:.5em 2em}body{background:#a9a9a9}</style><title>Login Manager</title></head><body>)";
const char HTML2[] PROGMEM = R"(<h2>SSID Passwort</h2><form action="/" method="post"><p>SSID:<br><input name="ssid" placeholder="Name vom Netzwerk" required>
</label></p><p>Passwort:<br><input name="passwort" pattern="[!-~]{8,64}" placeholder="PW vom Netzwerk" required></label></p><p><p>
Einstellen wie du schalten möchtest</p><input type="radio" id="ml" name="aktiv" value="0" checked><label for="ml">LOW Aktiv</label>
<input type="radio" id="mh" name="aktiv" value="1"><label for="mh">HIGH Aktiv</label></p><button>Absenden</button></form></body></html>)";
const char HTML3[] PROGMEM = "<h3> Ihre Eingaben wurden erfolgreich übertragen. Der Server wird neu gestartet. </h3>";
const char HTML4[] PROGMEM = "<center><h2>Erfolgreich angemeldet!</h2><p>lade die index.html hoch</p></center>";

void Connect() {
  char ssid[33];
  char password[65];
  preferences.begin("config", false);
  preferences.getString("ssid", ssid, sizeof ssid);
  preferences.getString("password", password, sizeof password);
  DEBUG((String)"NVS-SSID: " + ssid);
  DEBUG((String)"NVS-Passwort: " + password);
  uint8_t i = 0;
  WiFi.mode(WIFI_STA);                                   //Stationst-Modus --> Esp32 im Heimnetzwerk einloggen
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {                // Led blinkt während des Verbindungsaufbaus
    pinMode(LED_BUILTIN, OUTPUT);                        // OnBoardLed ESP32 NodeMCU-32s
    digitalWrite(LED_BUILTIN, 1);
    delay(500);
    digitalWrite(LED_BUILTIN, 0);
    delay(500);
    i++;
    Serial.printf(" % i sek\n", i);
    if (WiFi.status() != WL_CONNECTED && i > 20) {       // Ist der Router nicht erreichbar, wird ein eigenes Netzwerk erstellt.
      digitalWrite(LED_BUILTIN, 1);                      // Dauerleuchten der Led zeigt den AP Modus an.
      WiFi.disconnect();
      WiFi.mode(WIFI_AP);                                // Soft-Access-Point-Modus --> Access-Point Adresse http://192.168.4.1/
      DEBUG("\nStarte Soft AP");
      if (WiFi.softAP("EspConfig")) {
        Serial.printf("Verbinde dich mit dem Netzwerk \"EspConfig\"\nGib die IP %s im Browser ein\n\n", WiFi.softAPIP().toString().c_str());
      }
      break;
    }
  }
  if (WiFi.status() == WL_CONNECTED) {
    DEBUG("\nVerbunden mit: " + WiFi.SSID() + "\nEsp32 IP: " + WiFi.localIP().toString() + "\n");
    Serial.printf("\nGib diese URL in deinem Browser ein: %s\n\n", WiFi.localIP().toString().c_str());
  }
  server.on("/", HTTP_GET, handleRoot);
  server.on("/", HTTP_POST, handleConfig);
}

void handleConfig() {
  if (server.hasArg("ssid") && server.hasArg("passwort")) {
    preferences.putString("ssid", server.arg(0));
    preferences.putString("password", server.arg(1));
    preferences.putBool("active", server.arg(2).toInt());
    server.send(200, "text/html", (String)HTML1 + HTML3);
    delay(500);
    save();          // vor dem Neustart Betriebsstunden speichern
    ESP.restart();
  }
}

void handleRoot() {                                            // Html Startseite
  if (WiFi.status() != WL_CONNECTED) {
    server.send(200, "text/html", (String)HTML1 + HTML2);      // besteht keine Verbindung zur Station wird HTML2 gesendet
  }
  else {
    if (!handleFile(server.urlDecode(server.uri()))) {         // index.html aus Spiffs senden   // ohne "spiffs.ino" Zeile auskommentieren
      server.send(200, "text/html", (String)HTML1 + HTML4);    // existiert keine index.html wird HTML4 gesendet
    }                                                                                            // ohne "spiffs.ino" Zeile auskommentieren
  }
}

void reStation() {                                             // der Funktionsaufruf "reStation();" sollte im "loop" stehen
  static unsigned long lastMillis = 0;                         // nach Stromausfall startet der Esp.. schneller als der Router
  unsigned long currentMillis = millis();
  if (currentMillis - lastMillis >= 3e5) {                     // im AP Modus aller 5 Minuten prüfen ob der Router verfügbar ist
    if (WiFi.status() != WL_CONNECTED) Connect();
    lastMillis = currentMillis;
  }
}

Dualschaltuhr.ino

// ****************************************************************
// Sketch Esp32 Dual Zeitschaltuhr Modular(Tab)
// created: Jens Fleischer, 2018-12-06
// last mod: Jens Fleischer, 2019-01-04
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp32, Relais Modul o. Mosfet IRF3708 o. Fotek SSR-40 DA
// für Relais Modul
// GND an GND
// IN1 an T4 = GPIO13
// IN2 an T5 = GPIO12
// VCC an VIN -> je nach verwendeten Esp.. möglich
// Jumper JD-VCC VCC
// alternativ ext. 5V Netzteil verwenden
//
// für Mosfet IRF3708
// Source an GND
// Mosfet1 Gate an T4 = GPIO13
// Mosfet2 Gate an T5 = GPIO12
//
// für 3V Solid State Relais
// GND an GND
// SSR1 Input + an T4 = GPIO13
// SSR2 Input + an T5 = GPIO12
//
// Software: Esp32 Arduino Core 1.0.0 - 2.0.14
// Getestet auf: ESP32 NodeMCU-32s
/******************************************************************
  Copyright (c) 2018 Jens Fleischer. All rights reserved.

  This file is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.
  This file is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Dualschaltuhr sollte als Tab eingebunden werden.
// #include <WebServer.h> & #include <Preferences.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP32 Webservers ist erforderlich.
// Der Lokalzeit Tab ist zum ausführen der Zeitschaltuhr einzubinden.
// Die Funktion "setupSchaltUhr();" muss im Setup aufgerufen werden.
// Zum schalten muss die Funktion "dualSchaltuhr();" im loop(); aufgerufen werden.
/**************************************************************************************/

bool aktiv, relState[2];
const uint8_t relPin[] = {T4, T5};         // Pin für Relais eventuell einstellen
const auto item = 10;
char switchTime[item * 2][6];
uint8_t switchActive[item] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1};

void setupSchaltUhr() {
  aktiv = preferences.getBool("active");
  DEBUG((String)"NVS-aktiv: " + aktiv);
  for (auto pin : relPin) digitalWrite(pin, !aktiv), pinMode(pin, OUTPUT);
  relState[0] = !aktiv;
  relState[1] = !aktiv;
  DEBUG((String)"Relais Aktiv Status: " + (relState[0] ? "LOW" : "HIGH"));
  char buffer[3];
  for (auto i = 0; i < item * 2; i++) {
    itoa (i, buffer, 10);
    preferences.getString(buffer, switchTime[i], sizeof(switchTime[i]));
  }
  preferences.getBytes("switchActive", switchActive, sizeof switchActive);
  server.on("/timer", HTTP_POST, []() {
    if (server.args() == 1) {
      switchActive[server.argName(0).toInt() / 2] = server.arg(0).toInt();
    }
    if (server.hasArg("sz0")) {
      for (auto i = 0; i < server.args(); i++) {
        strcpy (switchTime[i], server.arg(i).c_str());
      }
    }
    char buffer[3];
    for (auto i = 0; i < item * 2; i++) {
      itoa (i, buffer, 10);
      preferences.putString(buffer, switchTime[i]);
    }
    preferences.putBytes("switchActive", switchActive, sizeof switchActive);
    String temp = "[";
    for (auto i = 0; i < item * 2; i++) {
      if (temp != "[") temp += ',';
      temp += (String)R"(")" + switchTime[i] + R"(")";
    }
    for (auto i = 0; i < item; i++) {
      temp += (String)R"(,")" + switchActive[i] + R"(")";
    }
    server.send(200, "application/json", temp += added() + "]");
  });
  server.on("/timer", HTTP_GET, []() {
    if (server.hasArg("tog1")) relState[0] = !relState[0];    // Relais1 manuell schalten
    if (server.hasArg("tog2")) relState[1] = !relState[1];    // Relais2 manuell schalten
    server.send(200, "application/json", (String)R"([")" + localTime() + R"(",")" + (relState[0] ? aktiv : !aktiv) + R"(",")" + (relState[1] ? aktiv : !aktiv) + R"("])");
  });
  server.on("/config", []() {
    if (server.hasArg("delall")) {
      preferences.clear();                                    // alle Daten löschen
      preferences.end();
      server.send(204, "");
      ESP.restart();
    } else {
      server.send(200, "text/html", (String)HTML1 + HTML2);   // Passwort, SSID ändern
    }
  });
}

void dualSchaltuhr() {
  static uint8_t oldmin = 60, oldState[] = {aktiv, aktiv};
  hobbsMeter(relState[0], relState[1]);                       // Betriebsstundenzähler Aufrufen mit Led Status
  if (tm.tm_min != oldmin) {
    oldmin = tm.tm_min;
    char buf[6];
    snprintf(buf, sizeof(buf), "%.2d:%.2d", tm.tm_hour, tm.tm_min);
    Serial.println(buf);
    for (auto i = 0; i < item * 2; i++) {
      if (i < item) {
        if (switchActive[i / 2] && !strcmp(switchTime[i], buf)) i % 2 ? relState[0] = !aktiv : relState[0] = aktiv; // Relais1 nach Zeit schalten
      }
      else {
        if (switchActive[i / 2] && !strcmp(switchTime[i], buf)) i % 2 ? relState[1] = !aktiv : relState[1] = aktiv; // Relais2 nach Zeit schalten
      }
    }
  }
  if (relState[0] != oldState[0] || relState[1] != oldState[1]) {
    for (auto i = 0; i < 2; i++) {
      oldState[i] = relState[i];
      digitalWrite(relPin[i], relState[i]);
      DEBUG(digitalRead(relPin[i]) == aktiv ? (String)"Relais" + (1 + i) + " an" : (String)"Relais" + (1 + i) + " aus");
    }
  }
}

Lokalzeit.ino

// ****************************************************************
// Sketch Esp32 Lokalzeit Modular(Tab)
// created: Jens Fleischer, 2018-07-15
// last mod: Jens Fleischer, 2018-12-29
// ****************************************************************
// Hardware: Esp32
// Software: Esp32 Arduino Core 1.0.0 - 2.0.14
// Getestet auf: ESP32 NodeMCU-32s
/******************************************************************
  Copyright (c) 2018 Jens Fleischer. All rights reserved.

  This file is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.
  This file is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Lokalzeit sollte als Tab eingebunden werden.
// #include <WebServer.h> oder #include <WiFi.h> muss im Haupttab aufgerufen werden
// Funktion "setupTime();" muss im setup() nach dem Verbindungsaufbau aufgerufen werden.
/**************************************************************************************/

const char* const PROGMEM ntpServer[] = {"fritz.box", "de.pool.ntp.org", "at.pool.ntp.org", "ch.pool.ntp.org", "ptbtime1.ptb.de", "europe.pool.ntp.org"};

bool getTime() {
  configTime(0, 0, ntpServer[1]);                       // deinen NTP Server einstellen (von 0 - 5 aus obiger Liste)
  if (!getLocalTime(&tm)) {
    return false;
  } else {
    setenv("TZ", "CET-1CEST,M3.5.0/02,M10.5.0/03", 1);  // https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
    return true;
  }
}

void setupTime() {
  if (!getTime()) {
    DEBUG_F("Zeit konnte nicht geholt werden\n");
  } else {
    getLocalTime(&tm);
    DEBUG_F(&tm, "Programmstart: %A, %B %d %Y %H:%M:%S");
  }
  server.on("/zeit", []() {
    server.send(200, "application/json",  "\"" + (String)localTime() + "\"");
  });
}

char* localTime() {
  static char buf[9];
  static time_t lastsek = 0;
  getLocalTime(&tm);
  static time_t lastday = tm.tm_mday;
  if (tm.tm_sec != lastsek) {
    lastsek = tm.tm_sec;
    strftime (buf, sizeof(buf), "%T", &tm);           // http://www.cplusplus.com/reference/ctime/strftime/
    if (tm.tm_mday != lastday) {
      lastday = tm.tm_mday;
      getTime();
    }
  }
  return buf;
}

Spiffs.ino

// ****************************************************************
// Sketch Esp32 Datei Manager spezifisch sortiert Modular(Tab)
// created: Jens Fleischer, 2023-01-02
// last mod: Jens Fleischer, 2023-03-27
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp32
// Software: Esp32 Arduino Core 1.0.0 - 2.0.14
// Geprüft: mit 4MB Flash
// Getestet auf: ESP32 NodeMCU-32s
/******************************************************************
  Copyright (c) 2020 Jens Fleischer. All rights reserved.

  This file is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.
  This file is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Spiffs sollte als Tab eingebunden werden.
// #include <SPIFFS.h> #include <WebServer.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP32 Webservers ist erforderlich.
// "server.onNotFound()" darf nicht im Setup des ESP32 Webserver stehen.
// Die Funktion "setupFS();" muss im Setup aufgerufen werden.
/**************************************************************************************/

#include <detail/RequestHandlersImpl.h>
#include <list>

const char HELPER[] PROGMEM = R"(<form method="POST" action="/upload" enctype="multipart/form-data">
<input type="file" name="[]" multiple><button>Upload</button></form>Lade die spiffs.html hoch.)";

void setupFS() {                                                                             // Funktionsaufruf "setupFS();" muss im Setup eingebunden werden
  SPIFFS.begin(true);
  server.on("/list", handleList);
  server.on("/format", formatSpiffs);
  server.on("/upload", HTTP_POST, sendResponce, handleFileUpload);
  server.onNotFound([]() {
    if (!handleFile(server.urlDecode(server.uri())))
      server.send(404, "text/plain", "FileNotFound");
  });
}

void handleList() {                                                                         // Senden aller Daten an den Client
  File root = SPIFFS.open("/");
  typedef std::pair<String, int> prop;
  std::list<prop> dirList;                                                                  // Liste anlegen
  while (File f = root.openNextFile()) dirList.emplace_back(f.name(), f.size());            // Liste füllen
  dirList.sort([](const prop & f, const prop & l) {                                         // Liste sortieren
    if (server.arg(0) == "1") {
      return f.second > l.second;
    } else {
      for (uint8_t i = 0; i < 31; i++) {
        if (tolower(f.first[i]) < tolower(l.first[i])) return true;
        else if (tolower(f.first[i]) > tolower(l.first[i])) return false;
      }
      return false;
    }
  });
  String temp = "[";
  for (auto& p : dirList) {
    if (temp != "[") temp += ',';
    temp += "{\"name\":\"" + (p.first.startsWith("/") ? p.first.substring(1) : p.first) + "\",\"size\":\"" + formatBytes(p.second) + "\"}";
  }
  temp += R"(,{"usedBytes":")" + formatBytes(SPIFFS.usedBytes() * 1.05) + R"(",)" +         // Berechnet den verwendeten Speicherplatz + 5% Sicherheitsaufschlag
          R"("totalBytes":")" + formatBytes(SPIFFS.totalBytes()) + R"(","freeBytes":")" +   // Zeigt die Größe des Speichers
          (SPIFFS.totalBytes() - (SPIFFS.usedBytes() * 1.05)) + R"("}])";                   // Berechnet den freien Speicherplatz + 5% Sicherheitsaufschlag
  server.send(200, "application/json", temp);
}

bool handleFile(String&& path) {
  if (server.hasArg("delete")) {
    SPIFFS.remove(server.arg("delete"));                                                    // Datei löschen
    sendResponce();
    return true;
  }
  if (!SPIFFS.exists("/spiffs.html"))server.send(200, "text/html", HELPER);                 // ermöglicht das hochladen der spiffs.html
  if (path.endsWith("/")) path += "index.html";
  return SPIFFS.exists(path) ? ({File f = SPIFFS.open(path); server.streamFile(f, StaticRequestHandler::getContentType(path)); f.close(); true;}) : false;
}

void handleFileUpload() {                                  // Dateien vom Rechnenknecht oder Klingelkasten ins SPIFFS schreiben
  static File fsUploadFile;
  HTTPUpload& upload = server.upload();
  if (upload.status == UPLOAD_FILE_START) {
    if (upload.filename.length() > 30) {
      upload.filename = upload.filename.substring(upload.filename.length() - 30, upload.filename.length());  // Dateinamen auf 30 Zeichen kürzen
    }
    DEBUG("FileUpload Name: " + upload.filename);
    fsUploadFile = SPIFFS.open("/" + server.urlDecode(upload.filename), "w");
  } else if (upload.status == UPLOAD_FILE_WRITE) {
    DEBUG("FileUpload Data: " + (String)upload.currentSize);
    if (fsUploadFile)
      fsUploadFile.write(upload.buf, upload.currentSize);
  } else if (upload.status == UPLOAD_FILE_END) {
    if (fsUploadFile)
      fsUploadFile.close();
    DEBUG("FileUpload Size: " + (String)upload.totalSize);
  }
}

void formatSpiffs() {       //Formatiert den Speicher
  SPIFFS.format();
  sendResponce();
}

void sendResponce() {
  server.sendHeader("Location", "spiffs.html");
  server.send(303, "message/http");
}

const String formatBytes(size_t const& bytes) {            // lesbare Anzeige der Speichergrößen
  return bytes < 1024 ? static_cast<String>(bytes) + " Byte" : bytes < 1048576 ? static_cast<String>(bytes / 1024.0) + " KB" : static_cast<String>(bytes / 1048576.0) + " MB";
}

bool freeSpace(uint16_t const& printsize) {               // Funktion um beim speichern in Logdateien zu prüfen ob noch genügend freier Platz verfügbar ist.
  DEBUG_F(formatBytes(SPIFFS.totalBytes() - (SPIFFS.usedBytes() * 1.05)) + " im Spiffs frei");
  return (SPIFFS.totalBytes() - (SPIFFS.usedBytes() * 1.05) > printsize) ? true : false;
}

Stundenzaehler.ino

// ****************************************************************
// Sketch Esp32 Dual Betriebsstundenzähler Modular(Tab)
// created: Jens Fleischer, 2018-12-06
// last mod: Jens Fleischer, 2023-11-13
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp32
// Software: Esp32 Arduino Core 1.0.0 - 2.0.14
// Getestet auf: ESP32 NodeMCU-32s
/******************************************************************
  Copyright (c) 2018 Jens Fleischer. All rights reserved.

  This file is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.
  This file is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Betriebsstundenzähler sollte als Tab eingebunden werden.
// #include <Preferences.h> muss im Haupttab aufgerufen werden
// Die Funktionalität des ESP32 Webservers ist erforderlich.
// Die Funktion "setupHobbsMeter();" muss im Setup aufgerufen werden.
/**************************************************************************************/
uint32_t totalmin[2];

void setupHobbsMeter() {
  totalmin[0] = preferences.getULong("HourMeter1");              // Betriebstunden(minuten) lesen
  totalmin[1] = preferences.getULong("HourMeter2");
  server.on("/hourmeter", HTTP_GET, []() {                       // Betriebstunden(minuten) zurücksetzen
    if (server.hasArg("hm1")) {
      preferences.putULong("HourMeter1", (totalmin[0] = 0));
    }
    if (server.hasArg("hm2")) {
      preferences.putULong("HourMeter2", (totalmin[1] = 0));
    }
    server.send(204, "");
  });
}

void hobbsMeter(bool &state_0, bool &state_1) {                  // Aufrufen mit Led Status
  static uint32_t lastMillis[2], lastmin[2] {0, 0};
  auto currentMillis = millis();
  if (currentMillis - lastMillis[0] >= 6e4) {
    lastMillis[0] = currentMillis;
    if (state_0 == aktiv) totalmin[0]++;                         // Betriebstundenzähler Relais 1 wird um eine Minute erhöht
    if (state_1 == aktiv) totalmin[1]++;                         // Betriebstundenzähler Relais 2 wird um eine Minute erhöht
  }
  if (currentMillis - lastMillis[1] >= 1728e5 && (totalmin[0] != lastmin[0] || totalmin[1] != lastmin[1])) {   // bei Änderungen aller 2 Tage Betriebsstunden speichern
    lastMillis[1] = currentMillis;
    lastmin[0] = totalmin[0];
    lastmin[1] = totalmin[1];
    save();
  }
}

String added() {
  return R"(,")" + operatingTime(totalmin[0]) + R"(",")" +  operatingTime(totalmin[1]) + R"(")";
}

String operatingTime(uint32_t &tmin) {                           // Betriebstunden(minuten) formatieren
  char buf[11];
  snprintf(buf, sizeof(buf), "%d,%d", tmin / 60, tmin / 6 % 10);
  return buf;
}

void save() {                                                    // Betriebstunden(minuten) speichern
  preferences.putULong("HourMeter1", totalmin[0]);
  preferences.putULong("HourMeter2", totalmin[1]);
}

admin.html

<!DOCTYPE HTML> <!-- For more information visit: https://fipsok.de -->
<html lang="de">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="style32.css">
    <title>ESP32 Admin</title>
    <SCRIPT>
      window.addEventListener('load', () => {
        document.querySelector('#spiff').addEventListener('click', () => {
          window.location = '/spiffs.html';
        });
        document.querySelector('#home').addEventListener('click', () => {
          window.location = '/';
        });
        document.querySelector('#restart').addEventListener('click', () => {
          if (confirm('Bist du sicher!')) re('restart');
        });
        document.querySelector('#reconnect').addEventListener('click', re.bind(this, 'reconnect'));
        document.querySelector('#hostbutton').addEventListener('click', check.bind(this, document.querySelector('input')));
        once();
        var output = document.querySelector('#note');
        function once(arg1, arg2) {
          fetch('/admin/once', {
            method: 'POST',
            body: arg1
          }).then( resp => {
            return resp.json();
          }).then( obj => {
            output.innerHTML = '';
            output.classList.remove('note');
            document.querySelector('form').reset();
            if (arg1 == undefined) wait = window.setInterval(renew, 1000);
            if (arg2 == 'reconnect') re(arg2);
            document.querySelector('#file').innerHTML = obj['File'];
            document.querySelector('#build').innerHTML = obj['Build'];
            document.querySelector('#local').innerHTML = obj['LocalIP'];
            document.querySelector('#host').innerHTML = obj['Hostname'];
            document.querySelector('#ssid').innerHTML = obj['SSID'];
            document.querySelector('#gateway').innerHTML = obj['GatewayIP'];
            document.querySelector('#kanal').innerHTML = obj['Channel'];
            document.querySelector('#mac').innerHTML = obj['MacAddress'];
            document.querySelector('#subnet').innerHTML = obj['SubnetMask'];
            document.querySelector('#bss').innerHTML = obj['BSSID'];
            document.querySelector('#client').innerHTML = obj['ClientIP'];
            document.querySelector('#dns').innerHTML = obj['DnsIP'];
            document.querySelector('#reset1').innerHTML = obj['Reset1'];
            document.querySelector('#reset2').innerHTML = obj['Reset2'];
            document.querySelector('#cpufreq').innerHTML = obj['CpuFreqMHz'];
            document.querySelector('#freeheap').innerHTML = obj['FreeHeap'];
            document.querySelector('#csize').innerHTML = obj['ChipSize'];
            document.querySelector('#cspeed').innerHTML = obj['ChipSpeed'];
            document.querySelector('#cmode').innerHTML = obj['ChipMode'];
			document.querySelector('#ide').innerHTML = obj['IdeVersion'].replace(/(\d)(\d)(\d)(\d)/,obj['IdeVersion'][3]!=0 ? '$1.$3.$4' : '$1.$3.');
            document.querySelector('#sdk').innerHTML = obj['SdkVersion'];
          }).catch(function (err) {
            re();
          });
        }
        function renew() {
          fetch('admin/renew').then( resp => {
            return resp.json();
          }).then( array => {
            document.querySelector('#runtime').innerHTML = array[0];
            document.querySelector('#temp').innerHTML = array[1];
            document.querySelector('#rssi').innerHTML = array[2];
          });
        }
        function check(inObj) {
          !inObj.checkValidity() ? (output.innerHTML = inObj.validationMessage, output.classList.add('note')) : (once(inObj.value, 'reconnect'));
        }
        function re(arg) {
          window.clearInterval(wait);
          fetch(arg);
          output.classList.add('note');
          if (arg == 'restart') {
            output.innerHTML = 'Der Server wird neu gestartet. Die Daten werden in 10 Sekunden neu geladen.';
            setTimeout(once, 10000);
          } else if (arg == 'reconnect') {
            output.innerHTML = 'Die WiFi Verbindung wird neu gestartet. Daten werden in 5 Sekunden neu geladen.';
            setTimeout(once, 5000);
          } else {
            output.innerHTML = 'Es ist ein Verbindungfehler aufgetreten. Es wird versucht neu zu verbinden.';
            setTimeout(once, 2000);
          }
        }
      });
    </SCRIPT>
  </head>
  <body>
    <h1>ESP32 Admin Page</h1>
    <main>
      <section id="left">
        <span>Runtime ESP:</span>
        <span>WiFi RSSI:</span>
        <span>CPU Temperatur:</span>
        <span>Sketch Name:</span>
        <span>Sketch Build:</span>
        <span>IP address:</span>
        <span>Hostname:</span>
        <span>Connected to:</span>
        <span>Gateway IP:</span>
        <span>Channel:</span>
        <span>MacAddress:</span>
        <span>SubnetMask:</span>
        <span>BSSID:</span>	  
        <span>Client IP:</span>
        <span>DnsIP:</span>
        <span>Reset CPU 1:</span>
        <span>Reset CPU 2:</span>
        <span>CPU Freq:</span>
        <span>FreeHeap:</span>
        <span>FlashSize:</span>
        <span>FlashSpeed:</span>
        <span>FlashMode:</span>
		<span>Arduino IDE Version:</span>
        <span>SDK-Version:</span>
      </section>
      <section>
        <data id="runtime">00:00:00</data>
        <div>
          <data id="rssi"></data>
          dBm
        </div>
        <div>
          <data id="temp"></data>
          °C
        </div>
        <data id="file">?</data>
        <data id="build">0</data>
        <data id="local">0</data>
        <data id="host">?</data>
        <data id="ssid">?</data>
        <data id="gateway">0</data>
        <data id="kanal">0</data>
        <data id="mac">0</data>
        <data id="subnet">0</data>
        <data id="bss">0</data>
        <data id="client">0</data>
        <data id="dns">0</data>
        <data id="reset1">0</data>
        <data id="reset2">0</data>
        <div>
          <data id="cpufreq"></data>
          MHz
        </div>
        <data id="freeheap">0</data>
        <data id="csize">0</data>
        <div>
          <data id="cspeed"></data>
          MHz
        </div>
        <data id="cmode">0</data>
		<data id="ide">0</data>
        <data id="sdk">0</data>
      </section>
    </main>
    <div>
      <button class="button" id="spiff">Spiffs</button>
      <button class="button" id="home">Startseite</button>
    </div>
    <div id="note"></div>
    <div>
      <form><input placeholder=" neuer Hostname" pattern="([A-Za-z0-9\-]{1,32})" title="Es dürfen nur Buchstaben 
	    (a-z, A-Z), Ziffern (0-9) und Bindestriche (-) enthalten sein. Maximal 32 Zeichen" required>
        <button class="button" type="button" id="hostbutton">Name Senden</button>
      </form>
    </div>
    <div>
      <button class="button" id="reconnect">WiFi Reconnect</button>
      <button class="button" id="restart">ESP Restart</button>
    </div>
  </body>
</html>
index.html

<!DOCTYPE HTML> <!-- For more information visit: https://fipsok.de -->
<html lang="de">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="style32.css">
    <title>Dual Zeitschaltuhr</title>
    <style>
	  div > [id^=del] {
	  margin: .5em;
	  cursor: pointer;
	  }
	  body {
      color: #15dfdf;
	  }
      div {
      display: flex;
      }
      span {
      padding: 0.5em;
      }
      #tog1,#tog2 {
      padding: .7em 1.2em;
      cursor: pointer;
      }
      time {
      text-shadow: 2px 2px 2px black;
      font-size: 1.3em;
      font-weight: bold;
      margin: auto;
      }
      input {
      height: auto;
      font-weight: bold;
      }
      .note:after {
      content: "Schaltzeiten gespeichert";
      }
      form {
      box-shadow: 5px 3px 10px #4e4d4d;
	  min-height: 274px;
      }
      svg {
      height: 4em;
      }
      #bu {
      background-color: #333;
	  color: #33cccc;
	  border: outset #555;
      }
      .tab {
      overflow: hidden;
      }
      .tab button {
      background-color: #999;
	  color: #33cccc;
      border: none;
	  margin-top: 0em;
      transition: 0.8s;
      border-top-right-radius: .5em;
      border-top-left-radius: .5em;
      }
      .tab button:hover {
      background-color: #666;
      }
      .tab button.active {
      background-color: #333;
      }
	  #tab3 {
	  background-color: inherit;
      box-shadow: none;
	  }
	  #ctab3 {
	  padding: 1em 0 0 2em;
	  }
      .tabcontent {
      display: none;
      padding: .5em .7em .5em .1em;
      background-color: #333;
	  min-height: 258px;
      }
      .tabcontent input {
      background-color: inherit;
      font-size: 3em;
	  color: inherit;
	  border: solid #555;
      }
	  .tabcontent [name^=bu] {
      width: 2em;
	  cursor: pointer;
	  }
	  .none {
	  color: #777;
	  }
      @media only screen and (max-width: 600px) {
      .tabcontent input,input {
      font-size: 2em;
      width: auto;
      border: none;
	  }
	  .tab button,#bu,#tog1,#tog2 {
	  width: auto;
	  }
	  svg {
	  height: 3em;
	  }
      }	
    </style>
    <script>      
      var item = 0x14;
	  var d = document;
      d.addEventListener('DOMContentLoaded', () => {
        dom(), renew();
        d.getElementById('bu').addEventListener('click', () => {
          send(new FormData(d.querySelector('form')));
        });
		d.getElementById('delpw').addEventListener('click',  () => {
          window.location = '/config';
        });
		d.getElementById('delall').addEventListener('click', config.bind(this, 'config?delall='));
		for (var i = 1; i < 4; i++) {
          d.getElementById(`tab${i}`).addEventListener('click', openTab);
		  if (i< 3) {
		    d.getElementById(`tog${i}`).addEventListener('click', renew);
		    d.getElementById(`delhm${i}`).addEventListener('click', config.bind(this, `hourmeter?hm${i}=`));
		  }
        }
		for (var i = 0; i < item - 1; i = i + 2) {
		  d.querySelector(`[name=bu${i}]`).addEventListener('click', active);
		}
      },send(), setInterval(renew, 1000));
	  function dom() {
        var buf = '';
        for (var i = 0; i < item; i++) {
          buf += `${i%2 ? `<span> -- </span>` : `<div id='ak${i}'><span name='bu${i}'></span>`}<input type='time' name='sz${i}' value=''>${i%2 ? '</div>' : ''}`;
          if (i == item / 2 - 1) {
            d.getElementById('ctab1').insertAdjacentHTML('beforeend', buf);
            buf = '';
          }
          if (i == item - 1) d.getElementById('ctab2').insertAdjacentHTML('beforeend', buf);
        }
      }
	  function active() {
	    let formData = new FormData()
        formData.append(this.parentNode.id.substr(2, 4), this.textContent == 'ON' ? '0' : '1');
	    send(formData);
      }
      function send(arg) {
        fetch('/timer', {
          method: 'POST',
          body: arg
        }).then(response => {
          if(arg && arg.has('sz0'))  {
            var el = d.getElementById('out');
            el.classList.add('note');
            setTimeout(() => {
              el.classList.remove('note');
            }, 5000);
          }
          return response.json();
        }).then(array => {
          array.forEach(function (x, i) {
		    if (i < item) {
			  d.querySelector(`[name=sz${i}]`).value = x;
			} else if (i < item + item/2) {
			  d.querySelector(`[name=bu${(i-item)*2}]`).textContent = (x%2 ? 'ON' : 'OFF');
			  var el = d.getElementById(`ak${(i-item)*2}`);
              x%2 ? el.classList.remove('none') : el.classList.add('none');
			} else {
			  d.getElementById(`hm${i%30}`).innerHTML = x;
			}
          });
        });
      }
      function openTab(event) {
        for (var i = 1; i < 4; i++) {
          d.getElementById(`ctab${i}`).style.display = 'none';
          d.getElementById(`tab${i}`).classList.remove('active');
        }
        d.getElementById('c' + event.currentTarget.id).style.display = 'block';
        event.target.classList.add('active')
      }
	  function config(arg) {
	    if (confirm('Bist du sicher!')) {
		  fetch(`/${arg}`).then(response => {
          if (arg != 'config?delall=')send();
          });
	    }
	  }
      function renew(event) {
        if (event) event = event.currentTarget.id;
        fetch(`timer?${event}=`).then(response => {
          return response.json();
        }).then(array => {
          d.querySelector('time').innerHTML = array[0];
		  for (var i = 1; i < array.length; i++) {
		    d.getElementById(`color${i}`).style.fill=array[i] == 0 ? '' : '#ff0';
            d.getElementById(`on${i}`).style.visibility=array[i] == 0 ? 'hidden' : 'visible';
		  }
        });
      }
    </script>
  </head>
  <body>
    <h2>Zeitschaltuhr</h2>
	<main>
      <div class="tab">
        <button class="button active" id="tab1">&#9203; Relais 1</button>
        <button class="button" id="tab2">&#9203; Relais 2</button>
		<button class="button" id="tab3"><time>00:00:00</time></button>
      </div>
      <form>
        <div id="ctab1" class="tabcontent" style="display: block;">
        </div>
        <div id="ctab2" class="tabcontent">
        </div>
		<div id="ctab3" class="tabcontent">
		  <p>
		    <span>Hour meter device 1</span><span id="hm0"></span>h
		  <br>
		    <span>Hour meter device 2</span><span id="hm1"></span>h
		  </p>
		  <div>
	        <span id="delhm1">&#10060; Hour Meter 1 zurücksetzen</span>
		  </div>
		  <span id="delhm2">&#10060; Hour Meter 2 zurücksetzen</span>
		  <div>
	        <span id="delpw">&#127757; Passwort/SSID/Schaltstatus ändern</span>
		  </div>
		  <span id="delall">&#10060; Alle Daten löschen</span>
        </div>
      </form>
	</main>
    <div>
      <button class="button" id="bu">&#9200; Speichern</button>
      <div id="tog1">
        R1
        <svg viewBox="0 0 486 486">
          <g id="color1">
            <use xlink:href="#body"/>
            <use id="on1" xlink:href="#beam"/>
		  </g>	
        </svg>
      </div>
      <div id="tog2">
        R2
        <svg viewBox="0 0 486 486">
		<g id="color2">
          <use xlink:href="#body"/>
          <use id="on2" xlink:href="#beam"/>
		  </g>
        </svg>
      </div>
    </div>
    <p id="out"></p>
	<svg style="display: none;">
      <path id="body" d="m256.5 160.8c0-7.4-6-13.5-13.5-13.5-47.6 0-86.4 38.7-86.4 86.4 0 7.4 6 13.5 13.5 13.5 7.4 0 13.5-6 13.5-13.5 0-32.8 26.7-59.4 59.4-59.4 7.5 0 13.5-6 13.5-13.5zm106.2 72.5c0 32.3-12.8 61.6-33.6 83.1-15.8 16.4-26 37.3-29.4 59.6-1.5 9.6-9.8 16.7-19.6 16.7h-74.3c-9.7 0-18.1-7-19.5-16.6-3.5-22.3-13.8-43.5-29.6-59.8-20.4-21.2-33.1-50-33.4-81.7-0.7-66.6 52.3-120.5 118.9-121 66.5-0.5 120.5 53.3 120.5 119.7zm-64.3 191.4v14.2c0 11.3-8.3 20.7-19.1 22.3l-3.5 12.9c-1.9 7-8.2 11.9-15.5 11.9h-34.7c-7.3 0-13.6-4.9-15.5-11.9l-3.4-12.9c-10.9-1.7-19.2-11-19.2-22.4v-14.2c0-7.6 6.1-13.7 13.7-13.7h83.5c7.6 0.1 13.7 6.2 13.7 13.8z"/>
      <path id="beam" d="m376.57 341.98c-5.3-5.3-13.8-5.3-19.1 0s-5.3 13.8 0 19.1l33.5 33.5c2.6 2.6 6.1 3.9 9.5 3.9s6.9-1.3 9.5-3.9c5.3-5.3 5.3-13.8 0-19.1zm-262.8-224.8c2.6 2.6 6.1 3.9 9.5 3.9s6.9-1.3 9.5-3.9c5.3-5.3 5.3-13.8 0-19.1l-33.5-33.5c-5.3-5.3-13.8-5.3-19.1 0s-5.3 13.8 0 19.1zm253.3 4c3.4 0 6.9-1.3 9.5-3.9l33.5-33.5c5.3-5.3 5.3-13.8 0-19.1s-13.8-5.3-19.1 0l-33.5 33.5c-5.3 5.3-5.3 13.8 0 19.1 2.7 2.6 6.1 3.9 9.6 3.9zm-253.3 220.8-33.5 33.5c-5.3 5.3-5.3 13.8 0 19.1 2.6 2.6 6.1 3.9 9.5 3.9s6.9-1.3 9.5-3.9l33.5-33.5c5.3-5.3 5.3-13.8 0-19.1-5.2-5.3-13.8-5.3-19 0zm351.1-125.9h-47.3c-7.4 0-13.5 6-13.5 13.5 0 7.4 6 13.5 13.5 13.5h47.3c7.4 0 13.5-6 13.5-13.5 0-7.4-6-13.5-13.5-13.5zm-378.6 13.5c0-7.4-6-13.5-13.5-13.5h-47.3c-7.4 0-13.5 6-13.5 13.5 0 7.4 6 13.5 13.5 13.5h47.3c7.5 0 13.5-6 13.5-13.5zm158.9-158.9c7.4 0 13.5-6 13.5-13.5v-47.3c0-7.4-6-13.5-13.5-13.5s-13.5 6-13.5 13.5v47.3c0 7.5 6.1 13.5 13.5 13.5z"/>
    </svg>
  </body>
</html>
spiffs.html

<!DOCTYPE HTML> <!-- For more information visit: https://fipsok.de -->
<html lang="de">
   <head>
      <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="stylesheet" href="style32.css">
      <title>Esp32 Datei Manager</title>
      <script>
         document.addEventListener('DOMContentLoaded', () => {
		   let span = document.querySelector('span');
           let main = document.querySelector('main');
		   let elem = document.querySelectorAll('input');
           fetch('json').then(function (response) {
             return response.json();
           }).then(function (json) {
             for (var i = 0; i < json.length - 1; i++) {
               let dir = `<li><a href ="${json[i].name}">${json[i].name}</a><small> ${json[i].size}</small>`;
			   dir += `<a href ="${json[i].name}"download="${json[i].name}"> Download </a>or <a href ="${json[i].name}?delete=/${json[i].name}">Delete </a>`;
               main.insertAdjacentHTML('beforeend', dir);
             }
             main.insertAdjacentHTML('beforeend', `<li><b>SPIFFS</b> belegt ${json[i].usedBytes} von ${json[i].totalBytes}`);
			 document.querySelectorAll('a:last-child').forEach((node) => {
			   node.addEventListener('click', () => {
	             if (!confirm('Bist du sicher!')) event.preventDefault();
	           });
		     });
			 free = json[i].freeBytes;
           });
		   elem[0].addEventListener('change', () => {
			 let nBytes = elem[0].files[0].size, output = `${nBytes} Byte`;
             for (var aMultiples = [
               ' KB',
               ' MB'
               ], i = 0, nApprox = nBytes / 1024; nApprox > 1; nApprox /= 1024, i++) {
               output = nApprox.toFixed(2) + aMultiples[i];
             }
             if (nBytes > free) {
               span.innerHTML = `<li><small> Dateigröße: ${output}</small><strong style="color: red;"> Ungenügend Speicher frei </strong></li>`;
               elem[1].setAttribute('disabled', 'disabled');
             } 
             else {
               span.innerHTML = `<li><b>Dateigröße:</b> ${output}</li>`;
               elem[1].removeAttribute('disabled');
             }
		   });
		   elem[2].addEventListener('click', () => {
	         if (!confirm(`Wirklich formatieren? Alle Daten gehen verloren.\nDu musst anschließend spiffs.html wieder laden.`)) event.preventDefault();
	       }); 
         });
      </script>
   </head>
   <body>
      <h2>ESP32 Datei Manager</h2>
      <form action="/upload" method="POST" enctype="multipart/form-data"><input type="file" name="upload">
         <input type="submit" value="Upload" disabled>
      </form>
      <div>
         <span></span>
         <main></main>
      </div>
      <form action="/format" method="POST"><input type="submit" value="Format SPIFFS"></form>
   </body>
</html>
style32.css

/*For more information visit: https://fipsok.de*/
body {
font-family: sans-serif;
background-color: #a9a9a9;
display: flex;
flex-flow: column;
align-items: center;
}
h1,h2 {
color: #e1e1e1;
text-shadow: 2px 2px 2px black;
}
li {
background-color: #7cfc00;
list-style-type: none;
margin-bottom: 10px;
padding-right: 5px;
border-top: 3px solid #7cfc00;
border-bottom: 3px solid #7cfc00;
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.7);
}
li a:first-child, li b {
background-color: #ff4500;
font-weight: bold;
color: white;
text-decoration: none;
padding: 0 5px;
border-top: 3.2px solid #ff4500;
border-bottom: 3.6px solid #ff4500;
text-shadow: 2px 2px 1px black;
cursor:pointer;
}
input {
height: 35px;
font-size: 13px;
}
h1+main {
display: flex;
}
section {
display: flex;
flex-direction: column;
padding: 0.2em;
}
#left {
align-items: flex-end;
text-shadow: 1px 1px 2px #757474;
}
.note {
background-color: salmon;
padding: 0.5em;
margin-top: 1em;
text-align: center;
max-width: 320px;
border-radius: 0.5em;
}
.button {
width: 130px;
height: 40px;
font-size: 16px;
margin-top: 1em;
cursor: pointer;
background-color: #adff2f;
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.7);
}
[type=submit] {
height: 40px;
min-width: 70px;
}
[value^=Format] {
background-color: #ddd;
}
[title] {
background-color: silver;
font-size: 16px;
width: 125px;
}
15 Kommentare - Kommentar eintragen
Peter ❘ 04.06.2020
Wie ergänze ich weiter ausgänge?

Antwort:
Mit "pinMode(pin, OUTPUT);", oder verstehe ich deine Frage falsch?

Gruß Fips
Hans ✪ ✪ ✪ 23.04.2020
Moin,
erst einmal echt gutes Script!
Nur mal eine Frage am Rande, kann man das Script auch für mehrere Relais ausweiten und wie?

Hans

Antwort:
Kann man machen, wenn es erforderlich ist.
Mit C++, Html, CSS und Javascript.

Gruß Fips
Olaf ✪ ✪ ✪ 04.03.2020
Moin Fips,

Genau das brauche ich für eine Heuraufe zum Steuern von Jalousien. Ich komme aber nicht über den Punkt, die spiffs.html hochzuladen. Was mache ich falsch? Gruss Olaf

Antwort:
Hast du eine Größe für den Spiffs in der IDE eingestellt?

Gruß Fips
Manfred ✪ ✪ ✪ 20.01.2020
Danke für das Projekt, genau das was ich gesucht habe, leider läst sich das Projekt nicht compalieren, es fehlen definitionen.
LED_BUILTIN
Connect()
wo definierst du GPIO 12 und 13

Viele Grüße
Manfred

Antwort:
LED_BUILTIN ist definiert wenn du in der IDE unter Werkzeuge das richtige Board einstellst. Oder du weist LED_BUILTIN selber einen GPIO zu.

Dualschaltuhr.ino
const uint8_t relPin[] = {T4, T5};

Gruß Fips
Volker ✪ ✪ ✪ 28.09.2019
Sehr gutes Projekt.

Wie kann ich Dich mal kontaktieren.
Kannst Du Dir vorstellen, bei der Entweicklung eines Lernmoduls für Schüler mit zu helfen?

Viele Grüße
Volker

Antwort:
Nein, tut mir leid!
Ich bin kein Programmierer, ausserdem lässt es die knappe Freizeit nicht zu.

Gruß Fips
Kommentar eintragen

*