Update: 2021-01-03

Sonoff Dual zur Steuerung der Aussenbeleuchtung mittels 10W und 30W Led Strahler.

Download Projekt

SonoffDual_2021.ino

// ****************************************************************
// Sketch Esp8266 Webserver Modular(Tab)
// created: Jens Fleischer, 2018-05-16
// last mod: Jens Fleischer, 2020-12-26
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.7.2 - 2.7.4
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Switch, Sonoff Dual
/******************************************************************
  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 WebServer Tab ist der Haupt Tab mit "setup" und "loop".
// #include <LittleFS.h> #include <ESP8266WebServer.h> müssen im Haupttab aufgerufen werden.
// Die Funktionalität des ESP8266 Webservers ist erforderlich.
// "server.onNotFound()" darf nicht im Setup des ESP8266 Webserver stehen.
// Inklusive Arduino OTA-Updates (Erfordert freien Flash-Speicher)
/**************************************************************************************/

#include <ESP8266WebServer.h>
#include <ArduinoOTA.h>     // https://arduino-esp8266.readthedocs.io/en/latest/ota_updates/readme.html
#include <LittleFS.h>
#include <time.h>

struct tm tm;

ESP8266WebServer server(80);

const String sketchName() {                                    // Dateiname für den Admin Tab ab EspCoreVersion 2.6.0
  char file[sizeof(__FILE__)] = __FILE__;
  char * pos = strchr(file, '.'); *pos = '\0';
  return file;
}

void setup() {
  Serial.begin(19200);                                         // Sonoff Dual braucht diese Baudrate für die Relais
  setupFS();
  connectWifi();
  admin();
  setupTime();
  sonoffDual();
  setupTimerSwitch();
  setupHobbsMeter();
  ArduinoOTA.begin();
  server.begin();
}

void loop() {
  ArduinoOTA.handle();
  server.handleClient();
  if (millis() < 0x2FFF || millis() > 0xFFFFF0FF) runtime();   // Die Funktion "runtime()" wird nur für den Admin Tab gebraucht.
  static bool once {0};
  if (millis() > 54e4 && !once++) connectWifi();               // Wifi Verbindung einmalig nach 9 Minuten neu starten für Stromausfall.
  static uint32_t previousMillis;
  uint32_t currentMillis {millis()};
  if (currentMillis - previousMillis >= 100) {
    previousMillis = currentMillis;
    localTime();
    dualTimerSwitch();
  }
}

Admin.ino

// ****************************************************************
// Sketch Esp8266 Admin Modular(Tab)
// created: Jens Fleischer, 2019-12-17
// last mod: Jens Fleischer, 2020-12-21
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.6.1 - 2.7.4
// Geprüft: von 1MB bis 16MB Flash
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Switch, Sonoff Dual
/******************************************************************
  Copyright (c) 2019 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 <LittleFS.h> #include <ESP8266WebServer.h> müssen im Haupttab aufgerufen werden.
// Die LittleFS.ino muss im ESP8266 Webserver enthalten sein.
// Funktion "admin();" muss im setup() nach setupFS() und 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();"
// Die Funktion "sketchName()" muss im Haupttab stehen.
/**************************************************************************************/

const char* const PROGMEM flashChipMode[] = {"QIO", "QOUT", "DIO", "DOUT", "Unbekannt"};

void admin() {                          // Funktionsaufruf "admin();" muss im Setup eingebunden werden
  File file = LittleFS.open("/config.json", "r");
  if (file) {
    String newhostname = file.readStringUntil('\n');
    if (newhostname != "") {
      WiFi.hostname(newhostname.substring(1, newhostname.length() - 1));
      file.close();
      ArduinoOTA.setHostname(WiFi.hostname().c_str());
    }
  }
  server.on("/admin/renew", handlerenew);
  server.on("/admin/once", handleonce);
  server.on("/reconnect", []() {
    server.send(304, "message/http");
    WiFi.reconnect();
  });
  server.on("/restart", []() {
    server.send(304, "message/http");
    save();      //Wenn Werte vor dem Neustart gespeichert werden sollen
    ESP.restart();
  });
}

//Es kann entweder die Spannung am ADC-Pin oder die Modulversorgungsspannung (VCC) ausgegeben werden.
/*
void handlerenew() {    // Um die am ADC-Pin anliegende externe Spannung zu lesen, verwende analogRead (A0)
  server.send(200, "application/json", "[\"" + runtime() + "\",\"" + WiFi.RSSI() + "\",\"" + analogRead(A0) + "\"]");     // Json als Array
}
*/
ADC_MODE(ADC_VCC);
void handlerenew() {   // Zum Lesen der Modulversorgungsspannung (VCC), verwende ESP.getVcc()
  server.send(200, "application/json", "[\"" + runtime() + "\",\"" + WiFi.RSSI() + "\",\"" + ESP.getVcc() / 1000 + "," + ESP.getVcc() % 1000 / 10 + " V" + "\"]");
}

void handleonce() {
  if (server.arg(0) != "") {
    WiFi.hostname(server.arg(0));
    File f = LittleFS.open("/config.json", "w");                    // Datei zum schreiben öffnen
    f.printf("\"%s\"\n", WiFi.hostname().c_str());
    f.close();
  }
  String temp = "{\"File\":\"" + sketchName() + "\", \"Build\":\"" + __DATE__ + " " + __TIME__ + "\", \"SketchSize\":\"" + formatBytes(ESP.getSketchSize()) +
                "\", \"SketchSpace\":\"" + formatBytes(ESP.getFreeSketchSpace()) + "\", \"LocalIP\":\"" +  WiFi.localIP().toString() +
                "\", \"Hostname\":\"" + WiFi.hostname() + "\", \"SSID\":\"" + WiFi.SSID() + "\", \"GatewayIP\":\"" +  WiFi.gatewayIP().toString() +
                "\", \"Channel\":\"" +  WiFi.channel() + "\", \"MacAddress\":\"" +  WiFi.macAddress() + "\", \"SubnetMask\":\"" +  WiFi.subnetMask().toString() +
                "\", \"BSSID\":\"" +  WiFi.BSSIDstr() + "\", \"ClientIP\":\"" + server.client().remoteIP().toString() + "\", \"DnsIP\":\"" + WiFi.dnsIP().toString() +
                "\", \"ResetReason\":\"" + ESP.getResetReason() + "\", \"CpuFreqMHz\":\"" + F_CPU / 1000000 + "\", \"FreeHeap\":\"" + formatBytes(ESP.getFreeHeap()) +
                "\", \"HeapFrag\":\"" + ESP.getHeapFragmentation() + "\", \"ChipSize\":\"" +  formatBytes(ESP.getFlashChipSize()) +
                "\", \"ChipSpeed\":\"" + ESP.getFlashChipSpeed() / 1000000 + "\", \"ChipMode\":\"" + flashChipMode[ESP.getFlashChipMode()] +
                "\", \"IdeVersion\":\"" + ARDUINO + "\", \"CoreVersion\":\"" + ESP.getCoreVersion() + "\", \"SdkVersion\":\"" + ESP.getSdkVersion() +  extra() + "\"}";
  server.send(200, "application/json", temp);     // Json als Objekt
}

