Update: 2024-02-25

Esp8266 Zeitschaltuhr Singel Arduino Tab.

Zeitschaltuhr.ino

// ****************************************************************
// Arduino IDE Tab Esp8266 Zeitschaltuhr Singel Modular
// created: Jens Fleischer, 2019-07-20
// last mod: Jens Fleischer, 2020-09-21
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266, Relais Modul o. Mosfet IRF3708 o. Fotek SSR-40 DA
// für Relais Modul
// GND an GND
// IN1 an D5 = GPIO14
// VCC an VIN -> je nach verwendeten Esp.. möglich
// Jumper JD-VCC VCC
// alternativ ext. 5V Netzteil verwenden
//
// für Mosfet IRF3708
// Source an GND
// Mosfet1 Gate an D5 = GPIO14
//
// für 3V Solid State Relais
// GND an GND
// SSR1 Input + an D5 = GPIO14
//
// 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 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 Lokalzeit Tab ist zum ausführen der Zeitschaltuhr einzubinden.
// Die Funktion "singleTimerSwitch();" muss im Setup aufgerufen werden.
// Zum schalten muss die Funktion "timerSwitch();" im loop(); aufgerufen werden.
/**************************************************************************************/

//#define SPIFFS LittleFS  // Einkommentieren wenn LittleFS als Filesystem genutzt wird

const auto aktiv = LOW;                       // LOW für LOW aktive Relais oder HIGH für HIGH aktive (zB. SSR, Mosfet) einstellen
const uint8_t relPin = D5;                    // Pin für Relais einstellen
const auto count = 30;                        // Anzahl Schaltzeiten (analog Html Dokument) einstellen 1 bis 60
bool relState{!aktiv};

struct Collection {
  bool fixed;
  byte switchActive[count];
  byte wday[count];
  char switchTime[count * 2][6];
} times;

void singleTimerSwitch() {
  digitalWrite(relPin, !aktiv);
  pinMode(relPin, OUTPUT);
  File file = SPIFFS.open("/ctime.dat", "r");
  if (file && file.size() == sizeof(times)) {                          // Einlesen aller Daten falls die Datei im Spiffs vorhanden und deren Größe stimmt.
    file.read(reinterpret_cast<byte*>(&times), sizeof(times));
    file.close();
  } else {                                                             // Sollte die Datei nicht existieren
    for (auto i = 0; i < count; i++) {
      times.switchActive[i] = 1;                                       // werden alle Schaltzeiten
      times.wday[i] = ~times.wday[i];                                  // und alle Wochentage aktiviert.
    }
  }
  server.on("/timer", HTTP_POST, []() {
    if (server.args() == 1) {
      times.switchActive[server.argName(0).toInt()] = server.arg(0).toInt();
      printer();
      String temp = "\"";
      for (auto& elem : times.switchActive) temp += elem;
      temp += "\"";
      server.send(200, "application/json", temp);
    }
    if (server.hasArg("sTime")) {
      byte i {0};
      char str[count * 14];
      strcpy (str, server.arg("sTime").c_str());
      char* ptr = strtok(str, ",");
      while (ptr != NULL) {
        strcpy (times.switchTime[i++], ptr);
        ptr = strtok(NULL, ",");
      }
      if (server.arg("sDay")) {
        i = 0;
        strcpy (str, server.arg("sDay").c_str());
        char* ptr = strtok(str, ",");
        while (ptr != NULL) {
          times.wday[i++] = atoi(ptr);
          ptr = strtok(NULL, ",");
        }
        printer();
      }
    }
    String temp = "[";
    for (auto& elem : times.switchTime) {
      if (temp != "[") temp += ',';
      temp += (String)"\"" + elem + "\"";
    }
    temp += ",\"";
    for (auto& elem : times.switchActive) {
      temp += elem;
    }
    for (auto& elem : times.wday) {
      temp += "\",\"";
      temp += elem;
    }
    temp += "\"]";
    server.send(200, "application/json", temp);
  });
  server.on("/timer", HTTP_GET, []() {
    if (server.hasArg("tog") && server.arg(0) == "tog") relState = !relState;             // Relais1 Status manuell ändern
    if (server.hasArg("tog") && server.arg(0) == "fix") {
      times.fixed = !times.fixed;                                                         // alle Schalzeiten deaktivieren/aktivieren
      printer();
    }
    server.send(200, "application/json", (String)"[\"" + (relState == aktiv) + "\"," + localTime().substring(1, 11) + ",\"" + times.fixed + "\"]");
    //server.send(200, "application/json", (String)"[[\"" + (relState == aktiv) + "\",\"" + times.fixed + "\"]," + localTime() + "\"]");
  });
}

