2018-11-28
Funktioniert ab Core Version 2.4.0

Esp8266 Zeitschaltuhr Dual

Zeitschaltuhr mit NTP Zeitsynchronisation

Automatischer Wechsel zwischen Sommer und Normalzeit

Betriebsstundenzähler für die Angeschlossenen Verbraucher

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

Esp8266 Zeitschaltuhr

Highlight

Der Sketch Zeitschaltuhrdual ist für LOW und HIGH aktive Relais, Solid State Relais oder Mosfet geeignet. Dies muss vor dem Hochladen einmalig im Sketch, im Tab Dualschaltuhr.ino, eingestellt werden. Die optische Schaltzustandsanzeige ist gleichzeitig der Button zum manuellen Ein-/Ausschalten der Ausgänge.

Esp32 Zeitschaltuhr

Eine Betriebsstundenanzeige der Angeschlossenen Geräte erfolgt auf der "Admin" Webseite.

Esp32 Zeitschaltuhr

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

Die Schaltzeiten können mittels Schaltfläche ON/OFF aktiviert oder deaktiviert werden. Ein-und Ausschaltzeiten werden in einer Textdatei im Spiffs des Esp.. 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

Anleitung zur Inbetriebnahme

Zuerst im "Connect" Tab deine Wlan Zugangsdaten eingeben, anschliesend im "Dualschaltuhr" Tab die Variable "aktiv" auf HIGH oder LOW setzen. "const auto aktiv = HIGH;" Je nachdem welche Komponenten du an den Ausgängen betreiben möchtest. Eventuell den NTP Zeitserver in der Zeit.ino ändern. Anschließend den Sketch hochladen.
Im Seriellen Monitor wird die IP des ESP.. angezeigt. "deineIP/spiffs.html" Kopiere diese URL in die Adresszeile deines Browsers und verbinde dich mit deinem ESP8266.
Falls sich im Spiffs (Speicher) des Esp8266 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 Esp8266 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="style.css">
    <title>Dual Zeitschaltuhr</title>
    <style>
	  body {
      color: #15dfdf;
	  }
      div {
      display: flex;
      }
      span {
      padding: 0.5em;
      }
      #tog1,#tog2 {
      padding: .7em 1.2em;
      cursor: pointer;
	  color: #777;
      }
      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";
	  color: #777;
      }
      form {
      box-shadow: 5px 3px 10px #4e4d4d;
      }
      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;
      }
      .tabcontent {
      display: none;
      padding: .5em .7em .5em .1em;
      background-color: #333;
      }
      .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>      
      document.addEventListener('DOMContentLoaded', () => {
        dom(), renew();
        document.querySelector('#bu').addEventListener('click', () => {
          send(new FormData(document.querySelector('form')));
        });
        document.querySelector('#tab1').addEventListener('click', openTab);
        document.querySelector('#tab2').addEventListener('click', openTab);
        document.querySelector('#tog1').addEventListener('click', renew);
        document.querySelector('#tog2').addEventListener('click', renew);
		for (var i = 0; i < 19; i = i + 2) {
		  document.querySelector(`[name=bu${i}]`).addEventListener('click', active);
		}
      },send(), setInterval(renew, 1000));
	  function dom() {
        var buf = '';
        for (var i = 0; i < 0x14; 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 == 9) {
            document.querySelector('#ctab1').insertAdjacentHTML('beforeend', buf);
            buf = '';
          }
          if (i == 19) document.querySelector('#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 = document.querySelector('p');
            el.classList.add('note');
            setTimeout(() => {
              el.classList.remove('note');
            }, 5000);
          }
          return response.json();
        }).then(array => {
          array.forEach(function (x, i) {
		    if (i < 20) document.querySelector(`[name=sz${i}]`).value = x;
			if (i >= 20) {
			  document.querySelector(`[name=bu${(i-20)*2}]`).textContent = (x%2 ? 'ON' : 'OFF');
			  var el = document.getElementById(`ak${(i-20)*2}`);
              x%2 ? el.classList.remove("none") : el.classList.add("none");
			}
          });
        });
      }
      function openTab(event) {
        var a = event.target.id.charAt(3)%2+1;
        document.getElementById(`ctab${a}`).style.display = "none";
        document.getElementById(`tab${a}`).classList.remove("active");
        document.getElementById('c' + event.target.id).style.display = "block";
        event.target.classList.add("active")
      }
      function renew(event) {
        if (event) event = event.currentTarget.id;
        fetch(`timer?${event}=`).then(response => {
          return response.json();
        }).then(array => {
          document.querySelector('time').innerHTML = array[0];
          document.querySelector('#an1').setAttribute('visibility', array[1] == 0 ? "hidden" : "visible");
          document.querySelector('#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>
		<time>00:00:00</time>
      </div>
      <form>
        <div id="ctab1" class="tabcontent" style="display: block;">
        </div>
        <div id="ctab2" class="tabcontent">
        </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></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 zur Esp8266 Zeitschaltuhr Dual übersichtlich aufgeteilt in Tabs.

Zeitschaltuhrdual.ino

// ****************************************************************
// Sketch Esp8266 Webserver Modular(Tab)
// created: Jens Fleischer, 2018-05-16
// last mod: Jens Fleischer, 2018-11-25
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Switch, Sonoff Dual
/******************************************************************
  Copyright (c) 2018 Jens Fleischer. All rights reserved.

  This file is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.
  This file is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.
*******************************************************************/
// Der WebServer Tab ist der Haupt Tab mit "setup" und "loop".
// #include "FS.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
/**************************************************************************************/

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

ESP8266WebServer server(80);

struct tm tm;

void setup() {
  Serial.begin(115200);
  delay(100);
  Serial.printf("\nSketchname: %s\nBuild: %s\n%s\n\n", (__FILE__), (__TIMESTAMP__), ESP.getFullVersion().c_str());
  ArduinoOTA.begin();

  spiffs();
  admin();
  Connect();
  setupTime();
  setupSchaltUhr();
  setupHobbsMeter();
  ArduinoOTA.onStart([]() {
    speichern();                  // vor dem Sketch Update Betriebsstunden in Datei schreiben
  });
  server.begin();
}

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;
    Zeitstempel();
  }
  dualSchaltuhr();
}

Admin.ino

// ****************************************************************
// Sketch Esp8266 Admin Modular(Tab)
// created: Jens Fleischer, 2018-05-09
// last mod: Jens Fleischer, 2018-09-07
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Geprüft: von 1MB bis 16MB Flash
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Switch, Sonoff Dual
/******************************************************************
  Copyright (c) 2018 Jens Fleischer. All rights reserved.

  This file is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.
  This file is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Admin 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.
// Die Spiffs.ino muss im ESP8266 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();"
/**************************************************************************************/

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

void admin() {                          // Funktionsaufruf "admin();" muss im Setup eingebunden werden
  File file = SPIFFS.open("/config.json", "r");
  if (file) {
    String newhostname = file.readStringUntil('\n');
    if (newhostname != "") {
      WiFi.hostname(newhostname.substring(1, newhostname.length() - 1));
      file.close();
      ArduinoOTA.setHostname(WiFi.hostname().c_str());
    }
  }
  server.on("/admin/renew", handlerenew);
  server.on("/admin/once", handleonce);
  server.on("/reconnect", []() {
    server.send(204, "", "");
    WiFi.reconnect();
  });
  server.on("/restart", []() {
    server.send(204, "", "");
    speichern();      //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() + "\",\"" + analogRead(A0) + "\",\"" + WiFi.RSSI() + "\"]");     // Json als Array
}
/*
  ADC_MODE(ADC_VCC);
  void handlerenew() {   // Zum Lesen der Modulversorgungsspannung (VCC), verwende ESP.getVcc()
  server.send(200, "application/json", "[\"" + runtime() + "\",\"" + ESP.getVcc() + "\",\"" + WiFi.RSSI() + "\"]");     // Json als Array
  }
*/
void handleonce() {
  if (server.arg(0) != "") {
    WiFi.hostname(server.arg(0));
    File f = SPIFFS.open("/config.json", "w");                    // Datei zum schreiben öffnen
    f.printf("\"%s\"\n", WiFi.hostname().c_str());
    f.close();
  }
  char *pos = strrchr((__FILE__), strstr (__FILE__, "\\") ? '\\' : '/');  *pos = '\0';
  String temp = "{\"File\":\"" + String(__FILE__).substring(String(__FILE__).lastIndexOf (strstr (__FILE__, "\\") ? '\\' : '/') + 1, String(__FILE__).length()) +
                "\", \"Build\":\"" +  String(__DATE__) + " " + String(__TIME__) + "\", \"SketchSize\":\"" + formatBytes(ESP.getSketchSize()) +
                "\", \"SketchSpace\":\"" + formatBytes(ESP.getFreeSketchSpace()) + "\", \"LocalIP\":\"" +  WiFi.localIP().toString() +
                "\", \"Hostname\":\"" + WiFi.hostname() + "\", \"SSID\":\"" + WiFi.SSID() + "\", \"GatewayIP\":\"" +  WiFi.gatewayIP().toString() +
                "\", \"Channel\":\"" +  WiFi.channel() + "\", \"MacAddress\":\"" +  WiFi.macAddress() + "\", \"SubnetMask\":\"" +  WiFi.subnetMask().toString() +
                "\", \"BSSID\":\"" +  WiFi.BSSIDstr() + "\", \"ClientIP\":\"" + server.client().remoteIP().toString() + "\", \"DnsIP\":\"" + WiFi.dnsIP().toString() +
                "\", \"ResetReason\":\"" + ESP.getResetReason() + "\", \"CpuFreqMHz\":\"" + F_CPU / 1000000 + "\", \"FreeHeap\":\"" + formatBytes(ESP.getFreeHeap()) +
                "\", \"ChipSize\":\"" +  formatBytes(ESP.getFlashChipSize()) + "\", \"ChipSpeed\":\"" + ESP.getFlashChipSpeed() / 1000000 +
                "\", \"ChipMode\":\"" + flashChipMode[ESP.getFlashChipMode()] + "\", \"CoreVersion\":\"" + ESP.getCoreVersion() +
                "\", \"SdkVersion\":\"" + ESP.getSdkVersion() +  zusatz() + "\"}";
  server.send(200, "application/json", temp);     // Json als Objekt
}

String runtime() {
  static uint8_t rolloverCounter = 0;
  static uint32_t letzteMillis = 0;
  uint32_t aktuelleMillis = millis();
  if (aktuelleMillis < letzteMillis) {       // prüft Millis Überlauf
    rolloverCounter++;
  }
  letzteMillis = aktuelleMillis;
  uint32_t 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 Esp8266 Connect Modular(Tab) mit optischer Anzeige
// created: Jens Fleischer, 2018-04-08
// last mod: Jens Fleischer, 2018-08-14
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// 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 Connect sollte als Tab eingebunden werden.
// #include <ESP8266WebServer.h> muss im Haupttab aufgerufen werden
// Die Funktionalität des ESP8266 Webservers ist erforderlich.
// Die Funktion "Connect();" 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";             // << kann 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
#endif

void Connect() {      // Funktionsaufruf "Connect();" 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 oder Passwort in den Flash geschrieben werden sollen
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
#ifdef CONFIG
  WiFi.config(staticIP, gateway, subnet);
#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);
    i++;
    Serial.printf(" %i sek\n", i);
    if (i > 9) {
      Serial.print("\nVerbindung zum AP fehlgeschlagen !\n\n");
      ESP.restart();
    }
  }
  Serial.println("\nVerbunden mit: " + WiFi.SSID());
  Serial.printf("\nGib diese URL in deinem Browser ein: %s/spiffs.html\n\n",WiFi.localIP().toString().c_str());
}

Dualschaltuhr.ino

In diesem Tab einstellen ob die Ausgänge LOW oder HIGH aktiv geschaltet werden.


// ****************************************************************
// Sketch Esp8266 Zeitschaltuhr Dual Modular(Tab)
// created: Jens Fleischer, 2018-11-25
// last mod: Jens Fleischer, 2018-11-26
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266, Relais Modul o. Mosfet IRF3708 o. Fotek SSR-40 DA
// für Relais Modul
// GND an GND
// IN1 an D5 = GPIO14
// IN2 an D6 = GPIO12
// VCC an VIN -> je nach verwendeten Esp.. möglich
// Jumper JD-VCC VCC
// alternativ ext. 5V Netzteil verwenden
//
// für Mosfet IRF3708
// Source an GND
// Gate an D5 = GPIO14
// Gate an D6 = GPIO12
//
// für 3V Solid State Relais
// GND an GND
// SSR1 Input + an D5 = GPIO14
// SSR2 Input + an D6 = GPIO12
//
// 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 Zeitschaltuhr 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 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.
/**************************************************************************************/
const auto aktiv = HIGH;                   // LOW für LOW aktive Relais oder HIGH für HIGH aktive (zB. SSR, Mosfet) einstellen
const uint8_t relPin[] = {D5, D6};         // Pin für Relais einstellen
const auto item = 20;
char switchTime[item][6];
uint8_t switchActive[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
bool relState[] = {!aktiv, !aktiv};

void setupSchaltUhr() {
  for (auto pin : relPin) digitalWrite(pin, !aktiv), pinMode(pin, OUTPUT);
  File file = SPIFFS.open("/szeit.txt", "r");
  if (file) {
    for (auto i = 0; i < item; i++) {
      file.readBytesUntil('\n', switchTime[i], sizeof(switchTime[i]));
    }
    for (auto i = 0; i < item / 2; i++) {
      switchActive[i] = file.parseInt();
    }
    file.close();
  }
  server.on("/timer", HTTP_POST, []() {
    if (server.args() == 1) {
      switchActive[server.argName(0).toInt() / 2] = server.arg(0).toInt();
      printer();
    }
    if (server.hasArg("sz0")) {
      for (auto i = 0; i < server.args(); i++) {
        strcpy (switchTime[i], server.arg(i).c_str());
      }
      printer();
    }
    String temp = "[";
    for (auto i = 0; i < item; i++) {
      if (temp != "[") temp += ',';
      temp += (String)"\"" + switchTime[i] + "\"";
    }
    for (auto i = 0; i < item / 2; i++) {
      temp += (String)",\"" + switchActive[i] + "\"";
    }
    temp += "]";
    server.send(200, "application/json", temp);
  });
  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", "[\"" + Zeitstempel() + "\",\"" + (relState[0] ? aktiv : !aktiv) + "\",\"" + (relState[1] ? aktiv : !aktiv) + "\"]");
  });
}
void printer() {
  File file = SPIFFS.open("/szeit.txt", "w");
  if (file) {
    for (auto i = 0; i < item; i++) {
      file.printf("%s\n", switchTime[i]);
    }
    for (auto i = 0; i < item / 2; i++) {
      file.printf("%d\n", switchActive[i]);
    }
    file.close();
  }
}
void dualSchaltuhr() {
  static uint8_t oldmin = 60;
  static uint8_t oldState[] = {aktiv, aktiv};
  hobbsMeter(relState[0], relState[1]);
  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; i++) {
      if (i < item / 2) {
        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]);
      Serial.println(digitalRead(relPin[i]) == aktiv ? (String)"Relais" + (1 + i) + " an" : (String)"Relais" + (1 + i) + " aus");
    }
  }
}