const String runtime() {
  static uint8_t rolloverCounter;
  static uint32_t previousMillis;
  uint32_t currentMillis {millis()};
  if (currentMillis < previousMillis) rolloverCounter++;       // prüft millis() auf Überlauf
  previousMillis = currentMillis;
  uint32_t sec {(0xFFFFFFFF / 1000) * rolloverCounter + (currentMillis / 1000)};
  char buf[20];
  snprintf(buf, sizeof(buf), "%*.d %.*s %02d:%02d:%02d",
           sec < 86400 ? 0 : 1, sec / 86400, sec < 86400 ? 0 : sec >= 172800 ? 4 : 3, "Tage", sec / 3600 % 24, sec / 60 % 60, sec % 60);
  return buf;
}

Connect.ino

// ****************************************************************
// Sketch Sonoff Dual Connect Modular(Tab) mit optischer Anzeige
// created: Jens Fleischer, 2018-06-16
// last mod: Jens Fleischer, 2020-12-21
// ****************************************************************
// Hardware: Sonoff Dual
// Software: Esp8266 Arduino Core 2.4.2 - 2.7.4
// Getestet auf: Sonoff Dual
/******************************************************************
  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 Connect sollte als Tab eingebunden werden.
// #include <ESP8266WebServer.h> muss im Haupttab aufgerufen werden
// Die Funktionalität des ESP8266 Webservers ist erforderlich.
// Die Funktion "connectWifi();" muss im Setup eingebunden werden.
// Dieser Connect Tab ist speziell für das Sonoff Dual.
/**************************************************************************************/

void connectWifi() {                                 // Funktionsaufruf "connectWifi();" muss im Setup eingebunden werden.
  const char* ssid = "Netzwerkname";                 // Darf bis zu 32 Zeichen haben.
  const char* password = "PasswortvomNetzwerk";      // Mindestens 8 Zeichen jedoch nicht länger als 64 Zeichen.
  constexpr uint8_t LEDPIN = 13;                     // Oneboard Led Sonoff Dual (blue)
  uint8_t i = 0;
  pinMode(LEDPIN, OUTPUT);
  WiFi.persistent(false);                            // Auskommentieren wenn Netzwerkname und Passwort in den Flash geschrieben werden sollen.
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(50);
    digitalWrite(LEDPIN, millis() % 500 >= 250);
    if (++i > 240) ESP.restart();
  }
  digitalWrite(LEDPIN, HIGH);
}

Dualschaltuhr.ino

// ****************************************************************
// Sketch Esp8266 Zeitschaltuhr Dual Modular(Tab)
// created: Jens Fleischer, 2019-03-08
// last mod: Jens Fleischer, 2020-12-21
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266, Relais Modul o. Mosfet IRF3708 o. Fotek SSR-40 DA
// für Relais Modul
// GND an GND
// IN1 an D5 = GPIO14
// IN2 an D6 = 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 D5 = GPIO14
// Mosfet2 Gate an D6 = GPIO12
//
// für 3V Solid State Relais
// GND an GND
// SSR1 Input + an D5 = GPIO14
// SSR2 Input + an D6 = GPIO12
//
// Software: Esp8266 Arduino Core 2.4.2 / 2.5.2 / 2.6.3 / 2.7.4
// Getestet auf: Nodemcu, Wemos D1 Mini Pro
/******************************************************************
  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 Wochenzeitschaltuhr sollte als Tab eingebunden werden.
// #include <LittleFS.h> #include <ESP8266WebServer.h> müssen im Haupttab aufgerufen werden.
// Die Funktionalität des ESP8266 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.
/**************************************************************************************/

constexpr auto ACTIVE = HIGH;                    // LOW für LOW aktive Relais oder HIGH für HIGH aktive (zB. SSR, Mosfet) einstellen
constexpr auto COUNT = 12;                      // Anzahl Schaltzeiten (analog Html Dokument) einstellen 2 bis 40
char switchTime[COUNT * 2][6];
byte switchActive[COUNT];
byte wday[COUNT];
bool relState[] = {!ACTIVE, !ACTIVE};

