2018-12-08

Esp32 Zeitschaltuhr Dual

Zeitschaltuhr mit NTP Zeitsynchronisation

Automatischer Wechsel zwischen Sommer und Normalzeit

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

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

Esp32 Zeitschaltuhr

Login Manager

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

Esp32 Zeitschaltuhr

Highlight

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

Esp32 Zeitschaltuhr

Funktionen

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

Esp32 Zeitschaltuhr

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

Esp32 Zeitschaltuhr

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

Esp32 Zeitschaltuhr

Anleitung zur Inbetriebnahme

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

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

Download Projekt

Die Webseite zur Esp32 Zeitschaltuhrdual

index.html

<!-- For more information visit: https://fipsok.de -->
<!DOCTYPE HTML>
<html lang="de">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="style32.css">
    <title>Dual Zeitschaltuhr</title>
    <style>
	  div > [id^=del] {
	  margin: .5em;
	  cursor: pointer;
	  }
	  body {
      color: #15dfdf;
	  }
      div {
      display: flex;
      }
      span {
      padding: 0.5em;
      }
      #tog1,#tog2 {
      padding: .7em 1.2em;
      cursor: pointer;
      }
      time {
      text-shadow: 2px 2px 2px black;
      font-size: 1.3em;
      font-weight: bold;
      margin: auto;
      }
      input {
      height: auto;
      font-weight: bold;
      }
      .note:after {
      content: "Schaltzeiten gespeichert";
      }
      form {
      box-shadow: 5px 3px 10px #4e4d4d;
	  min-height: 274px;
      }
      svg {
      height: 4em;
      }
      #bu {
      background-color: #333;
	  color: #33cccc;
	  border: outset #555;
      }
      .tab {
      overflow: hidden;
      }
      .tab button {
      background-color: #999;
	  color: #33cccc;
      border: none;
	  margin-top: 0em;
      transition: 0.8s;
      border-top-right-radius: .5em;
      border-top-left-radius: .5em;
      }
      .tab button:hover {
      background-color: #666;
      }
      .tab button.active {
      background-color: #333;
      }
	  #tab3 {
	  background-color: inherit;
      box-shadow: none;
	  }
	  #ctab3 {
	  padding: 1em 0 0 2em;
	  }
      .tabcontent {
      display: none;
      padding: .5em .7em .5em .1em;
      background-color: #333;
	  min-height: 258px;
      }
      .tabcontent input {
      background-color: inherit;
      font-size: 3em;
	  color: inherit;
	  border: solid #555;
      }
	  .tabcontent [name^=bu] {
      width: 2em;
	  cursor: pointer;
	  }
	  .none {
	  color: #777;
	  }
      @media only screen and (max-width: 600px) {
      .tabcontent input,input {
      font-size: 2em;
      width: auto;
      border: none;
	  }
	  .tab button,#bu,#tog1,#tog2 {
	  width: auto;
	  }
	  svg {
	  height: 3em;
	  }
      }	
    </style>
    <script>      
      var item = 0x14;
	  var d = document;
      d.addEventListener('DOMContentLoaded', () => {
        dom(), renew();
        d.getElementById('bu').addEventListener('click', () => {
          send(new FormData(d.querySelector('form')));
        });
		d.getElementById('delpw').addEventListener('click',  () => {
          window.location = '/config';
        });
		d.getElementById('delall').addEventListener('click', config.bind(this, 'config?delall='));
		for (var i = 1; i < 4; i++) {
          d.getElementById(`tab${i}`).addEventListener('click', openTab);
		  if (i< 3) {
		    d.getElementById(`tog${i}`).addEventListener('click', renew);
		    d.getElementById(`delhm${i}`).addEventListener('click', config.bind(this, `hourmeter?hm${i}=`));
		  }
        }
		for (var i = 0; i < item - 1; i = i + 2) {
		  d.querySelector(`[name=bu${i}]`).addEventListener('click', active);
		}
      },send(), setInterval(renew, 1000));
	  function dom() {
        var buf = '';
        for (var i = 0; i < item; i++) {
          buf += `${i%2 ? `<span> -- </span>` : `<div id="ak${i}"><span name="bu${i}"></span>`}<input type="time" name="sz${i}" value="">${i%2 ? "</div>" : ""}`;
          if (i == item / 2 - 1) {
            d.getElementById('ctab1').insertAdjacentHTML('beforeend', buf);
            buf = '';
          }
          if (i == item - 1) d.getElementById('ctab2').insertAdjacentHTML('beforeend', buf);
        }
      }
	  function active() {
	    let formData = new FormData()
        formData.append(this.parentNode.id.substr(2, 4), this.textContent == 'ON' ? '0' : '1');
	    send(formData);
      }
      function send(arg) {
        fetch('/timer', {
          method: 'POST',
          body: arg
        }).then(response => {
          if(arg && arg.has('sz0'))  {
            var el = d.getElementById('out');
            el.classList.add('note');
            setTimeout(() => {
              el.classList.remove('note');
            }, 5000);
          }
          return response.json();
        }).then(array => {
          array.forEach(function (x, i) {
		    if (i < item) {
			  d.querySelector(`[name=sz${i}]`).value = x;
			} else if (i < item + item/2) {
			  d.querySelector(`[name=bu${(i-item)*2}]`).textContent = (x%2 ? 'ON' : 'OFF');
			  var el = d.getElementById(`ak${(i-item)*2}`);
              x%2 ? el.classList.remove("none") : el.classList.add("none");
			} else {
			  d.getElementById(`hm${i%30}`).innerHTML = x;
			}
          });
        });
      }
      function openTab(event) {
        for (var i = 1; i < 4; i++) {
          d.getElementById(`ctab${i}`).style.display = "none";
          d.getElementById(`tab${i}`).classList.remove("active");
        }
        d.getElementById('c' + event.currentTarget.id).style.display = "block";
        event.target.classList.add("active")
      }
	  function config(arg) {
	    if (confirm('Bist du sicher!')) {
		  fetch(`/${arg}`).then(response => {
          if (arg != 'config?delall=')send();
          });
	    }
	  }
      function renew(event) {
        if (event) event = event.currentTarget.id;
        fetch(`timer?${event}=`).then(response => {
          return response.json();
        }).then(array => {
          d.querySelector('time').innerHTML = array[0];
          d.getElementById('an1').setAttribute('visibility', array[1] == 0 ? "hidden" : "visible");
          d.getElementById('an2').setAttribute('visibility', array[2] == 0 ? "hidden" : "visible");
        });
      }
    </script>
  </head>
  <body>
    <h2>Zeitschaltuhr</h2>
	<main>
      <div class="tab">
        <button class="button active" id="tab1">&#9203; Relais 1</button>
        <button class="button" id="tab2">&#9203; Relais 2</button>
		<button class="button" id="tab3"><time>00:00:00</time></button>
      </div>
      <form>
        <div id="ctab1" class="tabcontent" style="display: block;">
        </div>
        <div id="ctab2" class="tabcontent">
        </div>
		<div id="ctab3" class="tabcontent">
		  <p>
		    <span>Hour meter device 1</span><span id="hm0"></span>h
		  <br>
		    <span>Hour meter device 2</span><span id="hm1"></span>h
		  </p>
		  <div>
	        <span id="delhm1">&#10060; Hour Meter 1 zurücksetzen</span>
		  </div>
		  <span id="delhm2">&#10060; Hour Meter 2 zurücksetzen</span>
		  <div>
	        <span id="delpw">&#127757; Passwort/SSID/Schaltstatus ändern</span>
		  </div>
		  <span id="delall">&#10060; Alle Daten löschen</span>
        </div>
      </form>
	</main>
    <div>
      <button class="button" id="bu">&#9200; Speichern</button>
      <div id="tog1">
        R1
        <svg viewBox="0 0 486 486">
          <use xlink:href="#aus"/>
          <use id="an1" xlink:href="#an"/>
        </svg>
      </div>
      <div id="tog2">
        R2
        <svg viewBox="0 0 486 486">
          <use xlink:href="#aus"/>
          <use id="an2" xlink:href="#an"/>
        </svg>
      </div>
    </div>
    <p id="out"></p>
    <svg style="display: none;">
      <g>
        <path id="aus" d="m 242.19922,113.59961 c -66.6,0.5 -119.59844,54.4 -118.89844,121 0.3,31.7 12.99844,60.50117 33.39844,81.70117 15.8,16.3 26.10156,37.49883 29.60156,59.79883 1.4,9.6 9.8,16.59961 19.5,16.59961 h 74.29883 c 9.8,0 18.09961,-7.09922 19.59961,-16.69922 3.4,-22.3 13.60039,-43.19961 29.40039,-59.59961 20.8,-21.5 33.59961,-50.79961 33.59961,-83.09961 0,-66.4 -54,-120.20117 -120.5,-119.70117 z M 243,147.30078 c 7.5,0 13.5,6.1 13.5,13.5 0,7.5 -6,13.5 -13.5,13.5 -32.7,0 -59.40039,26.59844 -59.40039,59.39844 0,7.5 -6.1,13.5 -13.5,13.5 -7.5,0 -13.5,-6.1 -13.5,-13.5 0,-47.7 38.80039,-86.39844 86.40039,-86.39844 z m 161.60156,80.60156 c 0.1446,-0.31458 -0.015,-0.007 -0.86718,1.74219 0.007,0.003 0.0129,0.007 0.0195,0.01 0.32337,-0.65238 0.71524,-1.46389 0.84765,-1.75196 z m -0.87695,1.57422 c -0.009,0.0212
          -0.0521,0.11666 -0.0605,0.13672 0.022,0.01 0.0444,0.0193 0.0664,0.0293 0.005,-0.0415 -0.004,-0.11015 -0.006,-0.16602 z M 121.33203,341.63672 c 1.17005,1.49197 0.76644,0.70619 0.19141,0.01 -0.0638,-0.002 -0.12757,-0.009 -0.19141,-0.01 z m 239.45117,1.32226 c -0.0193,0.0165 -0.0374,0.0342 -0.0566,0.0508 -0.11686,0.12652 -0.24425,0.24514 -0.35351,0.37695 -0.12067,0.14556 0.15965,-0.1093 0.43164,-0.3711 -0.008,-0.0185 -0.0134,-0.0382 -0.0215,-0.0566 z m -159.58398,67.94141 c -7.6,0 -13.69922,6.09922 -13.69922,13.69922 v 14.20117 c 0,11.4 8.29922,20.69844 19.19922,22.39844 l 3.40039,12.90039 c 1.9,7 8.2,11.90039 15.5,11.90039 h 34.70117 c 7.3,0 13.6,-4.90039 15.5,-11.90039 l 3.5,-12.90039 c 10.8,-1.6 19.09961,-10.99883 19.09961,-22.29883 v -14.20117 c 0,-7.6 -6.10117,-13.69883 -13.70117,-13.79883 z"/>
        <path id="an" d="m 298.4,424.7v14.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.7 c-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.5 C292.3,411,298.4,417.1,298.4,424.7z M362.7,233.3c0,32.3-12.8,61.6-33.6,83.1c-15.8,16.4-26,37.3-29.4,59.6 c-1.5,9.6-9.8,16.7-19.6,16.7h-74.3c-9.7,0-18.1-7-19.5-16.6c-3.5-22.3-13.8-43.5-29.6-59.8c-20.4-21.2-33.1-50-33.4-81.7 c-0.7-66.6,52.3-120.5,118.9-121C308.7,113.1,362.7,166.9,362.7,233.3z M256.5,160.8c0-7.4-6-13.5-13.5-13.5 c-47.6,0-86.4,38.7-86.4,86.4c0,7.4,6,13.5,13.5,13.5c7.4,0,13.5-6,13.5-13.5c0-32.8,26.7-59.4,59.4-59.4 C250.5,174.3,256.5,168.3,256.5,160.8z M243,74.3c7.4,0,13.5-6,13.5-13.5V13.5c0-7.4-6-13.5-13.5-13.5s-13.5,6-13.5,13.5v47.3 C229.5,68.3,235.6,74.3,243,74.3z M84.1,233.2c0-7.4-6-13.5-13.5-13.5H23.3c-7.4,0-13.5,6
          -13.5,13.5c0,7.4,6,13.5,13.5,13.5h47.3 C78.1,246.7,84.1,240.7,84.1,233.2z M462.7,219.7h-47.3c-7.4,0-13.5,6-13.5,13.5c0,7.4,6,13.5,13.5,13.5h47.3 c7.4,0,13.5-6,13.5-13.5C476.2,225.8,470.2,219.7,462.7,219.7z M111.6,345.6l-33.5,33.5c-5.3,5.3-5.3,13.8,0,19.1 c2.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.1C125.4,340.3,116.8,340.3,111.6,345.6z M364.9,124.8 c3.4,0,6.9-1.3,9.5-3.9l33.5-33.5c5.3-5.3,5.3-13.8,0-19.1c-5.3-5.3-13.8-5.3-19.1,0l-33.5,33.5c-5.3,5.3-5.3,13.8,0,19.1 C358,123.5,361.4,124.8,364.9,124.8z M111.6,120.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.1L97.1,68.2 c-5.3-5.3-13.8-5.3-19.1,0c-5.3,5.3-5.3,13.8,0,19.1L111.6,120.8z M374.4,345.6c-5.3-5.3-13.8-5.3-19.1,0c-5.3,5.3-5.3,13.8,0,19.1 l33.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.1L374.4,345.6z" fill="#ffff00"/>
      </g>
    </svg>
  </body>
