Update: 2023-12-12

Esp8266 Zeitschaltuhr

Singel Zeitschaltuhr mit NTP Zeitsynchronisation

Automatischer Wechsel zwischen Sommer und Normalzeit

Optionaler Zugriffsschutz mittels HTTP-Authentifizierung

Betriebsstundenzähler für den Angeschlossenen Verbraucher

Schalten bei Dämmerung

Verbrauchsanzeige in Kilowattstunden

Ereignisdatenspeicher für Schaltzeiten und Auslöser

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".

Esp8266 Zeitschaltuhr

Highlight

Der Sketch Zeitschaltuhr ist für LOW und HIGH aktive Relais, Solid State Relais oder Mosfet geeignet. Dies muss vor dem Hochladen einmalig im Sketch, im Tab Schaltuhr.ino, eingestellt werden. Es lassen sich bis zu 100 Ein-/Aus-Schaltzeiten für ein angeschlossenes Gerät einstellen. Dies kann vor dem Hochladen im Tab Schaltuhr.ino eingestellt werden. Rechts neben dem Button zum manuellen Ein-/Ausschalten des Ausgangs befindet sich die optische Schaltzustandsanzeige.

Esp Zeitschaltuhr

Eine Betriebsstundenanzeige der Angeschlossenen Geräte erfolgt durch Klick/Touch auf die Uhrzeit. Gib den Verbrauch deines Gerätes in Watt in die Maske ein. Der Betriebstundenzähler lässt sich per Klick/Touch auf den Button zurücksetzen.

Esp Zeitschaltuhr

Es wird für jeden Monat eine neue CSV Datei angelegt, in der die Schaltzeiten und der Initiator des Schaltvorgangs gespeichert werden.

Ansicht Exel (formatiert)

Apr An Initiator Aus Initiator
1. 04:40:00 Programm 04:50:00 Programm
1. 21:36:20 192.168.178.36 21:36:41 192.168.178.36
1. 22:00:11 192.168.178.45 22:00:35 192.168.178.45
2. 04:38:00 Programm 04:40:00 Programm
3. 04:38:00 Programm 04:40:00 Programm
3. 20:47:07 192.168.178.36 20:47:48 192.168.178.36
3. 20:52:16 192.168.178.36 20:52:51 192.168.178.36
4. 05:47:09 Taster 05:53:21 Taster
5. 04:38:00 Programm 04:40:00 Programm

Esp8266 Zeitschaltuhr

Funktionen

Leuchtet das Sonne Symbol sind Schaltzeiten zur Dämmerung eingestellt. Ein Klick/Touch auf das Symbol öffnet bzw. schließt die Box zur Einstellung. Die täglich neu errechneten Dämmerungszeiten werden nun angezeigt. Es ist möglich das Einschalten bei Dämmerung mit einer einzelnen Ausschaltzeit (zB.: um 21:30) zu kombinieren.

Esp8266 Zeitschaltuhr

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

Grafik Esp8266 Zeitschaltuhr

Anleitung zur Inbetriebnahme

Zuerst im "Connect" Tab deine Wlan Zugangsdaten eingeben, anschliesend im "Schaltuhr" Tab die Konstante "ACTIVE" auf HIGH oder LOW setzen. "constexpr auto ACTIVE = LOW;" Je nachdem welche Komponente du an den Ausgängen betreiben möchtest. Du kannst die Anzahl der Schaltzeiten pro Relais an deinen Bedarf anpassen. Diese werden im "Schaltuhr" Tab definiert. "constexpr uint8_t RECORDS = 20;" Im Tab Sonnenlauf den Längen- und Breitengrad deines Standortes eintragen. Eventuell den NTP Zeitserver und die Zeitzone, für deinen Standort, in der "Lokalzeit.ino" ändern. Anschließend den Sketch hochladen.
Im Seriellen Monitor wird die IP des ESP.. angezeigt. "deineIP/fs.html" Kopiere diese URL in die Adresszeile deines Browsers und verbinde dich mit deinem ESP8266.
Falls sich im LittleFS (Speicher) des Esp8266 noch keine "fs.html" befindet wird ein kleiner Helfer zu deinem Browser gesendet. Mit diesem kannst du die "fs.html" und die "style.css" hochladen. Jetzt wird der Filesystem 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. Ein Klick/Touch auf das Blitz Symbol bringt dich zurück zum Filesystem Manager.

Neu: Optionaler Zugriffsschutz mittels HTTP-Authentifizierung.

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

Optional kann zusätzlich ein mechanischer Taster zum manuellen Schalten angeschlossen werden.

Zum Fernschalten des Verbrauchers am Esp mit Zeitschaltuhr von einem zweiten Esp8266 mittels Taster kann der HttpClientButton verwendet werden.

Zeitschaltuhr_Singel.ino

// ****************************************************************
// Sketch Esp8266 Zeitschaltuhr Modular(Tab)
// created: Jens Fleischer, 2019-10-04
// last mod: Jens Fleischer, 2023-12-12
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.7.0 - 3.1.2
// Getestet auf: Nodemcu, Wemos D1 Mini Pro
/******************************************************************
  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.
*******************************************************************/
// 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>

ESP8266WebServer server(80);

//#define DEBUGGING                                          // Einkommentieren für die Serielle Ausgabe.
constexpr bool PROTECT {0};                                  // HTML-Basisauthentifizierung aktivieren. (1 = aktiviert)
const char* www_username = "admin";                          // Die Anmeldeinformationen für HTML Seiten müssen im Browser eingegeben werden.
const char* www_password = "esp8266";

#ifdef DEBUGGING
#define DEBUG_B(...) Serial.begin(__VA_ARGS__)
#define DEBUG_P(...) Serial.println(__VA_ARGS__)
#define DEBUG_F(...) Serial.printf(__VA_ARGS__)
#else
#define DEBUG_B(...)
#define DEBUG_P(...)
#define DEBUG_F(...)
#endif

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

void setup() {
  DEBUG_B(115200);
  delay(100);
  DEBUG_F(PSTR("\n\nSketchname: %s\nBuild: %s\t\tIDE: %d.%d.%d\n%s\n\n"),
          (__FILE__), (__TIMESTAMP__), ARDUINO / 10000, ARDUINO % 10000 / 100, ARDUINO % 10, ESP.getFullVersion().c_str());

  setupFS();
  connectWifi();
  admin();
  setupTime();
  setupTimerSwitch();
  setupHobbsMeter();
  ArduinoOTA.onStart([]() {
    toSave();                                                // vor dem Sketch Update Betriebsstunden in Datei schreiben
  });
  ArduinoOTA.begin();
  server.begin();
}

void loop() {
  ArduinoOTA.handle();
  server.handleClient();
  if (millis() < 0x2FFF || millis() > 0xFFFFF0FF) runtime(); // Uptime für den Admin Tab
  localTime();                                               // Funktionsaufruf Uhrzeit aktualisieren
  timerSwitch();                                             // Funktionsaufruf Zeitschaltuhr
  sunRun();                                                  // Funktionsaufruf zur Berechnung der Dämmerungszeiten einmal Täglich
}