void setupTimerSwitch() {
  File file = LittleFS.open(existFolder("Config") + "/dtime.dat", "r");
  if (file) {                                                          // Einlesen aller Daten falls die Datei im LittleFS vorhanden ist.
    file.read(switchActive, sizeof(switchActive));
    file.read(wday, sizeof(wday));
    while (file.read() != '\n');
    for (auto i = 0; i < COUNT * 2; i++) {
      file.readBytesUntil('\n', switchTime[i], sizeof(switchTime[i]));
    }
    file.close();
  } else {                                                             // Sollte die Datei nicht existieren
    for (auto i = 0; i < COUNT; i++) {
      switchActive[i] = 1;                                             // werden alle Schaltzeiten
      wday[i] = ~wday[i];                                              // und alle Wochentage aktiviert.
    }
  }
  server.on("/timer", HTTP_POST, []() {
    if (server.args() == 1) {
      switchActive[server.argName(0).toInt()] = server.arg(0).toInt();
      toSave();
      String temp = "\"";
      for (auto i = 0; i < COUNT; i++) {
        temp += switchActive[i];
      }
      temp += "\"";
      server.send(200, "application/json", temp);
    }
    if (server.hasArg("sTime")) {
      char str[COUNT * 14];
      strcpy (str, server.arg("sTime").c_str());
      char* ptr = strtok(str, ",");
      byte i = 0, j = 0;
      while (ptr != NULL) {
        strcpy (switchTime[i++], ptr);
        ptr = strtok(NULL, ",");
      }
      if (server.arg("sDay")) {
        i = 0;
        strcpy (str, server.arg("sDay").c_str());
        char* ptr = strtok(str, ",");
        while (ptr != NULL) {
          wday[i++] = atoi(ptr);
          ptr = strtok(NULL, ",");
        }
        toSave();
      }
      else {
        server.send(400, "");
      }
    }
    String temp = "[";
    for (auto i = 0; i < COUNT * 2; i++) {
      if (temp != "[") temp += ',';
      temp += (String)"\"" + switchTime[i] + "\"";
    }
    temp += ",\"";
    for (auto i = 0; i < COUNT; i++) {
      temp += switchActive[i];
    }
    for (auto& elem : wday) {
      temp += "\",\"";
      temp += elem;
    }
    temp += "\"]";
    server.send(200, "application/json", temp);
  });
  server.on("/timer", HTTP_GET, []() {
    if (server.hasArg("tog") && server.arg(0) == "0") {
      relState[0] = !relState[0];                                      // Relais1 Status manuell ändern
      timeDataLogger(0, server.client().remoteIP().toString());        // Funktionsaufruf Zeitdatenlogger
    }
    if (server.hasArg("tog") && server.arg(0) == "1") {
      relState[1] = !relState[1];                                      // Relais2 Status manuell ändern
      timeDataLogger(1, server.client().remoteIP().toString());        // Funktionsaufruf Zeitdatenlogger
    }
    server.send(200, "application/json", "[\"" + static_cast<String>(relState[0] == ACTIVE) + "\",\"" + static_cast<String>(relState[1] == ACTIVE) + "\",\"" + localTime() + "\"]");
  });
}

void toSave() {
  File file = LittleFS.open(existFolder("Config") + "/dtime.dat", "w");
  if (file) {
    file.write(switchActive, sizeof(switchActive));
    file.write(wday, sizeof(wday));
    for (auto i = 0; i < COUNT * 2; i++) {
      file.printf("\n%s", switchTime[i]);
    }
    file.close();
  }
}

void dualTimerSwitch() {
  static uint8_t lastmin {60}, lastState[] {ACTIVE, ACTIVE};
  hobbsMeter(relState[0], relState[1]);                                // Funktionsaufruf Betriebsstundenzähler mit Relais Status
  if (tm.tm_min != lastmin) {
    lastmin = tm.tm_min;
    char buf[6];
    snprintf(buf, sizeof(buf), "%.2d:%.2d", tm.tm_hour, tm.tm_min);
    for (auto i = 0; i < COUNT * 2; i++) {
      if (switchActive[i / 2] && !strcmp(switchTime[i], buf)) {
        if (wday[i / 2] & (1 << (tm.tm_wday ? tm.tm_wday - 1 : 6))) {
          i < (COUNT % 2 ? COUNT + 1 : COUNT) ? ({relState[0] = i % 2 ? !ACTIVE : ACTIVE; timeDataLogger(0, "Programm");}) : ({relState[1] = i % 2 ? !ACTIVE : ACTIVE; timeDataLogger(1, "Programm");});  // Relais Status nach Zeit ändern
        }
      }
    }
  }
  if (relState[0] != lastState[0] || relState[1] != lastState[1]) {    // Relais schalten wenn sich der Status geändert hat
    for (auto i = 0; i < 2; i++) {
      lastState[i] = relState[i];
    }
    sonoffRelay_1();
    sonoffRelay_2();
  }
}

LittleFS.ino

// ****************************************************************
// Sketch Esp8266 Filesystem Manager spezifisch sortiert Modular(Tab)
// created: Jens Fleischer, 2020-06-08
// last mod: Jens Fleischer, 2020-10-31
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.7.0 - 2.7.4
// Geprüft: von 1MB bis 2MB Flash
// Getestet auf: Nodemcu
/******************************************************************
  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 LittleFS sollte als Tab eingebunden werden.
// #include <LittleFS.h> #include <ESP8266WebServer.h> müssen im Haupttab aufgerufen werden.
// Die Funktionalität des ESP8266 Webservers ist erforderlich.
// "server.onNotFound()" darf nicht im Setup des ESP8266 Webserver stehen.
// Die Funktion "setupFS();" muss im Setup aufgerufen werden.
/**************************************************************************************/

#include <list>
#include <tuple>

const char WARNING[] PROGMEM = R"(<h2>Der Sketch wurde mit "FS:none" kompilliert!)";
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 fs.html hoch.)";

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

bool handleList() {                                                                    // Senden aller Daten an den Client
  FSInfo fs_info;  LittleFS.info(fs_info);                                             // Füllt FSInfo Struktur mit Informationen über das Dateisystem
  Dir dir = LittleFS.openDir("/");
  using namespace std;
  using records = tuple<String, String, int>;
  list<records> dirList;
  while (dir.next()) {                                                                 // Ordner und Dateien zur Liste hinzufügen
    if (dir.isDirectory()) {
      uint8_t ran {0};
      Dir fold = LittleFS.openDir(dir.fileName());
      while (fold.next())  {
        ran++;
        dirList.emplace_back(dir.fileName(), fold.fileName(), fold.fileSize());
      }
      if (!ran) dirList.emplace_back(dir.fileName(), "", 0);
    }
    else {
      dirList.emplace_back("", dir.fileName(), dir.fileSize());
    }
  }
  dirList.sort([](const records & f, const records & l) {                              // Dateien sortieren
    if (server.arg(0) == "1") {
      return get<2>(f) > get<2>(l);
    } else {
      for (uint8_t i = 0; i < 31; i++) {
        if (tolower(get<1>(f)[i]) < tolower(get<1>(l)[i])) return true;
        else if (tolower(get<1>(f)[i]) > tolower(get<1>(l)[i])) return false;
      }
      return false;
    }
  });
  dirList.sort([](const records & f, const records & l) {                              // Ordner sortieren
    if (get<0>(f)[0] != 0x00 || get<0>(l)[0] != 0x00) {
      for (uint8_t i = 0; i < 31; i++) {
        if (tolower(get<0>(f)[i]) < tolower(get<0>(l)[i])) return true;
        else if (tolower(get<0>(f)[i]) > tolower(get<0>(l)[i])) return false;
      }
    }
    return false;
  });
  String temp = "[";
  for (auto& t : dirList) {
    if (temp != "[") temp += ',';
    temp += "{\"folder\":\"" + get<0>(t) + "\",\"name\":\"" + get<1>(t) + "\",\"size\":\"" + formatBytes(get<2>(t)) + "\"}";
  }
  temp += ",{\"usedBytes\":\"" + formatBytes(fs_info.usedBytes) +                      // Berechnet den verwendeten Speicherplatz
          "\",\"totalBytes\":\"" + formatBytes(fs_info.totalBytes) +                   // Zeigt die Größe des Speichers
          "\",\"freeBytes\":\"" + (fs_info.totalBytes - fs_info.usedBytes) + "\"}]";   // Berechnet den freien Speicherplatz
  server.send(200, "application/json", temp);
  return true;
}