</html>

Der Sketch Zeitschaltuhrdual übersichtlich aufgeteilt in Tabs.

Zeitschaltuhrdual32.ino

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

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

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

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

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

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

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

void loop() {
  ArduinoOTA.handle();
  server.handleClient();
  if (millis() < 0x2FFF || millis() > 0xFFFFF0FF) runtime();
  static auto letzteMillis = 0;
  auto aktuelleMillis = millis();
  if (aktuelleMillis - letzteMillis >= 1e3) {
    letzteMillis = aktuelleMillis;
    localTime();
  }
  dualSchaltuhr();
  reStation();
}

Admin.ino

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

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

#include <rom/rtc.h>

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

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

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

void handleonce() {
  if (server.arg(0) != "") {
    WiFi.setHostname(server.arg(0).c_str());
    File file = SPIFFS.open("/config.json", FILE_WRITE);
    file.printf("[\"%s\"]", WiFi.getHostname());
    file.close();
  }
  String fname = String(__FILE__).substring( 3, String(__FILE__).lastIndexOf ('\\'));
  String temp = "{\"File\":\"" + fname.substring(fname.lastIndexOf ('\\') + 1, fname.length()) + "\", \"Build\":\"" +  (String)__DATE__ + " " + (String)__TIME__ +
                "\", \"LocalIP\":\"" +  WiFi.localIP().toString() + "\", \"Hostname\":\"" + WiFi.getHostname() + "\", \"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() + "\", \"Reset1\":\"" + resetReason[rtc_get_reset_reason(0)] +
                "\", \"Reset2\":\"" + resetReason[rtc_get_reset_reason(1)] + "\", \"CpuFreqMHz\":\"" + ESP.getCpuFreqMHz() +
                "\", \"FreeHeap\":\"" + formatBytes(ESP.getFreeHeap()) + "\", \"ChipSize\":\"" +  formatBytes(ESP.getFlashChipSize()) +
                "\", \"ChipSpeed\":\"" + ESP.getFlashChipSpeed() / 1000000 + "\", \"ChipMode\":\"" + flashChipMode[ESP.getFlashChipMode()] +
                "\", \"SdkVersion\":\"" + ESP.getSdkVersion() + "\"}";
  server.send(200, "application/json", temp);     // Json als Objekt
}

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