Admin.ino

// ****************************************************************
// Sketch Esp8266 Admin IPv6 Modular(Tab)
// created: Jens Fleischer, 2020-01-26
// last mod: Jens Fleischer, 2021-06-09
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.6.0 - 3.1.2
// Geprüft: von 1MB bis 16MB Flash
// 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 Admin 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.
// Die LittleFS.ino muss im ESP8266 Webserver enthalten sein
// Funktion "admin();" muss im setup() nach setupFS() 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();"
// In der IDE unter Werkzeuge auf lwip Variant: ;"v2 IPv6 Lower Memory;" einstellen.
/**************************************************************************************/

#include <AddrList.h>

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

void admin() {                                                      // Funktionsaufruf "admin();" muss im Setup eingebunden werden
  File file = LittleFS.open(existFolder("Config") + "/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");
    toSave();                                                       //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() / 1024.0 + " V" + "\"]");
}

void handleonce() {
  String ipv6Local, ipv6Global;
#if LWIP_IPV6
  for (auto &a : addrList) {
    if (a.isV6()) a.isLocal() ? ipv6Local = a.toString() : ipv6Global = a.toString();
  }
#endif
  if (server.arg(0) != "") {
    WiFi.hostname(server.arg(0));
    File f = LittleFS.open(existFolder("Config") + "/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() + "\", \"IPv6l\":\"" + ipv6Local +
                "\", \"IPv6g\":\"" +  ipv6Global + "\", \"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() + "\"}";
  server.send(200, "application/json", temp);     // Json als Objekt
}

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;
}

In diesem Tab sind deine Wlan Zugangsdaten einzutragen.

Connect.ino

// ****************************************************************
// Sketch Esp8266 Connect STA Modular(Tab) mit optischer Anzeige
// created: Jens Fleischer, 2018-04-08
// last mod: Jens Fleischer, 2020-12-28
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.4.2 - 3.1.2
// 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 Connect STA sollte als Tab eingebunden werden.
// #include <ESP8266WebServer.h> oder #include <ESP8266WiFi.h> muss im Haupttab aufgerufen werden
// Die Funktion "connectWifi();" muss im Setup eingebunden werden.
/**************************************************************************************/

//#define CONFIG                                // Einkommentieren wenn der ESP dem Router die IP mitteilen soll.
#define NO_SLEEP                                // Auskommentieren wenn der Nodemcu den deep sleep Modus nutzt.

const char* ssid = "Netzwerkname";              // Darf bis zu 32 Zeichen haben.
const char* password = "PasswortvomNetzwerk";   // Mindestens 8 Zeichen jedoch nicht länger als 64 Zeichen.

#ifdef CONFIG
IPAddress staticIP(192, 168, 178, 99);          // Statische IP des NodeMCU ESP8266
IPAddress gateway(192, 168, 178, 1);            // IP-Adresse des Router
IPAddress subnet(255, 255, 255, 0);             // Subnetzmaske des Netzwerkes
IPAddress dns(192, 168, 178, 1);                // DNS Server
#endif

void connectWifi() {                            // Funktionsaufruf "connectWifi();" muss im Setup eingebunden werden.
  byte i = 0;
  //WiFi.disconnect();                          // Nur erforderlich wenn Esp den AP Modus nicht verlassen will.
  WiFi.persistent(false);                       // Auskommentieren wenn Netzwerkname und Passwort in den Flash geschrieben werden sollen.
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
#ifdef CONFIG
  WiFi.config(staticIP, gateway, subnet, dns);
#endif
  while (WiFi.status() != WL_CONNECTED) {
#ifdef NO_SLEEP
    pinMode(LED_BUILTIN, OUTPUT);               // OnBoardLed Nodemcu, Wemos D1 Mini Pro
    digitalWrite(LED_BUILTIN, 0);
#endif
    delay(500);
    digitalWrite(LED_BUILTIN, 1);
    delay(500);
    DEBUG_F(" %d sek\n", ++i);
    if (i > 9) {
      DEBUG_P(PSTR("\nVerbindung zum AP fehlgeschlagen !\n\n"));
      ESP.restart();
    }
  }
  DEBUG_P("\nVerbunden mit: " + WiFi.SSID());
  DEBUG_F("\nGib diese URL in deinem Browser ein: %s/fs.html\n\n", WiFi.localIP().toString().c_str());
}

LittleFS.ino

// ****************************************************************
// Sketch Esp8266 Filesystem Manager spezifisch sortiert Modular(Tab)
// created: Jens Fleischer, 2020-06-08
// last mod: Jens Fleischer, 2023-12-12
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.7.0 - 3.1.2
// 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");
  });
  const char * headerkeys[] = {"If-None-Match"} ;                                      // "If-None-Match" HTTP-Anfrage-Header einfügen
  server.collectHeaders(headerkeys, static_cast<size_t>(1));                           // für ETag Unterstüzung: vor Core Version 3.x.x.
}

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, size_t, time_t>;
  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(), fold.fileTime());
      }
      if (!ran) dirList.emplace_back(dir.fileName(), "", 0, 0);
    }
    else {
      dirList.emplace_back("", dir.fileName(), dir.fileSize(), dir.fileTime());
    }
  }
  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)) + "\",\"time\":\"" + get<3>(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 (!LittleFS.exists("fs.html")) server.send(200, "text/html", LittleFS.begin() ? HELPER : WARNING);     // ermöglicht das hochladen der fs.html
  if (!(PROTECT && !authenticated())) {
    if (server.hasArg("new")) {
      for (auto& c : {34, 37, 38, 47, 58, 59, 92}) for (auto& e : server.arg("new")) if (e == c) return sendResponce();    // Abbrechen bei nicht erlaubten Zeichen
      LittleFS.mkdir(server.arg("new"));
    }
    if (server.hasArg("sort")) return handleList();
    if (server.hasArg("delete")) {
      deleteRecursive(server.arg("delete"));
      return sendResponce();
    }
    if (path.endsWith("/")) path += "index.html";
    File f = LittleFS.open(path, "r");
    String eTag = String(f.getLastWrite(), HEX);                                       // Verwendet den Zeitstempel der Dateiänderung, um den ETag zu erstellen.
    if (server.header("If-None-Match") == eTag) {
      server.send(304);
      return true;
    }
    server.sendHeader("ETag", eTag);
    return LittleFS.exists(path) ? server.streamFile(f, mime::getContentType(path)) : false;
  }
  return 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();
}

bool sendResponce() {
  server.sendHeader("Location", "fs.html");
  server.send(303, "message/http");
  return true;
}

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;
}

bool authenticated() {
  if (server.authenticate(www_username, www_password)) return true;
  server.requestAuthentication(DIGEST_AUTH, "Anmeldung erforderlich", "Authentifizierung fehlgeschlagen!");
  return false;
}

Lokalzeit.ino