void deleteRecursive(const String &path) {
  if (LittleFS.remove(path)) {
    LittleFS.open(path.substring(0, path.lastIndexOf('/')) + "/", "w");
    return;
  }
  Dir dir = LittleFS.openDir(path);
  while (dir.next()) {
    deleteRecursive(path + '/' + dir.fileName());
  }
  LittleFS.rmdir(path);
}

bool handleFile(String &&path) {
  if (server.hasArg("new")) {
    String folderName {server.arg("new")};
    for (auto& c : {34, 37, 38, 47, 58, 59, 92}) for (auto& e : folderName) if (e == c) e = 95;    // Ersetzen der nicht erlaubten Zeichen
    LittleFS.mkdir(folderName);
  }
  if (server.hasArg("sort")) return handleList();
  if (server.hasArg("delete")) {
    deleteRecursive(server.arg("delete"));
    sendResponce();
    return true;
  }
  if (!LittleFS.exists("fs.html")) server.send(200, "text/html", LittleFS.begin() ? HELPER : WARNING);     // ermöglicht das hochladen der fs.html
  if (path.endsWith("/")) path += "index.html";
  return LittleFS.exists(path) ? ({File f = LittleFS.open(path, "r"); server.streamFile(f, mime::getContentType(path)); f.close(); true;}) : false;
}

void handleUpload() {                                                                  // Dateien ins Filesystem schreiben
  static File fsUploadFile;
  HTTPUpload& upload = server.upload();
  if (upload.status == UPLOAD_FILE_START) {
    if (upload.filename.length() > 31) {  // Dateinamen kürzen
      upload.filename = upload.filename.substring(upload.filename.length() - 31, upload.filename.length());
    }
    printf(PSTR("handleFileUpload Name: /%s\n"), upload.filename.c_str());
    fsUploadFile = LittleFS.open(server.arg(0) + "/" + server.urlDecode(upload.filename), "w");
  } else if (upload.status == UPLOAD_FILE_WRITE) {
    printf(PSTR("handleFileUpload Data: %u\n"), upload.currentSize);
    fsUploadFile.write(upload.buf, upload.currentSize);
  } else if (upload.status == UPLOAD_FILE_END) {
    printf(PSTR("handleFileUpload Size: %u\n"), upload.totalSize);
    fsUploadFile.close();
  }
}

void formatFS() {                                                                      // Formatiert das Filesystem
  LittleFS.format();
  sendResponce();
}