void printer() {
  File file = SPIFFS.open("/ctime.dat", "w");
  if (file) {
    file.write(reinterpret_cast<byte*>(&times), sizeof(times));
    file.close();
  }
}

void timerSwitch() {
  static uint8_t lastmin {CHAR_MAX};
  static uint8_t lastState {aktiv};
  localTime();                                        // Uhrzeit aktualisieren
  if (tm.tm_min != lastmin && !times.fixed) {
    lastmin = tm.tm_min;
    char buf[6];
    sprintf(buf, "%.2d:%.2d", tm.tm_hour, tm.tm_min);
    for (auto i = 0; i < count * 2; i++) {
      if (times.switchActive[i / 2] && !strcmp(times.switchTime[i], buf)) {
        if (times.wday[i / 2] & (1 << (tm.tm_wday ? tm.tm_wday - 1 : 6))) relState = i & 1 ? !aktiv : aktiv; // Relais Status nach Zeit ändern
      }
    }
  }
  if (relState != lastState) {                       // Relais schalten wenn sich der Status geändert hat
    lastState = relState;
    digitalWrite(relPin, relState);
    Serial.printf("Relais a%s\n", digitalRead(relPin) == aktiv ? "n" : "us");
  }
}

Die Webseite zur Esp8266 Singel Zeitschaltuhr.