// ****************************************************************
// Sketch Esp8266 Lokalzeit Modular(Tab)
// created: Jens Fleischer, 2020-09-20
// last mod: Jens Fleischer, 2023-01-11
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.6.0 - 3.1.2
// 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.
// Inclusive Abfrage ob die Zeit vom NTP Server geholt werden konnte.
/**************************************************************************************/

#include <time.h>

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

struct tm tm;

const char* const PROGMEM NTP_SERVER[] = {"fritz.box", "de.pool.ntp.org", "at.pool.ntp.org", "ch.pool.ntp.org", "ptbtime1.ptb.de", "europe.pool.ntp.org"};
const char* const PROGMEM MONTH_SHORT[] = {"Jan", "Feb", "Mrz", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"};

extern "C" uint8_t sntp_getreachability(uint8_t);

bool getNtpServer(bool reply = false) {
  uint32_t timeout {millis()};
  configTime("CET-1CEST,M3.5.0/02,M10.5.0/03", NTP_SERVER[1], NTP_SERVER[4]);   // Zeitzone einstellen https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
  do {
    delay(25);
    if (millis() - timeout >= 1e3) {
      DEBUG_F(PSTR("Warten auf NTP-Antwort %2502ld sec\n"), (millis() - timeout) / 1000);
      delay(975);
    }
    sntp_getreachability(0) ? reply = true : sntp_getreachability(1) ? reply = true : sntp_getreachability(2) ? reply = true : false;
  } while (millis() - timeout <= 16e3 && !reply);
  return reply;
}

void setupTime() {
#ifndef DEBUGGING
  getNtpServer();
#endif
  DEBUG_F(PSTR("NTP Synchronisation %s!\n\n"), getNtpServer() ? "erfolgreich" : "fehlgeschlagen");
}

String localTime() {
  static char buf[9];                                    // je nach Format von "strftime" eventuell anpassen
  static time_t lastsec;
  time_t now = time(&now);
  localtime_r(&now, &tm);
  if (tm.tm_sec != lastsec) {
    lastsec = tm.tm_sec;
    strftime (buf, sizeof(buf), "%T", &tm);            // http://www.cplusplus.com/reference/ctime/strftime/
    if (!(time(&now) % (SYNC_INTERVAL * 3600))) {
#ifndef DEBUGGING
      getNtpServer();
#endif
      DEBUG_F(PSTR("NTP Synchronisation %s!\n"), getNtpServer(true) ? "erfolgreich" : "fehlgeschlagen");
    }
  }
  return buf;
}

In diesem Tab einstellen ob dein Device LOW oder HIGH aktiv geschaltet wird.
Und die Anzahl der Schaltzeiten einstellen. (1 bis 100).

Schaltuhr.ino

// ****************************************************************
// Sketch Esp8266 Schaltuhr Modular(Tab)
// created: Jens Fleischer, 2019-10-04
// last mod: Jens Fleischer, 2021-08-23
// 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
// IN an D5 = GPIO14
// 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
// Mosfet Gate an D5 = GPIO14
//
// für 3V Solid State Relais
// GND an GND
// SSR Input + an D5 = GPIO14
//
// Software: Esp8266 Arduino Core 2.6.0 - 3.1.2
// Getestet auf: Nodemcu, Wemos D1 Mini Pro
/******************************************************************
  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 Zeitschaltuhr 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 "setupTimerSwitch();" muss im Setup aufgerufen werden.
// Zum schalten muss die Funktion "timerSwitch();" im loop(); aufgerufen werden.
/**************************************************************************************/

#include <tuple>
#include <vector>

constexpr auto ACTIVE = LOW;                                             // LOW für LOW aktive Relais oder HIGH für HIGH aktive (zB. SSR, Mosfet) einstellen
constexpr uint8_t DEVICE_PIN = D5;                                       // Pin für Device einstellen
constexpr uint8_t RECORDS = 20;                                          // Anzahl Schaltzeiten festlegen 1 - 100
bool pinState {!ACTIVE};

typedef char timeArr[6];
timeArr sunTime[4];

using namespace std;
using Subset = tuple<bool, uint8_t, String, String>;
vector<Subset> device;

struct extra {
  uint8_t sun;
  bool fixed;
} option;

void setupTimerSwitch() {
  digitalWrite(DEVICE_PIN, !ACTIVE);
  pinMode(DEVICE_PIN, OUTPUT);
  device.reserve(RECORDS);
  Subset t = make_tuple(1, 127, "", "");
  File file = LittleFS.open(existFolder("Config") + "/dtime.dat", "r");
  if (file) {                                                            // Einlesen aller Daten falls die Datei im LittleFS vorhanden und deren Größe stimmt.
    file.read(reinterpret_cast<uint8_t*>(&option), sizeof(option));
    for (uint8_t i = 0; i < device.capacity(); i++) {
      file.read(reinterpret_cast<uint8_t*>(&t), sizeof(Subset));
      device.emplace_back(t);
    }
    file.close();
#ifdef DEBUGGING
    uint8_t j = 0;
    for (const auto &e : device) DEBUG_F(PSTR("Subset %d: %d, %d, %s, %s\n"), ++j, get<0>(e), get<1>(e), get<2>(e).c_str(), get<3>(e).c_str());
#endif
  }
  else {
    DEBUG_P(F("Die \"dtime.dat\" ist nicht vorhanden!"));
    for (uint8_t i = 0; i < device.capacity(); i++) device.push_back(t);
  }
  DEBUG_F(PSTR("\nZeitschaltuhr Automatik %saktiviert\n\n"), option.fixed ? "de" : "");
  server.on("/timer", HTTP_POST, []() {
    if (server.hasArg("dTime")) {
      device.clear();
      Subset t = make_tuple(0, 0, "", "");
      char str[server.arg("dTime").length()];
      strcpy (str, server.arg("dTime").c_str());
      char* ptr = strtok(str, "[\"");
      for (auto i = 0; ptr != NULL; i++, i %= 4) {
        if (strcmp(ptr, ",")) {
          if (i == 0) {
            get<0>(t) = atoi(ptr);
          }
          else if (i == 1) {
            get<1>(t) = atoi(ptr);
          }
          else if (i == 2) {
            get<2>(t) = static_cast<String>(ptr);
          }
          else if (i == 3) {
            get<3>(t) = static_cast<String>(ptr);
            device.emplace_back(t);
          }
        }
        ptr = strtok(NULL, "\",[]");
      }
      printer();
    }
    String temp = "[";
    for (auto &t : device) {
      temp == "[" ? temp += "[" : temp += ",[";
      temp += "\"" + static_cast<String>(get<0>(t)) + "\",";
      temp += "\"" + static_cast<String>(get<1>(t)) + "\",";
      temp += "\"" + get<2>(t) + "\",";
      temp += "\"" + get<3>(t) + "\"]";
    }
    temp += "]";
    server.send(200, "application/json", temp);
  });
  server.on("/timer", HTTP_GET, []() {
    char buf[44];
    if (server.arg(0) == "tog") {
      pinState = !pinState;                                              // Pin Status manuell ändern
      timeDataLogger(ACTIVE, pinState, server.client().remoteIP().toString());   // Funktionsaufruf Zeitdatenlogger
    }
    else if (server.arg(0) == "fix") {
      option.fixed = !option.fixed;                                      // alle Schalzeiten deaktivieren/aktivieren
      printer();
    }
    if (server.arg(0) == "sun") {
      if (server.hasArg("select")) {
        option.sun = server.arg(1).toInt();
        printer();
      }
      snprintf(buf, sizeof(buf), "[[\"%s\",\"%s\",\"%s\",\"%s\"],[\"%d\"]]", sunTime[0], sunTime[1], sunTime[2], sunTime[3], option.sun);
    }
    else {
      snprintf(buf, sizeof(buf), "[\"%d\",\"%s\",\"%d\"]", pinState == ACTIVE, localTime().c_str(), option.fixed);
    }
    server.send(200, "application/json", buf);
  });
}

void printer() {
  File file = LittleFS.open(existFolder("Config") + "/dtime.dat", "w");
  if (file) {
    file.write(reinterpret_cast<const uint8_t*>(&option), sizeof(option));
    for (const auto &e : device) {
      file.write(reinterpret_cast<const uint8_t*>(&e), sizeof(e));
    }
    file.close();
  }
}

void timerSwitch() {
  static uint8_t lastmin {CHAR_MAX}, lastState {ACTIVE};
  hobbsMeter(pinState);                                                  // Funktionsaufruf Betriebsstundenzähler mit Pin Status
  button(pinState);                                                      // Funktionsaufruf Manueller Taster mit Pin Status
  timeDataLogger(ACTIVE, pinState, "Taster");                            // Funktionsaufruf Zeitdatenlogger
  if (tm.tm_min != lastmin && !option.fixed) {
    lastmin = tm.tm_min;
    char buf[6];
    sprintf(buf, "%.2d:%.2d", tm.tm_hour, tm.tm_min);
    for (auto &t : device) {
      if (get<0>(t) && (get<1>(t) & (1 << (tm.tm_wday ? tm.tm_wday - 1 : 6)))) {
        if (!get<2>(t).compareTo(buf)) pinState = ACTIVE;
        if (!get<3>(t).compareTo(buf)) pinState = !ACTIVE;
      }
    }
    for (auto i = 0; i < 4; i++) {
      if (!strcmp(sunTime[i], buf)) {                                    // Pin Status zum Sonnenstand ändern
        if (option.sun & (1 << i * 2)) pinState = !ACTIVE;
        if (option.sun & (1 << (i * 2 + 1))) pinState = ACTIVE;
      }
    }
    timeDataLogger(ACTIVE, pinState, "Programm");                        // Funktionsaufruf Zeitdatenlogger
  }
  if (pinState != lastState) {                                           // Pin schalten wenn sich der Status geändert hat
    lastState = pinState;
    digitalWrite(DEVICE_PIN, pinState);
    DEBUG_F(PSTR("Schaltausgang A%s\n"), digitalRead(DEVICE_PIN) == ACTIVE ? "n" : "us");
  }
}

In diesem Tab den Längen- und Breitengrad deines Standortes angeben.

Sonnenlauf.ino

// ****************************************************************
// Sketch Esp8266 Sonnenlauf Modular(Tab)
// source: https://lexikon.astronomie.info/zeitgleichung/neu.html
// created: Jens Fleischer, 2019-01-05
// last mod: Jens Fleischer, 2023-01-11
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.4.2 - 3.1.2
// 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 Sonnenlauf sollte als Tab eingebunden werden.
// Der Lokalzeit Tab (bzw. "struct tm") ist zum ausführen erforderlich.
// Gib zunächst den Längen- und Breitengrad deines Ortes an.
// https://www.laengengrad-breitengrad.de/
/**************************************************************************************/

constexpr double LONGITUDE = 12.348239;  // Geographische Länge
constexpr double LATITUDE = 51.346030;   // Geographische Breite

void sunRun() {
  static uint8_t lastday, lastdst;
  if (tm.tm_mday != lastday || tm.tm_isdst != lastdst) {    // Sonnenlauf für Tage mit Zeitumstellung zweimal Täglich berechnen
    lastday = tm.tm_mday;
    lastdst = tm.tm_isdst;
    const double w = LATITUDE * DEG_TO_RAD;
    double JD = julianDate(1900 + tm.tm_year, 1 + tm.tm_mon, tm.tm_mday);
    double T = (JD - 2451545.0) / 36525.0;
    double DK;
    double EOT = calculateEOT(DK, T);
    double h = -0.833333333333333 * DEG_TO_RAD; // Sonenaufgang/Sonnenuntergang
    double differenceTime = 12.0 * acos((sin(h) - sin(w) * sin(DK)) / (cos(w) * cos(DK))) / PI;
    strcpy(sunTime[1], outputFormat((12.0 - differenceTime - EOT) - LONGITUDE / 15.0 + (_timezone * -1) / 3600 + tm.tm_isdst));
    strcpy(sunTime[2], outputFormat((12.0 + differenceTime - EOT) - LONGITUDE / 15.0 + (_timezone * -1) / 3600 + tm.tm_isdst));
    h = -6.0 * DEG_TO_RAD;                      // Bürgerliche Dämmerung
    differenceTime = 12.0 * acos((sin(h) - sin(w) * sin(DK)) / (cos(w) * cos(DK))) / PI;
    strcpy(sunTime[0], outputFormat((12.0 - differenceTime - EOT) - LONGITUDE / 15.0 + (_timezone * -1) / 3600 + tm.tm_isdst));
    strcpy(sunTime[3], outputFormat((12.0 + differenceTime - EOT) - LONGITUDE / 15.0 + (_timezone * -1) / 3600 + tm.tm_isdst));
    DEBUG_F(PSTR("\nMorgendämmerung: %s\tAbenddämmerung: %s\nSonnenaufgang: %s\tSonnenuntergang: %s\n"), sunTime[0], sunTime[3], sunTime[1], sunTime[2]);
  }
}

double julianDate (int y, int m, int d) {       // Gregorianischer Kalender
  if (m <= 2) {
    m = m + 12;
    y = y - 1;
  }
  int gregorian = (y / 400) - (y / 100) + (y / 4); // Gregorianischer Kalender
  return 2400000.5 + 365.0 * y - 679004.0 + gregorian + (30.6001 * (m + 1)) + d + 12.0 / 24.0;
}

double InPi(double x) {
  int n = x / TWO_PI;
  x = x - n * TWO_PI;
  if (x < 0) x += TWO_PI;
  return x;
}

double calculateEOT(double &DK, double T) {
  double RAm = 18.71506921 + 2400.0513369 * T + (2.5862e-5 - 1.72e-9 * T) * T * T;
  double M  = InPi(TWO_PI * (0.993133 + 99.997361 * T));
  double L  = InPi(TWO_PI * (  0.7859453 + M / TWO_PI + (6893.0 * sin(M) + 72.0 * sin(2.0 * M) + 6191.2 * T) / 1296.0e3));
  double e = DEG_TO_RAD * (23.43929111 + (-46.8150 * T - 0.00059 * T * T + 0.001813 * T * T * T) / 3600.0);    // Neigung der Erdachse
  double RA = atan(tan(L) * cos(e));
  if (RA < 0.0) RA += PI;
  if (L > PI) RA += PI;
  RA = 24.0 * RA / TWO_PI;
  DK = asin(sin(e) * sin(L));
  RAm = 24.0 * InPi(TWO_PI * RAm / 24.0) / TWO_PI;
  double dRA = RAm - RA;
  if (dRA < -12.0) dRA += 24.0;
  if (dRA > 12.0) dRA -= 24.0;
  dRA = dRA * 1.0027379;
  return dRA ;
}

char* outputFormat(double sunTime) {
  if (sunTime < 0) sunTime += 24;
  else if (sunTime >= 24) sunTime -= 24;
  int8_t decimal = 60 * (sunTime - static_cast<int>(sunTime)) + 0.5;
  int8_t predecimal = sunTime;
  if (decimal >= 60) {
    decimal -= 60;
    predecimal++;
  }
  else if (decimal < 0) {
    decimal += 60;
    predecimal--;
    if (predecimal < 0) predecimal += 24;
  }
  static char buf[9];
  snprintf(buf, sizeof(buf), "%.2d:%.2d", predecimal, decimal);
  return buf;
}

Stundenzaehler.ino

// ****************************************************************
// Sketch Esp8266 Betriebsstundenzähler Modular(Tab)
// created: Jens Fleischer, 2019-07-21
// last mod: Jens Fleischer, 2020-09-22
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.6.0 - 3.1.2
// Getestet auf: Nodemcu, Wemos D1 Mini Pro
/******************************************************************
  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 Betriebsstundenzähler sollte als Tab eingebunden werden.
// #include <FS.h> #include <ESP8266WebServer.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP8266 Webservers ist erforderlich.
// Der LittleFS Tab ist zum ausführen des Betriebsstundenzähler einzubinden.
// Die Funktion "setupHobbsMeter();" muss im Setup aufgerufen werden.
/**************************************************************************************/

uint32_t totalmin;
uint16_t watt;

void setupHobbsMeter() {
  File file = LittleFS.open(existFolder("Config") + "/hobbs.json", "r");   // Betriebstunden(minuten) beim Neustart einlesen
  if (file) {
    char buf[file.size()];
    file.readBytes(buf, sizeof buf);
    totalmin = atoi(strtok(buf, "{\":runtime"));
    watt = atoi(strtok(NULL, "\":,wattage"));
    file.close();
  }
  server.on("/hobbs", HTTP_GET, []() {
    uint32_t power;
    if (server.argName(0) == "watt") {
      watt = server.arg(0).toInt();
      toSave();
    }
    if (server.argName(0) == "reset") {
      totalmin = 0;                                                         // Betriebsstundenzähler zurücksetzen
      toSave();
    }
    power = (watt * totalmin) / 6000;
    char buf[34];
    snprintf(buf, sizeof(buf), PSTR("[\"%d,%d\",\"%d\",\"%d,%d\"]"), totalmin / 60, totalmin / 6 % 10, watt, power / 10, power % 10);
    server.send(200, "application/json", buf);
  });
}

void hobbsMeter(const bool &state) {                                        // Aufrufen mit Pin Status
  static uint32_t lastmin, previousMillis[] {0, 0};
  uint32_t currentMillis {millis()};
  if (currentMillis - previousMillis[0] >= 6e4) {
    previousMillis[0] = currentMillis;
    if (state == ACTIVE) totalmin++;                                        // Betriebstundenzähler des Gerätes wird um eine Minute erhöht
  }
  if (currentMillis - previousMillis[1] >= 864e5 && totalmin != lastmin) {  // einmal am Tage Betriebsstunden in Datei schreiben wenn sich der Wert geändert hat
    previousMillis[1] = currentMillis;
    lastmin = totalmin;
    toSave();
  }
}

void toSave() {
  File file = LittleFS.open(existFolder("Config") + "/hobbs.json", "w");    // Betriebstunden(minuten) speichern
  if (file) {
    file.printf(R"({"runtime":"%u","wattage":"%d"})", totalmin, watt);
    file.close();
  }
}

Ein Taster kann optional angeschlossen werden.

Taster.ino

// ****************************************************************
// Sketch Esp8266 Zeitschaltuhr Taster Modular(Tab)
// created: Jens Fleischer, 2019-12-27
// last mod: Jens Fleischer, 2021-08-23
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// D2 = GPIO4  Anschluss Taster vom GPIO4 auf GND
// Software: Esp8266 Arduino Core 2.4.2 - 3.1.2
// Getestet auf: Nodemcu, Wemos D1 Mini Pro
/******************************************************************
  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 Zeitschaltuhr Taster sollte als Tab eingebunden werden.
// Die Funktion "button(pinState);" muss in "timerSwitch();" aufgerufen werden.
/**************************************************************************************/

constexpr uint8_t inputPIN {D2};                    // Pin für Taster einstellen

void button(bool &state) {                          // Aufrufen mit Pin Status
  pinMode(inputPIN, INPUT_PULLUP);                  // oder 10k Pullup-Widerstand von VCC zum inputPIN
  static bool previousStatus {0};
  static uint32_t debounceMillis;
  uint32_t currentMillis {millis()};
  if (currentMillis - debounceMillis >= 50) {       // 50 ms Taster Entprellzeit
    debounceMillis = currentMillis;
    bool currentStatus = digitalRead(inputPIN);
    if (!currentStatus && currentStatus != previousStatus) {
      state = !state;                               // Status toggeln
    }
    previousStatus = currentStatus;
  }
}

Zeitenlogger.ino

// ****************************************************************
// Sketch Esp8266 Zeitdatenlogger Modular(Tab)
// created: Jens Fleischer, 2019-03-31
// last mod: Jens Fleischer, 2023-01-11
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.6.0 - 3.1.2
// 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 bool ACTIVE, const bool &state, const String &customer) {  // Ereignisauslöser
  static bool lastState {!ACTIVE};
  if (state != lastState) {                                                    // Prüft ob sich der Pin Status geändert hat.
    char fileName[18];
    snprintf(fileName, sizeof(fileName), "/%d_%s.csv", 1900 + tm.tm_year, 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();
    }
    File f = LittleFS.open(existFolder("Data") + fileName, "a");               // Die Ereignisdaten für den aktuellen Monat speichern
    if (f) {
      state == ACTIVE ? f.printf("%d.;%s;%s;", tm.tm_mday, localTime().c_str(), customer.c_str()) : f.printf("%s;%s;\n", localTime().c_str(), customer.c_str());
    }
    f.close();
  }
  lastState = state;                                                           // Ersetzt den letzten Status durch den aktuellen 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>
	<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['IPv6l'] ? obj['IPv6l'] : 'inaktiv';
			span[9].innerHTML = obj['IPv6g'] ? obj['IPv6g'] : 'inaktiv';
			span[10].innerHTML = obj['Hostname'];
			span[11].innerHTML = obj['SSID'];
			span[12].innerHTML = obj['GatewayIP'];
			span[13].innerHTML = obj['Channel'];
			span[14].innerHTML = obj['MacAddress'];
			span[15].innerHTML = obj['SubnetMask'];
			span[16].innerHTML = obj['BSSID'];
			span[17].innerHTML = obj['ClientIP'];
			span[18].innerHTML = obj['DnsIP'];
			span[19].innerHTML = obj['ResetReason'];
			span[20].innerHTML = obj['CpuFreqMHz'] + " MHz";
			span[21].innerHTML = obj['FreeHeap'];
			span[22].innerHTML = obj['HeapFrag'] + "%";
			span[23].innerHTML = obj['ChipSize'];
			span[24].innerHTML = obj['ChipSpeed'] + " MHz";
			span[25].innerHTML = obj['ChipMode'];
			span[26].innerHTML = obj['IdeVersion'].replace(/(\d)(\d)(\d)(\d)/,obj['IdeVersion'][3]!=0 ? '$1.$3.$4' : '$1.$3.');
			span[27].innerHTML = obj['CoreVersion'].replace(/_/g,'.');
			span[28].innerHTML = obj['SdkVersion'];
			Object.keys(obj).forEach(val => {
				if (obj[val].length > 25) document.querySelectorAll(`[data-${val}]`).forEach(el => {el.classList.add('ip')});
			});
		  } 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 data-ipv6l>Link-Local:</span>
		<span data-ipv6g>IPv6:</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 data-clientip>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>
	  </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 data-ipv6l>0</span>
		<span data-ipv6g>0</span>
		<span>?</span>
		<span>?</span>
		<span>0</span>
		<span>0</span>
		<span>0</span>
		<span>0</span>
		<span>0</span>
		<span data-clientip>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>
		<span>0</span>
	  </aside>
	</main>
	<div>
	  <button>Filesystem</button>
	  <button>Startseite</button>
	</div>
	<div id="note"></div>
	<div>
	  <form>
		<input placeholder="neuer Hostname" pattern="([A-Za-z0-9\-]{0,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">Name Senden</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(`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, t = new Date(json[i].time*1000).toLocaleString();
		  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 title="Geändert: ${t}" 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\/%&\\:;]{0,31}[^\x22\/%&\\:;\s]{1}" 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>

Die Webseite zur Singel Zeitschaltuhr.

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>Timer</title>
	<style>
	  main {
		background: linear-gradient(to right, #3b82a5 ,#333 5%, #333 95%, #3b82a5);
		padding:1em 1.5em;
		border-radius: 2em;
	  }
	  div button, input, #top, #below, #view, #option{
		background-color: #333;
	  }
	  main, button, input:checked+label {
		color: #15dfdf;
	  }
	  button, svg, time, [name^=bu], #reset, #sun {
		cursor: pointer;
	  }
	  input, #view, #option, #reset {
		border: solid #555;
	  }
	  div:not([id^="st"]) {
		display: flex;
		align-items: center;
	  }
	  div+[type="checkbox"] {
		margin-left: 1em;
	  }
	  div+span {
		display: flex;
		justify-content: space-evenly;
		margin-left: 1.7em;
	  }
	  span {
		padding: 0.5em;
	  }
	  svg {
		width: 3em;
	  }
	  time {
		font-size: 1.4em;
		font-weight: bold;
		text-shadow: 1px 1px 1px #777;
	  }
	  input {
		height: auto;
		width: auto;
		font-size: 3em;
		font-weight: bold;
		color: inherit;
		padding: 0;
	  }
	  label {
		font-style: italic;
		color: #777;
	  }
	  button {
		border: outset #999;
		width: 50%;
		margin: 0;
		box-shadow: none;
	  }
	  button:only-of-type {
		width: 25%;
	  }
	  [name^=bu] {
		width: 2em;
	  }
	  #top {
		justify-content: space-evenly;
		position: sticky;
		top: 0;
		z-index: 1;
	  }
	  #tip {
		position: sticky;
		top: 50%;
		z-index: 2;
		height: 0;
	  }
	  #option {
		align-items: flex-end;
	  }
	  #view, #option {
		position: sticky;
		flex-direction: column;
		top: 4.4em;
		margin-bottom: .3em;
		z-index: 1;
	  }
	  #watt {
		font-size: 1em;
		width: 2.6em;
		text-align: end;
	  }
	  #kwh {
		cursor: default;
		border: solid #f00;
		margin: .5em;
		padding: .3em;
	  }
	  #below {
		position: sticky;
		bottom: .1em;
		padding: .7em 0 .7em 0;
	  }
	  #sun {
		font-size: 2em;
		padding: .5em 0;
	  }
	  .dark {
		opacity: .5;
	  }
	  .edit, .noedit {
		justify-content: center;
		width: 100%;
		background-color: red;
		color: #fff;
		padding: 1em;
		border-radius: .5em;
	  }
	  .edit:after {
		content: 'Eingabe gespeichert';  
	  }
	  .noedit:after {
		content: 'Nur 4 Ziffern 0 - 9 eingeben';		 
	  }
	  .fix, .greyed {
		color: #666 !important;
	  }
	  .none {
		display: none !important;
	  }
	  @media only screen and (max-width: 600px) {
		input {
		  font-size: 2.2em;
		}
	  }
	</style>
	<script>
	  const d = document;
	  d.addEventListener('DOMContentLoaded', async () => {
		let resp = await fetch('/timer', {method: 'post'});
		let array = await resp.json();
		var buf = '';
		for (var i = 0; i < array.length; i++) {
		  buf += `<div id=st${i}><div><span name=bu>ON</span><input type="time">\u00A0--\u00A0<input type="time"></div><span>`;
		  ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'].forEach(d => {buf += `<input type="checkbox" checked><label>${d} </label>`;});
		  buf += '</span></div>';
		}
		buf += '<div id="below"><button>&#9200; Zeiten Speichern</button><button>-</button></div>';
		d.querySelector('main').insertAdjacentHTML('beforeend', buf);
		buf = '';
		['Morgendämmerung', 'Sonnenaufgang', 'Sonnenuntergang', 'Abenddämmerung'].forEach((s, i) => {
		  buf += `<span>${s} <span id="sl${i}"></span><input type="checkbox" id="sr${i*2}"><label>Aus</label><input type="checkbox" id="sr${i*2+1}"><label>Ein</label></span>`;
		});
		d.querySelector('#option').insertAdjacentHTML('beforeend', buf);
		fill(array);
		renew(), renew('sun'), setInterval(renew, 1000);
		d.querySelector('main').classList.remove('none');
		btn = d.querySelectorAll(`button`);
		btn[0].addEventListener('click', renew.bind(this, 'tog'));
		btn[1].addEventListener('click', save);
		btn[2].addEventListener('click', renew.bind(this, 'fix'));
		node = d.querySelectorAll('[id^=sr]');
		node.forEach(el => {
		  el.addEventListener('change', () => {
			let i = event.target.id.slice(2, 3);
			i%2 == 0 ? node[++i].checked = false : node[--i].checked = false;
			setSun();
			let x = 0;
			node.forEach((el, i) => { if (el.checked) x = x | (1 << i) });
			renew('sun&select=' + x);
		  });
		});
		d.querySelectorAll(`[name=bu]`).forEach(el => {el.addEventListener('click', save)});
		d.querySelector('#reset').addEventListener('click', () => {if (confirm('Bist du sicher!')) hours('?reset=1');});
		d.querySelector('#watt').addEventListener('blur', () => {
		  let inObj = d.querySelector('#watt');
		  !inObj.checkValidity() ? out('noedit') : hours(`?watt=${parseInt(inObj.value)}`);
		});
		d.querySelector('time').addEventListener('click', () => { if (!d.getElementById('view').classList.toggle('none')) hours();});
		d.querySelector('svg').addEventListener('click', () => {window.location = '/fs.html';});
		sun.addEventListener('click', () => {d.getElementById('option').classList.toggle('none')});
	  });
	  function setSun() {
		let check = 0;
		node.forEach(el => { if (el.checked) ++check;});
		check ? sun.classList.remove('dark') : sun.classList.add('dark');
	  }
	  function out(arg) {
		let el = d.querySelector('#info').classList;
		el.add(arg);
		setTimeout(() => {
		  el.remove(arg);
		}, 4e3);
	  }
	  function fill(data) {
		data.forEach((v, i) => {
		  let c = d.querySelector(`#st${i} [name=bu]`);
		  c.textContent = (v[0] % 2 ? 'ON' : 'OFF');
		  d.querySelectorAll(`#st${i} [type=time]`).forEach((el, i) => { el.value = v[2+i]});
		  d.querySelectorAll(`#st${i} [type=checkbox]`).forEach((el, i) => {v[1] & (1 << i) ? el.checked = true : el.checked = false});
		  d.querySelectorAll(`#st${i}, #st${i} label`).forEach(el => {c.textContent == 'ON' ? el.classList.remove('greyed') : el.classList.add('greyed')});
		});
	  }
	  async function save() {
		if (this.type !== 'submit') this.textContent == 'ON' ? this.textContent = 'OFF' : this.textContent = 'ON';
		let form = new FormData(), data = [];
		d.querySelectorAll(`[id^=st]`).forEach(e => {
		  let x = 0, arr = [d.querySelector(`#${e.id} [name=bu]`).textContent == 'ON' ? '1' : '0'];
		  d.querySelectorAll(`#${e.id} [type=checkbox]`).forEach((el, i) => { if (el.checked) x = x | (1 << i) });
		  arr.push(x.toString());
		  d.querySelectorAll(`#${e.id} [type=time]`).forEach((el,i) => {arr.push(el.value != 0 ? el.value : 0)});
		  data.push(arr);
		});  
		form.append('dTime', JSON.stringify(data));	  
		const resp = await fetch('/timer', {method: 'post', body: form});
		resp.ok&&out('edit');
		const json = await resp.json();
		fill(json);
	  }
	  async function renew(arg = 'time') {
		const resp = await fetch(`timer?switch=${arg}`);
		const array = await resp.json();
		if (arg.startsWith('sun')) {
		  resp.ok&&arg.startsWith('sun&')&&out('edit');
		  array[0].forEach((el, i) => { d.getElementById(`sl${i}`).innerHTML = el;});					
		  d.querySelectorAll('[id^=sr]').forEach((el, i) => {array[1] & (1 << i) ? el.checked = true : el.checked = false});
		  setSun();
		}else {
		  d.querySelector('polygon').style.fill = array[0] == 0 ? '#eee' : '#ff0';
		  btn[0].innerHTML = array[0] == 1 ? '&#9995; ON' : '&#9995; OFF';
		  d.querySelector('time').innerHTML = array[1];
		  btn[2].innerHTML = array[2] == 1 ? '&#10006; Auto inaktiv' : '&#9203; Auto aktiv';
		  d.querySelectorAll(`div:not([id="top"]), label`).forEach(el => {array[2] == 0 ? el.classList.remove('fix') : el.classList.add('fix')});
		}
	  }
	  async function hours(arg = '') {
		const resp = await fetch(`hobbs${arg}`);
		resp.ok&&arg.startsWith('?')&&out('edit');
		const data = await resp.json();
		d.querySelector('strong').innerHTML = data[0] + ' h';
		d.getElementById('watt').value = data[1];
		d.getElementById('kwh').innerHTML = data[2] + ' kWh';
	  }
	</script>
  </head>
  <body>
	<main class="none">
	  <div id="top">
		<button>ON</button>	
		<svg viewBox="0 0 12 15">
		  <polygon points="10.421,6.754 6.498,6.75 12.058,2.357 9.734,2.357 1.687,8.436 5.584,8.436 0,14.02"></polygon>
		</svg>
		<time>00:00:00</time>
		<span id="sun" class="dark">
		  &#9728;&#65039;
		</span>
	  </div>
	  <div id="tip"><div id="info"></div></div>
	  <div id="view" class="none">
		<span>Betriebsstunden <strong></strong></span>
		<span>Verbrauch bei <input id="watt" pattern="[0-9]{1,4}"> Watt <span id="kwh"></span></span>
		<span id="reset">&#10060; Zähler zurücksetzen</span>
	  </div>
	  <div id="option" class="none"></div>
	</main>
  </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;
}
input:not(:placeholder-shown):invalid{
	color:red;
}
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';
}
@media only screen and (max-width: 500px) {
	.ip {
		right: 6em;
		position: relative;
	}
	aside {
		max-width: 50vw;
	}
}
30 Kommentare - Kommentar eintragen
Christoph ✪ ✪ ✪ 19.09.2021
Hallo Fips.