Connect.ino

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

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

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

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

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

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

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

Dualschaltuhr.ino

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

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

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

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

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

Lokalzeit.ino

// ****************************************************************
// Sketch Esp32 Lokalzeit Modular(Tab)
// created: Jens Fleischer, 2018-07-15
// last mod: Jens Fleischer, 2018-12-08
// ****************************************************************
// Hardware: Esp32
// Getestet auf: ESP32 Dev Module
/******************************************************************
  Copyright (c) 2018 Jens Fleischer. All rights reserved.

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

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

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

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

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

Spiffs.ino

// ****************************************************************
// Sketch Esp32 Datei Manager Modular(Tab)
// created: Jens Fleischer, 2018-07-06
// last mod: Jens Fleischer, 2018-11-21
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp32
// Geprüft: mit 4MB Flash
// Getestet auf: ESP32 NodeMCU-32s
/******************************************************************
  Copyright (c) 2018 Jens Fleischer. All rights reserved.

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

const char Helper[] PROGMEM = "<form method='POST' action='/upload' enctype='multipart/form-data'><input type='file'name='upload'>\
<input type='submit' value='Upload'></form>Lade die spiffs.html hoch.";
const char* Header = "HTTP/1.1 303 OK\r\nLocation:spiffs.html\r\nCache-Control: no-cache\r\n\r\n";

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

void handleList() {               // Senden aller Daten an den Client
  File root = SPIFFS.open("/");
  String temp = "{\"dir\":[";
  File f = root.openNextFile();
  while (f) {                            // Auflistung aller im Spiffs vorhandenen Dateien
    if (temp != "{\"dir\":[") temp += ',';
    temp += "{\"name\":\"" + String(f.name() + 1) + "\",\"size\":\"" + formatBytes(f.size()) + "\"}";
    f = root.openNextFile();
  }
  temp += ",{\"usedBytes\":\"" + formatBytes(SPIFFS.usedBytes() * 1.05) + "\"," +              // Berechnet den verwendeten Speicherplatz + 5% Sicherheitsaufschlag
          "\"totalBytes\":\"" + formatBytes(SPIFFS.totalBytes()) + "\",\"freeBytes\":\"" +     // Zeigt die Größe des Speichers
          (SPIFFS.totalBytes() - (SPIFFS.usedBytes() * 1.05)) + "\"}]}";                       // Berechnet den freien Speicherplatz + 5% Sicherheitsaufschlag
  server.send(200, "application/json", temp);
}

bool handleFile(String&& path) {
  if (server.hasArg("delete")) {
    SPIFFS.remove(server.arg("delete"));        // Datei löschen
    server.sendContent(Header);
    return true;
  }
  if (!SPIFFS.exists("/spiffs.html"))server.send(200, "text/html", Helper); //Upload the spiffs.html
  if (path.endsWith("/")) path += "index.html";
  return SPIFFS.exists(path) ? ({File f = SPIFFS.open(path, "r"); server.streamFile(f, contentType(path)); f.close(); true;}) : false;
}

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

void formatSpiffs() {       //Formatiert den Speicher
  SPIFFS.format();
  server.sendContent(Header);
}

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

const String &contentType(String& filename) {                   // ermittelt den Content-Typ
  if (filename.endsWith(".htm") || filename.endsWith(".html")) filename = "text/html";
  else if (filename.endsWith(".css")) filename = "text/css";
  else if (filename.endsWith(".js")) filename = "application/javascript";
  else if (filename.endsWith(".json")) filename = "application/json";
  else if (filename.endsWith(".png")) filename = "image/png";
  else if (filename.endsWith(".gif")) filename = "image/gif";
  else if (filename.endsWith(".jpg")) filename = "image/jpeg";
  else if (filename.endsWith(".ico")) filename = "image/x-icon";
  else if (filename.endsWith(".xml")) filename = "text/xml";
  else if (filename.endsWith(".pdf")) filename = "application/x-pdf";
  else if (filename.endsWith(".zip")) filename = "application/x-zip";
  else if (filename.endsWith(".gz")) filename = "application/x-gzip";
  else filename = "text/plain";
  return filename;
}

bool freeSpace(uint16_t const& printsize) {
  DEBUG_F(formatBytes(SPIFFS.totalBytes() - (SPIFFS.usedBytes() * 1.05)) + " im Spiffs frei");
  return (SPIFFS.totalBytes() - (SPIFFS.usedBytes() * 1.05) > printsize) ? true : false;
}

Stundenzaehler.ino

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

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

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

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

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

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

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

*