zeitschaltuhr.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 {
        padding: .5em .5em .5em .1em;
        box-shadow: 5px 3px 10px #4e4d4d;
        background-color: #333;
        color: #15dfdf;
      }
      div {
        display: flex;
        align-items: center;
        justify-content: space-evenly;
      }
      span {
        padding: 0.5em;
      }
      input {
        background-color: inherit;
        font-size: 3em;
        color: inherit;
        border: solid #555;
        height: auto;
        font-weight: bold;
      }
      label {
        font-size: .9em;
        font-style: italic;
		color: #777;
      }
	  input:checked+label {
        color: #15dfdf;
      }
      div+span {
        margin-left: 3em;
      }
      #top {
        position: sticky;
        top: 0;
        background-color: #333;
        z-index: 1;
      }
	  #bottom {
        position: sticky;
        bottom: .3em;
        padding: .5em 0 0 .5em;
      }
      time {
        text-shadow: 1px 1px 1px #777;
        font-size: 1.4em;
        font-weight: bold;
      }
      #bu, #fix, #tog {
        background-color: #333;
        color: #33cccc;
        cursor: pointer;
        border: outset #999;
        width: 100%;
        margin: 0;
      }
      #tog {
        width: 25%;
      }
      [name^=bu] {
        width: 2em;
        cursor: pointer;
      }
      .none {
        color: #777 !important;
      }
      svg {
        width: 3.5em;
      }
	  u {
        background-color: red !important;
        position: sticky;
        bottom: 4em;
      }
	  .note:after {
        content: "Schaltzeiten gespeichert";
        color: #fff;
      }
      @media only screen and (max-width: 600px) {
        input {
          font-size: 2.4em;
          width: auto;
        }
		div {
		  justify-content: space-between
		}
        div+span {
          margin-left: .2em;
        }
      }
    </style>
    <script>
      var count = 30;					<!-- Anzahl Schaltzeiten(analog Sketch) einstellen 1 bis 60 -->
	  var fixed;
      var d = document;
      d.addEventListener('DOMContentLoaded', () => {
        dom();
        d.querySelector('#bu').addEventListener('click', () => {
          let formData = new FormData();
          let arr = [];
          formData.append('sTime', Array.from(d.querySelectorAll('input[type=time]')).map(x => x.value != 0 ? x.value : 0));
          for (var i = 0; i < count; i++) {
            let x = 0;
            d.querySelectorAll(`input[name=c${i}]`).forEach((el, i) => { if (el.checked) x = x | (1 << i) });
            arr.push(x);
          }
          formData.append(`sDay`, arr);
          send(formData);
        });
        d.querySelector('#tog').addEventListener('click', renew);
        d.querySelector('#fix').addEventListener('click', renew);
        for (var i = 0; i < count;) d.querySelector(`[name=bu${i++}]`).addEventListener('click', setActive);
      }, renew(), send(), setInterval(renew, 1000));
      function dom() {
        var buf = '';
        for (var i = 0; i < count; i++) {
          buf += `<div id="ak${i}"><span name="bu${i}"></span><input type="time" id="sz${i * 2}" value=""><span> -- </span><input type="time" id="sz${i * 2 + 1}" value=""></div><span id="t${i}">`;
          ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"].forEach(v => {
            buf += `<input type="checkbox" name='c${i}'><label> ${v} </label>`;
          });
          buf += "</span>";
        }
        buf += '<div id="bottom"><button class="button" id="bu">&#9200; Zeiten Speichern</button><button class="button" id="fix">--</button></div>';
        d.querySelector('main').insertAdjacentHTML('beforeend', buf);
      }
      function setActive() {
        let formData = new FormData();
        formData.append(this.parentNode.id.substr(2, 5), this.textContent == 'ON' ? '0' : '1');
        send(formData);
      }
      function send(arg) {
        fetch('/timer', {
          method: 'post',
          body: arg
        }).then(resp => {
          if (resp.ok) {
            if (arg && arg.has('sTime')) {
              let el = d.querySelector('u');
              el.classList.add('note');
              setTimeout(() => {
                el.classList.remove('note');
              }, 5e3);
            }
          }
          return resp.json();
        }).then(array => {
          if (array.length > count) {
            array.forEach((v, i) => {
              if (i < count * 2) d.getElementById(`sz${i}`).value = v;
              if (i == count * 2) getActive(v);
              if (i > count * 2) {
                let el = d.getElementsByName(`c${i - count * 2 - 1}`);
                for (let k in el) {
                  v & (1 << k) ? el[k].checked = true : el.checked = false;
                }
              }
            });
          }
          else {
            getActive(array);
          }
        });
      }
      function getActive(arg) {
        for (var i = 0; i < count; i++) {
          if (arg.length > 0) d.querySelector(`[name=bu${i}]`).textContent = (arg[i] % 2 ? 'ON' : 'OFF');
          let el = d.getElementById(`ak${i}`).classList;
          fixed == 0 ? arg[i] % 2 ? el.remove("none") : el.add("none") : el.add("none");;
          el = d.getElementById(`t${i}`).childNodes;
          fixed == 0 ? el.forEach(v => { arg[i] % 2 ? v.classList.remove("none") : v.classList.add("none") }) : el.forEach(v => { v.classList.add("none") });
        }
      }
      function renew(id) {
        if (id) id = id.currentTarget.id;
        fetch(`timer?tog=${id}`).then(resp => {
          return resp.json();
        }).then(array => {
          d.getElementById('color').style.fill = array[0] == 0 ? '#eee' : '#ff0';
          d.getElementById('tog').innerHTML = array[0] == 0 ? '&#9995; ON' : '&#9995; OFF';
          d.querySelector('time').innerHTML = array[1];
          d.getElementById('fix').innerHTML = array[2] == 0 ? "&#10006; Auto inaktiv" : "&#9203; Auto aktiv";
          fixed = array[2];
          if (id == 'fix') fixed == 1 ? getActive(0) : send();
        });
      }
    </script>
  </head>
  <body>
    <h2>Zeitschaltuhr</h2>
    <main>
      <div id="top">
        <button id="tog">--</button>
        <svg viewBox="0 0 12 15">
          <polygon id="color" 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" />
        </svg>
        <time>00:00:00</time>
      </div>
    </main>
    <u></u>
  </body>
</html>