Dein Code ist schon einige Zeit im Einsatz und alles funktioniert super.
Vielen Dank noch mal.
Eine Frage habe ich aber noch:
Gibt es die Möglichkeit beim http- Aufruf der Seite an die IP noch etwas anzuhängen um mit der HTTP-Adresse das Relais zu schalten? Also z.b. 192.168…_/relais=on und dann auch wieder aus?

Vielen Dank schon mal im Voraus.

Christoph

Antwort:
Ja.
Schau dir mal den "HttpClientButton" zur Zeitschaltuhr an.
Dort wird 192.168…_/timer?tog=tog zum schalten gesendet.

Gruß Fips
Tobi ✪ ✪ ✪ 19.09.2021
Hallo, tolle Zeitschaltuhr. Vielen Dank.
Ist es möglich, die Zeitschaltuhr auf einem ESP01s laufen zu lassen?

Antwort:
Das kann ich dir nicht mit Sicherheit sagen, probiere es einfach aus.
Wenn es schön klein sein soll nehme ich einen D1 Mini Pro!

Gruß Fips
Andre ✪ ✪ ✪ 19.08.2021
Hallo Fips,

ich finde deine Projekte super! Ich habe schon ein paar Sachen von dir Übernommen und etwas angepasst für meine Sachen. Jetzt habe ich die Wochenzeitschaltuhr gesehen und finde sie mit dem Sonnenauf/Untergang super, nur scheine ich hier echt zu blöd sein (im gegensatz zu den anderen ). Ich habe folgendes Problem, in der Zeitschaltuhr.ini mosert der Compiler rum über