Spiffs.ino

// ****************************************************************
// Sketch Esp8266 Dateiverwaltung Modular(Tab)
// created: Jens Fleischer, 2018-04-01
// last mod: Jens Fleischer, 2018-09-29
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Geprüft: von 1MB bis 16MB Flash
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Switch, Sonoff Dual
/******************************************************************
  Copyright (c) 2018 Jens Fleischer. All rights reserved.

  This file is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.
  This file is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Spiffs 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.
// "server.onNotFound()" darf nicht im Setup des ESP8266 Webserver stehen.
// Die Funktion "spiffs();" muss im Setup aufgerufen werden.
/**************************************************************************************/

const char Header[] PROGMEM = "HTTP/1.1 303 OK\r\nLocation:spiffs.html\r\nCache-Control: no-cache\r\n\r\n";
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.";

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

void handleList() {                         // Senden aller Daten an den Client
  FSInfo fs_info;  SPIFFS.info(fs_info);    // Füllt FSInfo Struktur mit Informationen über das Dateisystem
  Dir dir = SPIFFS.openDir("/");            // Auflistung aller im Spiffs vorhandenen Dateien
  String temp = "[";
  while (dir.next()) {
    if (temp != "[") temp += ',';
    temp += "{\"name\":\"" + dir.fileName().substring(1) + "\",\"size\":\"" + formatBytes(dir.fileSize()) + "\"}";
  }
  temp += ",{\"usedBytes\":\"" + formatBytes(fs_info.usedBytes * 1.05) + "\"," +             // Berechnet den verwendeten Speicherplatz + 5% Sicherheitsaufschlag
          "\"totalBytes\":\"" + formatBytes(fs_info.totalBytes) + "\",\"freeBytes\":\"" +    // Zeigt die Größe des Speichers
          (fs_info.totalBytes - (fs_info.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);     // ermöglicht das hochladen der 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 handleUpload() {                            // Dateien vom Rechnenknecht oder Klingelkasten ins SPIFFS schreiben
  static File fsUploadFile;                      // Hält den aktuellen Upload
  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);
    }
    printf("handleFileUpload Name: /%s\n", upload.filename.c_str());
    fsUploadFile = SPIFFS.open("/" + server.urlDecode(upload.filename), "w");
  } else if (upload.status == UPLOAD_FILE_WRITE) {
    printf("handleFileUpload Data: %u\n", upload.currentSize);
    if (fsUploadFile)
      fsUploadFile.write(upload.buf, upload.currentSize);
  } else if (upload.status == UPLOAD_FILE_END) {
    if (fsUploadFile)
      fsUploadFile.close();
    printf("handleFileUpload Size: %u\n", 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) {            // Funktion um beim speichern in Logdateien zu prüfen ob noch genügend freier Platz verfügbar ist.
  FSInfo fs_info;   SPIFFS.info(fs_info);              // Füllt FSInfo Struktur mit Informationen über das Dateisystem
  //printf("Funktion: %s meldet in Zeile: %d FreeSpace: %s\n",__PRETTY_FUNCTION__,__LINE__,formatBytes(fs_info.totalBytes - (fs_info.usedBytes * 1.05)).c_str());
  return (fs_info.totalBytes - (fs_info.usedBytes * 1.05) > printsize) ? true : false;
}

Stundenzaehler.ino

// ****************************************************************
// Sketch Esp8266 Betriebsstundenzähler Dual Modular(Tab)
// created: Jens Fleischer, 2018-11-25
// last mod: Jens Fleischer, 2018-11-25
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
//
// 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 Betriebsstundenzähler sollte als Tab eingebunden werden.
// #include "FS.h" muss im Haupttab aufgerufen werden
// Die Funktion "setupHobbsMeter();" muss im Setup aufgerufen werden.
// Der Admin Tab ist zum ausführen erforderlich.
/**************************************************************************************/

uint32_t totalmin[2], lastmin[2];

void setupHobbsMeter() {
  File file = SPIFFS.open("/betrieb.txt", "r");
  if (file) {
    totalmin[0] = file.parseInt();
    totalmin[1] = file.parseInt();
    lastmin[0] = totalmin[0];
    lastmin[1] = totalmin[1];
    file.close();
  }
}

void hobbsMeter(bool &state_0, bool &state_1) {     // Aufrufen mit Relais Status
  static uint32_t letzteMillis, previousMillis;
  auto aktuelleMillis = millis();
  if (aktuelleMillis - letzteMillis >= 6e4) {
    letzteMillis = 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 - previousMillis >= 1728e5 && (totalmin[0] != lastmin[0] || totalmin[1] != lastmin[1])) {       // aller 2 Tage Betriebsstunden in Datei schreiben
    previousMillis = aktuelleMillis;
    lastmin[0] = totalmin[0];
    lastmin[1] = totalmin[1];
    speichern();
  }
}
String zusatz() {
  return "\", \"HourMeter1\":\"" + operatingTime(totalmin[0]) + "\", \"HourMeter2\":\"" +  operatingTime(totalmin[1]);
}
String operatingTime(uint32_t &tmin) {
  char buf[9];
  snprintf(buf, sizeof(buf), "%d,%d", tmin / 60, tmin / 6 % 10);
  return buf;
}

void speichern() {
  File file = SPIFFS.open("/betrieb.txt", "w");                    // Betriebstunden(minuten) speichern
  file.printf("%ld\n%ld\n", totalmin[0], totalmin[1]);
}

Zeit.ino

// ****************************************************************
// Sketch Esp8266 Zeitstempel Modular(Tab)
// created: Jens Fleischer, 2018-07-10
// last mod: Jens Fleischer, 2018-11-25
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Switch, Sonoff Dual
/******************************************************************
  Copyright (c) 2018 Jens Fleischer. All rights reserved.

  This file is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.
  This file is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Zeitstempel 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.
/**************************************************************************************/

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

void setupTime() {
  configTime(0, 0, ntpServer[0]);                       // deinen NTP Server einstellen (von 0 - 5 aus obiger Liste)
  setenv("TZ", "CET-1CEST,M3.5.0/02,M10.5.0/03", 1);    // https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
  delay(300);
  server.on("/zeit", []() {
    server.send(200, "application/json",  "\"" + Zeitstempel() + "\"");
  });
}

String Zeitstempel() {
  static char buf[20];
  static time_t lastsek = 0;
  time_t now = time(&now);
  localtime_r(&now, &tm);
  static time_t lastday = 0;
  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;
      configTime(0, 0, ntpServer[0]);                   // deinen NTP Server einstellen (von 0 - 5 aus obiger Liste)
      delay(250);
    }
  }
  return buf;
}

Kommentar eintragen

*