void sendResponce() {
  server.sendHeader("Location", "fs.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";
}

String existFolder( String const& foldername) {
  if (!LittleFS.exists(foldername)) LittleFS.mkdir(foldername);
  return foldername;
}

Lokalzeit.ino

// ****************************************************************
// Sketch Esp8266 SNTP Lokalzeit Modular(Tab)
// created: Jens Fleischer, 2020-10-10
// last mod: Jens Fleischer, 2020-12-26
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.7.2 - 2.7.4
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Switch, Sonoff Dual
/******************************************************************
  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 Lokalzeit sollte als Tab eingebunden werden.
// #include <ESP8266WebServer.h> oder #include <ESP8266WiFi.h> muss im Haupttab aufgerufen werden.
// Funktion "setupTime();" muss im setup() nach dem Verbindungsaufbau aufgerufen werden.
// Automatische Umstellung zwischen Sommer- und Normalzeit anhand der Zeitzone.
/**************************************************************************************/

constexpr uint32_t SYNC_INTERVAL = 12;                          // NTP Sync Interval in Stunden einstellen

const char* const PROGMEM MONTH_SHORT[] = {"Jan", "Feb", "Mrz", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"};

void setupTime() {                            // Es ist kein NTP Server erforderlich wenn der Timestamp vom DHCP-Server(Fritz Box) geholt werden kann.
  setTZ("CET-1CEST,M3.5.0/02,M10.5.0/03");    // Zeitzone einstellen https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
}

uint32_t sntp_update_delay_MS_rfc_not_less_than_15000() {       // SNTP Update Intervall einstellen
  return SYNC_INTERVAL * 36e5;
}

char* localTime() {
  static char buf[9];                                           // je nach Format von "strftime" eventuell anpassen
  static time_t previous;
  time_t now = time(&now);
  if (now != previous) {
    previous = now;
    localtime_r(&now, &tm);
    strftime (buf, sizeof(buf), "%T", &tm);                     // http://www.cplusplus.com/reference/ctime/strftime/
  }
  return buf;
}

SonoffDuo.ino

// ****************************************************************
// Sketch Esp8266 Sonoff Dual Modular(Tab)
// created: Jens Fleischer, 2018-06-15
// last mod: Jens Fleischer, 2020-12-26
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Sonoff Dual
// Software: Esp8266 Arduino Core 2.7.2 - 2.7.4
// Getestet auf: Sonoff Dual
/******************************************************************
  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 Sonoff Dual sollte als Tab eingebunden werden.
// #include <ESP8266WebServer.h> muss im Haupttab aufgerufen werden.
// Die Funktionalität des ESP8266 Webservers ist erforderlich.
// Die Funktion "sonoffDual();" muss im Setup aufgerufen werden.
/**************************************************************************************/

void sonoffDual() {
  server.on("/status", []() {                                   // sendet den Schaltzustand an die Webseite
    server.enableCORS(true);
    server.send(200, "application/json", "[\"" + static_cast<String>(relState[0]) +  "\",\"" + static_cast<String>(relState[1]) + "\"]");
  });
  server.on("/hand10W", []() {                                  // Taster vom Sketch Heizung
    relState[0] = !relState[0];
    timeDataLogger(0, server.client().remoteIP().toString());   // Funktionsaufruf Zeitdatenlogger
    server.send(200, "text/plain", "10W OK Heizung");
  });
  server.on("/hand30W", []() {                                  // Taster vom Sketch Heizung
    relState[1] = !relState[1];
    timeDataLogger(1, server.client().remoteIP().toString());   // Funktionsaufruf Zeitdatenlogger
    server.send(200, "text/plain", "30W OK Heizung");
  });
  ArduinoOTA.onStart([]() {
    save();                                                     // Betriebstunden(Minuten) speichern
    Serial.write(0xA0);
    Serial.write(0x04);
    Serial.write(0x00);                                         // beide Relais ausschalten
    Serial.write(0xA1);
    Serial.write("\n");
    Serial.flush();
  });
}

void sonoffRelay_1() {
  Serial.write(0xA0);
  Serial.write(0x04);
  if (relState[0]) {
    relState[1] ? Serial.write(0x03) : Serial.write(0x01);
  }
  else {
    relState[1] ? Serial.write(0x06) : Serial.write(0x00);
  }
  Serial.write(0xA1);
  Serial.write("\n");
  Serial.flush();
}

void sonoffRelay_2() {
  Serial.write(0xA0);
  Serial.write(0x04);
  if (relState[1]) {
    relState[0] ? Serial.write(0x03) : Serial.write(0x02);
  }
  else {
    relState[0] ? Serial.write(0x05) : Serial.write(0x00);
  }
  Serial.write(0xA1);
  Serial.write("\n");
  Serial.flush();
}

Stundenzaehler.ino

// ****************************************************************
// Sketch Esp8266 Betriebsstundenzähler Dual Modular(Tab)
// created: Jens Fleischer, 2018-11-25
// last mod: Jens Fleischer, 2020-12-21
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.4.2 / 2.5.2 / 2.6.3 / 2.7.4
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Dual
/******************************************************************
  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 <LittleFS.h> muss im Haupttab aufgerufen werden.
// Die Funktion "setupHobbsMeter();" muss im Setup aufgerufen werden.
// Der Admin Tab ist zum ausführen erforderlich.
/**************************************************************************************/

uint32_t totalmin[2], lastmin[2];

void setupHobbsMeter() {                                                   // Betriebstunden(minuten) beim Neustart einlesen
  File file = LittleFS.open(existFolder("Config") + "/hobbs.json", "r");
  if (file) {
    char buf[file.size()];
    file.readBytes(buf, sizeof buf);
    lastmin[0] = totalmin[0] = atoi(strtok(buf, "{\":runtimeOne"));
    lastmin[1] = totalmin[1] = atoi(strtok(NULL, "\":,runtimeTwo"));
    file.close();
  }
}

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

String extra() {                                                           // Betriebsstundenanzeige admin.html
  return "\", \"HourMeter1\":\"" + operatingTime(totalmin[0]) + "\", \"HourMeter2\":\"" +  operatingTime(totalmin[1]);
}

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

void save() {
  File file = LittleFS.open(existFolder("Config") + "/hobbs.json", "w");   // Betriebstunden(minuten) speichern
  if (file) {
    file.printf(R"({"runtimeOne":"%u","runtimeTwo":"%u"})", totalmin[0], totalmin[1]);
    file.close();
  }
}

Zeitlogger.ino

// ****************************************************************
// Sketch Esp8266 Zeitdatenlogger Dual Modular(Tab)
// created: Jens Fleischer, 2019-03-31
// last mod: Jens Fleischer, 2020-12-21
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.4.2 / 2.5.2 / 2.6.3 / 2.7.4
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Dual
/******************************************************************
  Copyright (c) 2019 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 Ereignisdatenspeicher sollte als Tab eingebunden werden.
// #include <LittleFS.h> muss im Haupttab aufgerufen werden.
// Der Lokalzeit Tab ist zum ausführen des Zeitdatenlogger erforderlich.
// Der LittleFS Tab ist zum ausführen erforderlich.
/**************************************************************************************/

void timeDataLogger(const uint8_t num, const String customer) {              // Relais Nummer und Ereignisauslöser
  static bool lastrelState[] {!ACTIVE, !ACTIVE};
  if (relState[num] != lastrelState[num]) {                                  // Prüft ob sich der Relais Status geändert hat.
    char fileName[17];
    snprintf(fileName, sizeof(fileName), "/Relais%d_%s.csv", 1 + num, MONTH_SHORT[tm.tm_mon]);
    if (!LittleFS.exists(existFolder("Data") + fileName)) {                  // Logdatei für den aktuellen Monat anlegen falls nicht vorhanden.
      File f = LittleFS.open(existFolder("Data") + fileName, "a");
      if (f) {
        f.printf("%s;An;Initiator;Aus;Initiator\n", MONTH_SHORT[tm.tm_mon]); // Kopfzeile schreiben
      }
      f.close();
      char path[23];
      snprintf(path, sizeof(path), "/Relais%d_%s.csv", 1 + num, MONTH_SHORT[(tm.tm_mon + 1) % 12]);
      LittleFS.remove(path);                                                 // Löscht die elf Monate alte Logdatei.
    }
    File f = LittleFS.open(existFolder("Data") + fileName, "a");             // Die Ereignisdaten für den aktuellen Monat speichern
    if (f) {
      relState[num] == ACTIVE ? f.printf("%d.;%s;%s;", tm.tm_mday, localTime(), customer.c_str()) : f.printf("%s;%s;\n", localTime(), customer.c_str());
    }
    f.close();
  }
  lastrelState[num] = relState[num];                                         // Ersetzt den letzten Status durch den aktuellen Relais Status.
}

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">
	<link rel="stylesheet" href="style.css">
	<title>ESP8266 Admin</title>
	<style>
	  #add {
		display: flex;
		flex-flow: column;
		align-items: flex-end;
		width: max-content;
	  }
	</style>
	<script>
	  addEventListener('load', () => {
		renew(), once();
		let output = document.querySelector('#note');
		let btn = document.querySelectorAll('button');
		let span = document.querySelectorAll('#right span'); 
		btn[0].addEventListener('click', () => {
		  location = '/fs.html';
		});
		btn[1].addEventListener('click', () => {
		  location = '/';
		});
		btn[2].addEventListener('click', check.bind(this, document.querySelector('input')));
		btn[3].addEventListener('click', re.bind(this, 'reconnect'));
		btn[4].addEventListener('click', () => {
		  if (confirm('Bist du sicher!')) re('restart');
		});
		async function once(val = '',arg) {
		  try {
			let resp = await fetch('/admin/once', { method: 'POST', body: val});
			let obj = await resp.json();
			output.innerHTML = '';
			output.classList.remove('note');
			document.querySelector('form').reset();
			if (val.length == 0) myIv = setInterval(renew, 1000);
			if (arg == 'reconnect') re(arg);
			span[3].innerHTML = obj['File'];
			span[4].innerHTML = obj['Build'];
			span[5].innerHTML = obj['SketchSize'];
			span[6].innerHTML = obj['SketchSpace'];
			span[7].innerHTML = obj['LocalIP'];
			span[8].innerHTML = obj['Hostname'];
			span[9].innerHTML = obj['SSID'];
			span[10].innerHTML = obj['GatewayIP'];
			span[11].innerHTML = obj['Channel'];
			span[12].innerHTML = obj['MacAddress'];
			span[13].innerHTML = obj['SubnetMask'];
			span[14].innerHTML = obj['BSSID'];
			span[15].innerHTML = obj['ClientIP'];
			span[16].innerHTML = obj['DnsIP'];
			span[17].innerHTML = obj['ResetReason'];
			span[18].innerHTML = obj['CpuFreqMHz'] + " MHz";
			span[19].innerHTML = obj['FreeHeap'];
			span[20].innerHTML = obj['HeapFrag'] + "%";
			span[21].innerHTML = obj['ChipSize'];
			span[22].innerHTML = obj['ChipSpeed'] + " MHz";
			span[23].innerHTML = obj['ChipMode'];
			span[24].innerHTML = obj['IdeVersion'].replace(/(\d)(\d)(\d)(\d)/,obj['IdeVersion'][3]!=0 ? '$1.$3.$4' : '$1.$3.');
			span[25].innerHTML = obj['CoreVersion'].replace(/_/g,'.');
			span[26].innerHTML = obj['SdkVersion'];
			span[27].innerHTML = obj['HourMeter1'] + " h";
			span[28].innerHTML = obj['HourMeter2'] + " h";
		  } catch(err) {
			re();
		  }  
        }
		async function renew() {
		  const resp = await fetch('admin/renew');
		  const array = await resp.json();
		  array.forEach((v, i) => {span[i].innerHTML = v});
		}
		function check(inObj) {
		  !inObj.checkValidity() ? (output.innerHTML = inObj.validationMessage, output.classList.add('note')) : (once(inObj.value, 'reconnect'));
		}
		function re(arg = '') {
		  clearInterval(myIv);
		  fetch(arg);
		  output.classList.add('note');
		  if (arg == 'restart') {
			output.innerHTML = 'Der Server wird neu gestartet. Die Daten werden in 15 Sekunden neu geladen.';
			setTimeout(once, 15000);
		  } 
		  else if (arg == 'reconnect'){
			output.innerHTML = 'Die WiFi Verbindung wird neu gestartet. Daten werden in 10 Sekunden neu geladen.';
			setTimeout(once, 10000);
		  }
		  else {
			output.innerHTML = 'Es ist ein Verbindungfehler aufgetreten. Es wird versucht neu zu verbinden.';
			setTimeout(once, 3000);
		  }
		}
	  });
	</script>
  </head>
  <body>
	<h1>ESP8266 Admin Page</h1>
	<main>
	  <aside id="left">
		<span>Runtime ESP:</span>
		<span>WiFi RSSI:</span>
		<span>ADC/VCC:</span>
		<span>Sketch Name:</span>
		<span>Sketch Build:</span>
		<span>SketchSize:</span>
		<span>FreeSketchSpace:</span>
		<span>IPv4 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 Ground:</span>
		<span>CPU Freq:</span>
		<span>FreeHeap:</span>
		<span>Heap Fragmentation:</span>
		<span>FlashSize:</span>
		<span>FlashSpeed:</span>
		<span>FlashMode:</span>
		<span>Arduino IDE Version:</span>
		<span>Esp Core Version:</span>
		<span>SDK Version:</span>
		<span>Hour Meter 1:</span>
		<span>Hour Meter 2:</span>
	  </aside>
	  <aside id="right">
		<span>0</span>
		<div>
		  <span></span>
		  dBm
		</div>
		<span>0</span>
		<span>?</span>
		<span>0</span>
		<span>0</span>
		<span>0</span>
		<span>0</span>
		<span>?</span>
		<span>?</span>
		<span>0</span>
		<span>0</span>
		<span>0</span>
		<span>0</span>
		<span>0</span>
		<span>?</span>
		<span>0</span>
		<span>0</span>
		<span>0</span>
		<span>0</span>
		<span>0</span>
		<span>0</span>
		<span>0</span>
		<span>0</span>
		<span>0</span>
		<span>0</span>
		<span>0</span>
		<div id="add">
		  <span>0</span>
		  <span>0</span>
		</div>
	  </aside>
	</main>
	<div>
	  <button>Filesystem</button>
	  <button>Home</button>
    </div>
	<div id="note"></div>
	<div>
	  <form>
		<input placeholder="new 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 type="button">Submit</button>
	  </form>
	</div>
	<div>
	  <button>WiFi Reconnect</button>
	  <button>ESP Restart</button>
	</div>
  </body>
</html>
fs.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">
	<link rel="stylesheet" href="style.css">
	<title>Filesystem Manager</title>
	<script>
	  document.addEventListener('DOMContentLoaded', () => {
		list(JSON.parse(localStorage.getItem('sortBy')));
		btn.addEventListener('click', () => { if (!confirm(`Wirklich formatieren? Alle Daten gehen verloren.\nDu musst anschließend fs.html wieder laden.`)) event.preventDefault()});
	  });
	  async function list(by, to = '/'){
		let resp = await fetch(`?sort=${by}`);
		let json = await resp.json();
		let myList = document.querySelector('main'), noted = '';
		myList.innerHTML = '<nav><input type="radio" id="/" name="group"><label for="/"> &#128193;</label><span id="cr">+&#128193;</nav></span><span id="si"></span>';
		document.querySelector('form').setAttribute('action', `/upload?f=${to}`);
		for (var i = 0; i < json.length - 1; i++) {
		  let dir = '', f = json[i].folder, n = json[i].name;
		  if (f != noted) {
			noted = f;
			dir = `<nav><input type="radio" id="${f}" name="group"><label for="${f}"></label> &#128193; ${f} <a href="?delete=/${f}">&#x1f5d1;&#xfe0f;</a></nav>`;
		  }
		  if (n != '') dir += `<li><a href="${f}/${n}">${n}</a><small> ${json[i].size}</small><a href="${f}/${n}"download="${n}"> Download</a> or<a href="?delete=${f}/${n}"> Delete</a>`;
		  myList.insertAdjacentHTML('beforeend', dir);
		}
		myList.insertAdjacentHTML('beforeend', `<li><b id="so">${by ? '&#9660;' : '&#9650;'} LittleFS</b> belegt ${json[i].usedBytes.replace(".00", "")} von ${json[i].totalBytes.replace(".00", "")}`);
		var free = json[i].freeBytes;
		cr.addEventListener('click', () => {
		  document.getElementById('no').classList.toggle('no');
		});
		so.addEventListener('click', () => {
		  list(by = ++by % 2, to);
		  localStorage.setItem('sortBy', JSON.stringify(by));
		});
		fs.addEventListener('change', e => {
		  for (var bytes = 0, i = 0; i < e.target.files.length; i++) bytes += e.target.files[i].size;
		  for (var output = `${bytes} Byte`, i = 0, circa = bytes / 1024; circa > 1; circa /= 1024) output = circa.toFixed(2) + [' KB', ' MB', ' GB'][i++];
		  if (bytes > free) {
			si.innerHTML = `<li><b> ${output}</b><strong> Ungenügend Speicher frei</strong></li>`;
			up.setAttribute('disabled', 'disabled');
		  }
		  else {
			si.innerHTML = `<li><b>Dateigröße:</b> ${output}</li>`;
			up.removeAttribute('disabled');
		  }
		});
		let node = document.querySelectorAll('main input');
		node.forEach(n => { if (n.id === to) n.setAttribute('checked', 'checked')});
		node.forEach(n => {
		  n.addEventListener('change', e => {
			if (e.target.checked) {
			  to = e.target.id
			  document.querySelector('form').setAttribute('action', `/upload?f=${to}`);
			}
		  });
		});
		document.querySelectorAll('[href^="?delete=/"]').forEach(node => { node.addEventListener('click', () => { if (!confirm('Sicher!')) event.preventDefault()})});
		document.querySelectorAll('main input').forEach(n => { if (n.id === to) n.setAttribute('checked', 'checked')});
	  }
	</script>
  </head>
  <body>
	<h2>ESP8266 Filesystem Manager</h2>
	<form method="post" enctype="multipart/form-data">
	  <input id="fs" type="file" name="up[]" multiple>
	  <button id="up" disabled>Upload</button>
	</form>
	<form id="no" class="no" method="post">
	  <input name="new" placeholder="Ordner Name" pattern="[^\x22/%&\\:;]{1,31}" title="Zeichen &#8220; % & / : ; \  sind nicht erlaubt." required>
	  <button>Create</button>
	</form>
	<main></main>
	<form action="/format" method="post">
	  <button id="btn">Format LittleFS</button>
	</form>
  </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">
    <link rel="stylesheet" href="style.css">
    <title>Dual Zeitschaltuhr</title>
	<style>
      main, #bu, .tab button, input:checked + label {
        color: #15dfdf;
      }
      div {
        display: flex;
      }
      span {
        padding: 0.5em;
      }
	  input {
        height: auto;
        font-weight: bold;
		background-color: inherit;
        font-size: 3em;
	    color: inherit;
	    border: solid #555;
      }
	  label {
	    font-size: .9em;
	  }
      svg {
        height: 4em;
      }
      * + [id*=ak]{ 
	    margin-top: .6em;
	  }
	  div+span {
	    margin-left: 3em;
	  }
      input + label {
        color: #777;
        font-style: italic;
      }
      #tog0,#tog1 {
        margin: .7em 1em;
        cursor: pointer;
	    color: #777;
      }
      time {
        text-shadow: 2px 2px 2px black;
        font-size: 1.3em;
        font-weight: bold;
        margin: auto;
      }
      .note:after {
        content: "Schaltzeiten gespeichert";
	    color: #777;
      }
      #bu {
	    border: outset #555;
      }
      .tab {
        overflow: hidden;
      }
      .tab button {
        background-color: #999;
        border: none;
	    margin-top: 0em;
        transition: 0.8s;
        border-radius: .5em .5em 0 0;
      }
      .tab button:hover {
        background-color: #666;
      }
	  #bu, .tabcontent, .tab button.active {
        background-color: #333;
      }
      .tabcontent {
        display: block;
        padding: .5em .7em .5em .1em;
	    box-shadow: 5px 3px 10px #4e4d4d;
      }
	  .tabcontent [name^=bu] {
        width: 2em;
	    cursor: pointer;
	  }
	  .none {
	    color: #777 !important;
	  }
	  .hide {
	    display: none;
	  }
      @media only screen and (max-width: 600px) {
        input {
          font-size: 2.4em;
          width: auto;
          border: none;
	    }
	    .tab button,#bu {
	      width: 7em;
	    }
	    #tog1 {
	      margin: .7em 0 0 0;
	    }
	    div+span {
	      margin-left: .2em;
	    }
      }
    </style>
    <script>
	  var count = 12;					<!-- Anzahl Schaltzeiten (analog Sketch) einstellen 2 bis 40 -->
	  var d = document;
      d.addEventListener('DOMContentLoaded', () => {
        dom(), renew();
        d.querySelector('#bu').addEventListener('click', () => {
          let arr = [], formData = new FormData();
		  formData.append('sTime', Array.from(d.querySelectorAll('input[type=time]')).map(x => x.value != 0 ? x.value : 0));
		  for (let i = 0; i < count; i++) {
		    let x = 0;
            d.querySelectorAll(`input[name=c${i}]`).forEach((el, i) => { if (el.checked) x = x | (1 << i) });
            arr.push(x);
          }
          formData.append(`sDay`, arr);
	      send(formData);
        });
        d.querySelector('#tab1').addEventListener('click', openTab);
        d.querySelector('#tab2').addEventListener('click', openTab);
        d.querySelector('#tog0').addEventListener('click', renew);
        d.querySelector('#tog1').addEventListener('click', renew);
        for (var i = 0; i < count;) d.querySelector(`[name=bu${i++}]`).addEventListener('click', setActive);
      },send(), setInterval(renew, 1000));
	  function dom() {
        var buf = '';
        for (var i = 0; i < count*2; i++) {
          buf += `${i%2 ? `<span> -- </span>` : `<div id="ak${i/2}"><span name="bu${i/2}"></span>`}<input type="time" name="sz${i}" value="">${i%2 ? `</div><span id="t${i/2|0}">` : ""}`;
		  if (i%2) {
		    ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'].forEach(v => {
              buf += `<input type="checkbox" name="c${(i-1)/2}"><label>${v} </label>`;
            });
			buf += "</span>";
		  }
          if (i == (count%2 ? count : count-1)) {
            d.querySelector('#ctab1').insertAdjacentHTML('beforeend', buf);
            buf = '';
          }if (i == count*2-1) d.querySelector('#ctab2').insertAdjacentHTML('beforeend', buf);
        }
      }
	  function setActive() {
	    let formData = new FormData();
        formData.append(this.parentNode.id.substr(2, 5), this.textContent == 'ON' ? '0' : '1');
	    send(formData);
      }
      function send(arg) {
        fetch('/timer', {
          method: 'post',
          body: arg
        }).then(resp => {
		  if (resp.ok) {
            if(arg && arg.has('sTime'))  {
              let el = d.querySelector('u').classList;
              el.add('note');
              setTimeout(() => {
                el.remove('note');
              }, 5e3);
            }
		  }
          return resp.json();
        }).then(array => {
		  if (array.length > count) {
            array.forEach((v, i) => {
		      if (i < count*2) d.querySelector(`[name=sz${i}]`).value = v;
			  if (i == count*2) getActive(v);
			  if (i > count*2) {
                let el = d.getElementsByName(`c${i - count * 2 - 1}`);
                for (let k in el) {
                  v & (1 << k) ? el[k].checked = true : el.checked = false;
                }
			  }
            });
		  }
		  else {
		    getActive(array);
		  }
        });
      }
	  function getActive(arg) {
	  	for (var i = 0; i < count; i++) {
		  d.querySelector(`[name=bu${i}]`).textContent = (arg[i]%2 ? 'ON' : 'OFF');
		  let el = d.getElementById(`ak${i}`).classList;
          arg[i]%2 ? el.remove("none") : el.add("none");
		  d.getElementById(`t${i}`).childNodes.forEach(v => {arg[i]%2 ? v.classList.remove("none") : v.classList.add("none")});
		}	
	  }
      function openTab() {
        let a = event.target.id.charAt(3)%2+1;
        d.getElementById(`ctab${a}`).classList.add("hide");
        d.getElementById(`tab${a}`).classList.remove("active");
        d.getElementById('c' + event.target.id).classList.remove("hide");
        event.target.classList.add("active")
      }
      function renew(ev) {
        if (ev) ev = ev.currentTarget.id.slice(3, 4);
        fetch(`timer?tog=${ev}`).then(resp => {
          return resp.json();
        }).then(array => {
          for (var i = 0; i < 2; i++) {
		    d.getElementById(`body${i}`).style.fill=array[i] == 0 ? '' : '#ff0';
            d.getElementById(`on${i}`).style.visibility=array[i] == 0 ? 'hidden' : 'visible';
		  }
		  d.querySelector('time').innerHTML = array[2];
        });
      }
    </script>
  </head>
  <body>
    <h2>Zeitschaltuhr</h2>
	<main>
      <div class="tab">
        <button class="active" id="tab1">&#9203; LED 1</button>
        <button id="tab2">&#9203; LED 2</button>
		<time>00:00:00</time>
      </div>
      <div id="ctab1" class="tabcontent">
      </div>
      <div id="ctab2" class="tabcontent hide">
      </div>
	</main>
    <div>
      <button class="button" id="bu">&#9200; Speichern</button>
      <div id="tog0">
        10W
        <svg viewBox="0 0 486 486">
          <use id="body0" href="#bulb"/>
          <use id="on0" href="#beam"/>
        </svg>
      </div>
      <div id="tog1">
        30W
        <svg viewBox="0 0 486 486">
          <use id="body1" href="#bulb"/>
          <use id="on1" href="#beam"/>
        </svg>
      </div>
    </div>
	<u></u>
	<svg class="hide">
	  <path id="bulb" 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" fill="#ff0" 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>