include LittleFS.h

was er nicht findet. (LittleFS.h: No such file or directory
) Ich haber es wie bei der Doppelten Zeitschaltuhr direkt aus dem Verzeichnis aufgerufen, so dass alle Tabs mit in der Arduino Ide geladen sind.

Probleme habe ich allerings auch (ich hatte es mal auf FS.h versucht) mit der
include AddrList.h
dort habe ich das selbe Problem. (Über die AddrList.h habe ich im Netz noch weniger gefunden)

Ich weiß, ich muss oben auch das LittleFS.h nehmen, da es überall im Script steht.

Wo habe ich hier das Brett vor dem Kopf. Hast du vielleicht einen Tip für mich...

(Es sind die Größer/Kleiner Zeichen davor,danach ... die konnte ich hier leider nicht mit rein schreiben)

Danke.



Antwort:
Welche EspCoreVersion verwendest du?

Gruß Fips
Christoph ✪ ✪ ✪ 29.07.2021
Hallo Fips,

habe gestern das getestet. Funktioniert super.
Danke.
Damit ist mir sehr geholfen.

Gruß
Christoph

Antwort:
Nicht schön, aber wenn es dir hilft ist es ok!

Gruß Fips
Christoph ✪ ✪ ✪ 25.07.2021
Hallo Fips,
danke schon mal.

Geschaltet werden muss eine Steuerung für eine automatische Absperrkette, die dann zu versch. Zeiten hoch und runter fahren soll.
Da man dann im WEB sieht, ob oben (AUS) oder unten (AN) wäre das ein super Code zum Verwenden.
Der Eingang sollte hier nicht länger als 0,5 sec. geschlossen werden, braucht aber natürlich auch etwas, bis die Absperrketten-Steuerung darauf reagiert.

Es müsste ja irgendwo in "Schaltuhr 184-187" was rein, dann passt aber das mit der PINState nicht mehr so gut - glaube ich.

Im Endefekt wäre das ja für alle Geräte etwas, die nur einen Einschalt- und Ausschalt-Impuls benötigen, aber visualisiert werden soll, dass das Gerät dennoch an ist.
Danke trotzdem schon mal.

Christoph


Antwort:
Eine schnelle aber schmutzige Lösung. (delay)
Zeile 162 ändern:
static uint8_t lastmin {CHAR_MAX}, lastState {!ACTIVE};
Zeile 186 und 187 erzetzen:
digitalWrite(DEVICE_PIN, ACTIVE);
delay(500);
digitalWrite(DEVICE_PIN, !ACTIVE);
DEBUG_F(PSTR("Schaltausgang A%s\n"), pinState == ACTIVE ? "n" : "us");