style.css

/* For more information visit:https://fipsok.de */
body {
	font-family: sans-serif;
	background-color: #87cefa;
	display: flex;
	flex-flow: column;
	align-items: center;
}
h1,h2 {
	color: #e1e1e1;
	text-shadow: 2px 2px 2px black;
}
li {
	background-color: #feb1e2;
	list-style-type: none;
	margin-bottom: 10px;
	padding: 2px 5px 1px 0;
	box-shadow: 5px 5px 5px rgba(0,0,0,0.7);
}
li a:first-child, li b {
	background-color: #8f05a5;
	font-weight: bold;
	color: white;
	text-decoration:none;
	padding: 2px 5px;
	text-shadow: 2px 2px 1px black;
	cursor:pointer;
}
li strong {
	color: red;
}
input {
	height:35px;
	font-size:14px;
	padding-left: .3em;
}
label + a {
	text-decoration: none;
}
h1 + main {
	display: flex;
}  
aside {
	display: flex;
	flex-direction: column;
	padding: 0.2em;
}
button {
	height:40px;
	width:130px;
	font-size:16px;
	margin-top: 1em;
	box-shadow: 5px 5px 5px rgba(0,0,0,0.7);
}
div button {
	background-color: #7bff97;
}
nav {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
}
#left {
	align-items:flex-end;
	text-shadow: 0.5px 0.5px 1px #757474;
}
#cr {
	font-weight: bold;
	cursor:pointer;
	font-size: 1.5em;
}
#up {
	width: auto; 
}
.note {
	background-color: #fecdee;
	padding: 0.5em;
	margin-top: 1em;
	text-align: center;
	max-width: 320px;
	border-radius: 0.5em;
}
.no {
	display: none;
}
form [title] {
	background-color: skyblue;
	font-size: 1em;
	width: 120px;
}
form:nth-of-type(2) {
	margin-bottom: 1em;
}
[value*=Format] {
	margin-top: 1em;
	box-shadow: 5px 5px 5px rgba(0,0,0,0.7);
}
[name="group"] {
	display: none;
}
[name="group"] + label {
	font-size: 1.5em;
	margin-right: 5px;
}
[name="group"] + label::before {
	content: "\002610";
}	
[name="group"]:checked + label::before {
	content: '\002611\0027A5';
}
Kommentar eintragen

*