Bleibt noch das Problem das der Esp bei einen Neustart den Status der Absperrketten nicht kennt.

Gruß Fips
Christoph ✪ ✪ ✪ 20.07.2021
Hallo.

Vielen Dank für die tolle Arbeit. Ich würde die Uhr aber so benötigen, dass diese beim Einschaltzeitpunkt nur einen Impuls (0,5 sec) sendet und bei m Ausschaltzeitpunkt auch. Ebenfalls, wenn ich manuell Ein- bzw. Ausschalten möchte.
Ist das irgenwie möglich, das zu integrieren, ohne dass alles komplett umgeschrieben werden muss.
Noch schöner wäre es natürlich, wenn dann in der WEB-Oberfläche trotzdem das Symbol für "An" leuchtet, obwohl nur der Impuls gesendet wurde.
Vielen Dank schon mal.
Gruß
Christoph

Antwort:
Eine erste Idee habe ich, die Zeit zum testen fehlt im Moment. Was soll denn geschaltet werden und muss der Impuls so lang sein?

Gruß Fips
Volker ✪ ✪ ✪ 15.03.2021
Hi,
Ich bin gerade dabei eine Led Stripe Lampe zu bauen und die Zeitschaltuhr ist genau das richtige.
Sie soll zeitgesteuert an und aus gehen.
Dafür brauchte ich noch eine zweite HTML Seite
wo Ich die Farben wächseln kann per Button.
Ein Webserver wo Ich die Farbe einstellen kann habe ich schon und lauft prima. Jetzt fehlt mir nur noch die Zeitschaltuhr.
Hoffe das reicht an Informationen.

Danke


Antwort:
Tut mir leid, ich sehe keine Frage zu meiner Zeitschaltuhr die ich dir beantworten könnte.

Gruß Fips
Volker ✪ ✪ ✪ 12.03.2021
Alles läuft prima.
Möchte gerne die Zeitschaltuhr für einen Led Strip nutzen,
und gerne noch eine HTML seite hinzufügen.
Kannst Du mir einen Tip geben wie ich das mache?

Antwort:
Vielleicht einfach den Strip an den Pin anschließen, der geschaltet wird?
Eine HTML Seite kannst du doch einfach ins Filesystem des Esp laden.
Deine sehr allgemeine Frage lässt sich nicht gezielter beantworten.

Gruß Fips
Harald ❘ 01.02.2021
Hallo Jens,

vor einiger Zeit hat hier mal Jemand gefragt, warum du keine Vergleiche der Art
"mach an, wenn Aktuelle Zeit zwischen Einschaltzeit und Ausschaltzeit liegt"
verwendest.

Das habe ich mich auch gefragt und weil ich diese Funktionalität haben wollte, habe ich sie (ziemlich clumsy) implementiert. Ich berechne die Minuten ab Mitternacht und mit ein bisschen Größer/Kleiner/UND/ODER klappt das dann ganz prima.

Geschaltet wird immer noch nur zur vollen Minute, aber das ist ok.
Mit dieser Änderung kann ich die Schaltuhr in die Steckdose stecken und liegt die aktuelle Zeit zwischen EIN und AUS, geht beim nächsten Minutenwechsel das Licht an, nicht erst am nächsten Tag zur EIN-Zeit.

Hast du Interesse an dieser Erweiterung?

Gruß, Harald

Antwort:
Du kannst es gerne mal schicken!

Gruß Fips
MichT ✪ ✪ ✪ 19.12.2020
Wie kann man in index.html die Anzahl der angezeigten Schaltzeiten einstellen?

In der alten Version konnte man in der function dom() die Variable 'count' modifizieren. Diese function gibt es nicht mehr, die analoge Zeile for (var i = 0; i < array.length; i++) {...} erlaubt keine Zahl, anstatt "array.length.

Gruß von Michael


Antwort:
Die Anzahl der Schaltzeiten werden in der aktuellen Version im Sketch eingestellt.
Diese werden im "Schaltuhr" Tab definiert. "constexpr uint8_t RECORDS = 20;"

Gruß Fips
Kommentar eintragen

*