Update: 2021-08-28
Esp8266 Wochen Zeitschaltuhr Dual
Webbasierte Zeitschaltuhr mit NTP Zeitsynchronisation
Automatischer Wechsel zwischen Sommer und Normalzeit
Betriebsstundenzähler für die Angeschlossenen Verbraucher
Ereignisdatenspeicher für Schaltzeiten und Auslöser
Eins vorweg, ich übernehme keinerlei Verantwortung falls ihr diesen Sketch nutzt um mittels Relais oder SSR Netzspannung zu schalten.
Ich stelle bewusst keine Schaltpläne dazu zur Verfügung. Wendet euch dafür an eine Elektrofachkraft mit entsprechender Ausbildung.
Getestet habe ich mit den, bei Arduino-Jüngern, beliebten blauen Relais Modulen, einem Mosfet "IRF3708" und einem Solid State Relais "Fotek SSR-40 DA".

Highlight
Der Sketch Zeitschaltuhrdual ist für LOW und HIGH aktive Relais, Solid State Relais oder Mosfet geeignet.
Es lassen sich bis zu 20 Ein-/Aus-Schaltzeiten pro Gerät einstellen.
Die optische Schaltzustandsanzeige ist gleichzeitig der Button zum manuellen Ein-/Ausschalten der Ausgänge.
Monatliche CSV Logdatei pro Relais zum Auswerten mit Exel.

Es wird für jeden Monat pro Relais eine neue CSV Datei angelegt, in der die Schaltzeiten und der Initiator des Schaltvorgangs gespeichert werden.
Die jeweils 11 Monate alte Logdatei wird automatisch gelöscht.
Ansicht Exel (formatiert)
Apr |
An |
Initiator |
Aus |
Initiator |
1. |
04:38:00 |
Programm |
04:40:00 |
Programm |
1. |
21:36:20 |
192.168.178.36 |
21:36:41 |
192.168.178.36 |
1. |
22:00:11 |
192.168.178.45 |
22:00:35 |
192.168.178.45 |
2. |
04:38:00 |
Programm |
04:40:00 |
Programm |
3. |
04:38:00 |
Programm |
04:40:00 |
Programm |
3. |
20:47:07 |
192.168.178.36 |
20:47:48 |
192.168.178.36 |
3. |
20:52:16 |
192.168.178.36 |
20:52:51 |
192.168.178.36 |
4. |
04:38:00 |
Programm |
04:40:00 |
Programm |
5. |
04:38:00 |
Programm |
04:40:00 |
Programm |

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

Funktionen
Durch Klick/Touch auf die Tabs kannst du zwischen beiden Relais navigieren.
Es wird die zugehörige Registerkarte mit den Ein-und Ausschaltzeiten und den Wochentagen eingeblendet.
Die Schaltzeiten können mittels Schaltfläche ON/OFF aktiviert oder deaktiviert werden.
Ein-und Ausschaltzeiten werden in der szeit.dat im Spiffs des Esp.. gespeichert.
Eingegebene Zeitperioden und aktivierte/deaktivierte Wochentage werden durch Klick/Touch des Speichern Buttons zum ESP.. gesendet.
Ein erfolgreiches speichern der Schaltzeiten auf dem Esp8266 Webserver wird im Webinterface für 5 Sekunden signalisiert.

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.
Du kannst die Anzahl der Schaltzeiten pro Relais an deinen Bedarf anpassen. Diese müssen im "Dualschaltuhr" Tab und in der "index.html" übereinstimmen.
Eventuell den NTP Zeitserver und die Zeitzone, für deinen Standort, in der "Lokalzeit.ino" ändern.
Anschließend den Sketch hochladen.
Im Seriellen Monitor wird die IP des ESP.. angezeigt. "deineIP/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" , das
Favicon und die
"index.html"
in den Speicher deines Esp... uploaden musst. Klick/Touch nun auf die index.html um zur Zeitschaltuhr zu kommen.
Download Projekt
Wochenzeitschaltuhr8266.ino
// ****************************************************************
// Sketch Esp8266 Wochenzeitschaltuhrdual Modular(Tab)
// created: Jens Fleischer, 2019-03-08
// last mod: Jens Fleischer, 2021-08-28
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.4.2 / 2.5.2 / 2.6.3 / 2.7.4
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Dual
/******************************************************************
Copyright (c) 2019 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// 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 (Erfordert 512K Spiffs)
/**************************************************************************************/
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#include <ESP8266WebServer.h>
#include <ArduinoOTA.h> // https://arduino-esp8266.readthedocs.io/en/latest/ota_updates/readme.html
#include <FS.h>
#include <time.h>
struct tm tm;
char file[sizeof(__FILE__)] = __FILE__; // Dateiname für den Admin Tab
ESP8266WebServer server(80);
void setup() {
Serial.begin(115200);
delay(100);
Serial.printf("\nSketchname: %s\nBuild: %s\t\tIDE: %d.%d.%d\n%s\n\n",
(__FILE__), (__TIMESTAMP__), ARDUINO / 10000, ARDUINO % 10000 / 100, ARDUINO % 10, ESP.getFullVersion().c_str());
spiffs();
Connect();
admin();
setupTime();
setupTimerSwitch();
setupHobbsMeter();
ArduinoOTA.onStart([]() {
save(); // vor dem OTA Sketch Update Betriebsstunden in Datei schreiben
});
ArduinoOTA.begin();
server.begin();
}
void loop() {
ArduinoOTA.handle();
server.handleClient();
if (millis() < 0x2FFF || millis() > 0xFFFFF0FF) runtime();
static uint32_t previousMillis {0};
uint32_t currentMillis {millis()};
if (currentMillis - previousMillis >= 1e2) {
previousMillis = currentMillis;
localTime();
dualTimerSwitch();
}
}
Admin.ino
// ****************************************************************
// Sketch Esp8266 Admin Modular(Tab)
// created: Jens Fleischer, 2018-05-09
// last mod: Jens Fleischer, 2021-06-09
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.4.2 / 2.5.2 / 2.6.3 /2.7.4
// 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(304, "message/http");
WiFi.reconnect();
});
server.on("/restart", []() {
server.send(304, "message/http");
save(); //Wenn Werte vor dem Neustart gespeichert werden sollen
ESP.restart();
});
}
//Es kann entweder die Spannung am ADC-Pin oder die Modulversorgungsspannung (VCC) ausgegeben werden.
void handlerenew() { // Um die am ADC-Pin anliegende externe Spannung zu lesen, verwende analogRead (A0)
server.send(200, "application/json", "[\"" + runtime() + "\",\"" + WiFi.RSSI() + "\",\"" + analogRead(A0) + "\"]"); // Json als Array
}
/*
ADC_MODE(ADC_VCC);
void handlerenew() { // Zum Lesen der Modulversorgungsspannung (VCC), verwende ESP.getVcc()
server.send(200, "application/json", "[\"" + runtime() + "\",\"" + WiFi.RSSI() + "\",\"" + ESP.getVcc() / 1024.0 + " V" + "\"]");
}
*/
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();
}
if (strchr(file, '/') || strchr(file, '\\')) {
char *pos = strrchr((file), strstr (file, "\\") ? '\\' : '/'); *pos = '\0';
char* ptr = strtok(file, "\\/");
while (ptr != NULL) {
strcpy(file, ptr);
ptr = strtok(NULL, "\\/");
}
}
else if (strchr(file, '.')) {
char * pos = strchr(file, '.'); *pos = '\0';
}
String temp = "{\"File\":\"" + static_cast<String>(file) + "\", \"Build\":\"" + static_cast<String>(__DATE__) + " " + static_cast<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()] + "\", \"IdeVersion\":\"" + ARDUINO +
"\", \"CoreVersion\":\"" + ESP.getCoreVersion() + "\", \"SdkVersion\":\"" + ESP.getSdkVersion() + extra() + "\"}";
server.send(200, "application/json", temp); // Json als Objekt
}
String runtime() {
static uint8_t rolloverCounter;
static uint32_t previousMillis;
uint32_t currentMillis {millis()};
if (currentMillis < previousMillis) rolloverCounter++; // prüft millis() auf Überlauf
previousMillis = currentMillis;
uint32_t sec {(0xFFFFFFFF / 1000) * rolloverCounter + (currentMillis / 1000)};
char buf[20];
snprintf(buf, sizeof(buf), "%*.d %.*s %02d:%02d:%02d",
sec < 86400 ? 0 : 1, sec / 86400, sec < 86400 ? 0 : sec >= 172800 ? 4 : 3, "Tage", sec / 3600 % 24, sec / 60 % 60, sec % 60);
return buf;
}
Connect.ino
// ****************************************************************
// Sketch Esp8266 Connect Modular(Tab) mit optischer Anzeige
// created: Jens Fleischer, 2018-04-08
// last mod: Jens Fleischer, 2019-04-14
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.4.2 / 2.5.2 / 2.6.3 / 2.7.4
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Dual
/******************************************************************
Copyright (c) 2018 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Connect sollte als Tab eingebunden werden.
// #include <ESP8266WebServer.h> oder #include <ESP8266WiFi.h> muss im Haupttab aufgerufen werden
// 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
IPAddress dns(192, 168, 178, 1); // DNS Server
#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, dns);
#endif
while (WiFi.status() != WL_CONNECTED) {
#ifdef NO_SLEEP
pinMode(LED_BUILTIN, OUTPUT); // OnBoardLed Nodemcu, Wemos D1 Mini Pro
digitalWrite(LED_BUILTIN, 0);
#endif
delay(500);
digitalWrite(LED_BUILTIN, 1);
delay(500);
Serial.printf(" %d sek\n", ++i);
if (i > 9) {
Serial.print("\nVerbindung zum AP fehlgeschlagen !\n\n");
ESP.restart();
}
}
Serial.println("\nVerbunden mit: " + WiFi.SSID());
Serial.println("Esp8266 IP: " + WiFi.localIP().toString());
}
In diesem Tab einstellen ob die Ausgänge LOW oder HIGH aktiv geschaltet werden.
In der "Dualschaltuhr.ino" die Anzahl der Schaltzeiten (analog index.html) einstellen. (2 bis 40)
Dualschaltuhr.ino
// ****************************************************************
// Sketch Esp8266 Zeitschaltuhr Dual Modular(Tab)
// created: Jens Fleischer, 2019-03-08
// last mod: Jens Fleischer, 2020-04-25
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266, Relais Modul o. Mosfet IRF3708 o. Fotek SSR-40 DA
// für Relais Modul
// GND an GND
// IN1 an D5 = GPIO14
// IN2 an D6 = GPIO12
// VCC an VIN -> je nach verwendeten Esp.. möglich
// Jumper JD-VCC VCC
// alternativ ext. 5V Netzteil verwenden
//
// für Mosfet IRF3708
// Source an GND
// Mosfet1 Gate an D5 = GPIO14
// Mosfet2 Gate an D6 = GPIO12
//
// für 3V Solid State Relais
// GND an GND
// SSR1 Input + an D5 = GPIO14
// SSR2 Input + an D6 = GPIO12
//
// Software: Esp8266 Arduino Core 2.4.2 / 2.5.2 / 2.6.3 /2.7.4
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Dual
/******************************************************************
Copyright (c) 2019 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Wochenzeitschaltuhr 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 "setupTimerSwitch();" muss im Setup aufgerufen werden.
// Zum schalten muss die Funktion "dualTimerSwitch();" im loop(); aufgerufen werden.
/**************************************************************************************/
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, D6}; // Pin für Relais einstellen
const auto count = 10; // Anzahl Schaltzeiten (analog Html Dokument) einstellen 2 bis 40
bool relState[] {!aktiv, !aktiv};
struct Collection {
byte switchActive[count];
byte wday[count];
char switchTime[count * 2][6];
};
Collection times;
void setupTimerSwitch() {
for (auto& pin : relPin) digitalWrite(pin, !aktiv), pinMode(pin, OUTPUT);
File file = SPIFFS.open("/stimes.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*>(×), sizeof(times)); // Deserialisierung
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();
toSave();
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")) {
strcpy (str, server.arg("sDay").c_str());
ptr = strtok(str, ",");
i = 0;
while (ptr != NULL) {
times.wday[i++] = atoi(ptr);
ptr = strtok(NULL, ",");
}
toSave();
}
else {
server.send(400, "");
}
}
String temp = "[";
for (auto& elem : times.switchTime) {
if (temp != "[") temp += ',';
temp += "\"" + static_cast<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) == "0") {
relState[0] = !relState[0]; // Relais1 Status manuell ändern
timeDataLogger(0, server.client().remoteIP().toString()); // Funktionsaufruf Zeitdatenlogger
}
if (server.hasArg("tog") && server.arg(0) == "1") {
relState[1] = !relState[1]; // Relais2 Status manuell ändern
timeDataLogger(1, server.client().remoteIP().toString()); // Funktionsaufruf Zeitdatenlogger
}
server.send(200, "application/json", ("[\"" + static_cast<String>(relState[0] == aktiv) + "\",\"" + static_cast<String>(relState[1] == aktiv) + "\",\"" + localTime() + "\"]"));
});
}
void toSave() {
File file = SPIFFS.open("/stimes.dat", "w");
if (file) {
file.write(reinterpret_cast<byte*>(×), sizeof(times)); // Serialisierung
file.close();
}
}
void dualTimerSwitch() {
static uint8_t lastmin {60}, lastState[] {aktiv, aktiv};
hobbsMeter(relState[0], relState[1]); // Funktionsaufruf Betriebsstundenzähler mit Relais Status
if (tm.tm_min != lastmin) {
lastmin = tm.tm_min;
char buf[6];
snprintf(buf, sizeof(buf), "%.2d:%.2d", tm.tm_hour, tm.tm_min);
for (auto i = 0; i < count * 2; i++) {
if (times.switchActive[i / 2] && !strcmp(times.switchTime[i], buf)) {
if (times.wday[i / 2] & (1 << (tm.tm_wday ? tm.tm_wday - 1 : 6))) {
i < (count % 2 ? count + 1 : count) ? ({relState[0] = i % 2 ? !aktiv : aktiv; timeDataLogger(0, "Programm");}) : ({relState[1] = i % 2 ? !aktiv : aktiv; timeDataLogger(1, "Programm");}); // Relais Status nach Zeit ändern
}
}
}
}
if (relState[0] != lastState[0] || relState[1] != lastState[1]) { // Relais schalten wenn sich der Status geändert hat
for (auto i = 0; i < 2; i++) {
lastState[i] = relState[i];
digitalWrite(relPin[i], relState[i]);
Serial.printf("Relais %d %s\n", 1 + i, digitalRead(relPin[i]) == aktiv ? "an" : "aus");
}
}
}
Lokalzeit.ino
// ****************************************************************
// Sketch Esp8266 Lokalzeit Modular(Tab)
// created: Jens Fleischer, 2018-07-10
// last mod: Jens Fleischer, 2021-08-15
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.4.2 - 3.0.2
// 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 Lokalzeit sollte als Tab eingebunden werden.
// #include <ESP8266WebServer.h> oder #include <ESP8266WiFi.h> muss im Haupttab aufgerufen werden.
// Funktion "setupTime();" muss im setup() nach dem Verbindungsaufbau aufgerufen werden.
/**************************************************************************************/
#include <sntp.h>
const uint32_t SYNC_INTERVAL = 12; // NTP Sync Interval in Stunden einstellen
const char* const PROGMEM NTP_SERVER[] = {"fritz.box", "de.pool.ntp.org", "at.pool.ntp.org", "ch.pool.ntp.org", "ptbtime1.ptb.de", "europe.pool.ntp.org"};
const char* const PROGMEM MONTH_NAMES[] = {"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"};
const char* const PROGMEM MONTH_SHORT[] = {"Jan", "Feb", "Mrz", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"};
void setNtp(const char* server1, const char* server2 = nullptr, const char* server3 = nullptr);
void setupTime() {
setNtp(NTP_SERVER[1], NTP_SERVER[2], NTP_SERVER[3]); // deinen NTP Server einstellen (von 0 - 5 aus obiger Liste)
server.on("/time", []() {
server.send(200, "application/json", localTime());
});
}
void setNtp(const char* server1, const char* server2, const char* server3) {
sntp_stop();
sntp_setservername(0, (char*)server1);
if (server2) sntp_setservername(1, (char*)server2);
if (server3) sntp_setservername(2, (char*)server3);
static uint8_t once {0};
if (!once++) {
sntp_set_timezone(0);
setenv("TZ", "CET-1CEST,M3.5.0/02,M10.5.0/03", 1); // Zeitzone einstellen https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
}
sntp_init();
}
char* localTime() {
static char buf[9]; // je nach Format von "strftime" eventuell anpassen
static time_t lastsec;
time_t now = time(&now);
localtime_r(&now, &tm);
if (tm.tm_sec != lastsec) {
lastsec = tm.tm_sec;
strftime(buf, sizeof(buf), "%T", &tm); // http://www.cplusplus.com/reference/ctime/strftime/
if (!(time(&now) % (SYNC_INTERVAL * 3600))) {
setupTime();
}
}
return buf;
}
Spiffs.ino
// ****************************************************************
// Sketch Esp8266 Dateiverwaltung spezifisch Sortiert Modular(Tab)
// created: Jens Fleischer, 2020-08-03
// last mod: Jens Fleischer, 2020-08-03
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.4.2 - 2.7.4
// Geprüft: von 1MB bis 16MB Flash
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Switch, Sonoff Dual
/******************************************************************
Copyright (c) 2020 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Diese Version von 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.
/**************************************************************************************/
#include <list>
const char WARNING[] PROGMEM = R"(<h2>Der Sketch wurde mit "no SPIFFS" kompilliert!)";
const char HELPER[] PROGMEM = R"(<form method="POST" action="/upload" enctype="multipart/form-data">
<input type="file" name="[]" multiple><button>Upload</button></form>Lade die spiffs.html hoch.)";
void spiffs() { // Funktionsaufruf "spiffs();" muss im Setup eingebunden werden
SPIFFS.begin();
server.on("/list", handleList);
server.on("/format", formatSpiffs);
server.on("/upload", HTTP_POST, sendResponce, 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
typedef std::pair<String, int> prop;
std::list<prop> dirList; // Liste anlegen
while (dir.next()) dirList.emplace_back(dir.fileName().substring(1), dir.fileSize()); // Liste füllen
dirList.sort([](const prop & f, const prop & l) { // Liste sortieren
if (server.arg(0) == "1") {
return f.second > l.second;
} else {
for (uint8_t i = 0; i < 30; i++) {
if (tolower(f.first[i]) < tolower(l.first[i])) return true;
else if (tolower(f.first[i]) > tolower(l.first[i])) return false;
}
return false;
}
});
String temp = "[";
for (auto& p : dirList) {
if (temp != "[") temp += ',';
temp += "{\"name\":\"" + p.first + "\",\"size\":\"" + formatBytes(p.second) + "\"}";
}
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
sendResponce();
return true;
}
if (!SPIFFS.exists("/spiffs.html"))server.send(200, "text/html", SPIFFS.begin() ? HELPER : WARNING); // 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, getContentType(path)); f.close(); true;}) : false;
}
void handleUpload() { // Dateien ins SPIFFS schreiben
static File fsUploadFile; // enthält den aktuellen Upload
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
if (upload.filename.length() > 30) {
upload.filename = upload.filename.substring(upload.filename.length() - 30, upload.filename.length()); // Dateinamen auf 30 Zeichen kürzen
}
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);
fsUploadFile.write(upload.buf, upload.currentSize);
} else if (upload.status == UPLOAD_FILE_END) {
printf("handleFileUpload Size: %u\n", upload.totalSize);
fsUploadFile.close();
}
}
void formatSpiffs() { // Formatiert den Speicher
SPIFFS.format();
sendResponce();
}
void sendResponce() {
server.sendHeader("Location", "spiffs.html");
server.send(303, "message/http");
}
const String formatBytes(size_t const& bytes) { // lesbare Anzeige der Speichergrößen
return bytes < 1024 ? static_cast<String>(bytes) + " Byte" : bytes < 1048576 ? static_cast<String>(bytes / 1024.0) + " KB" : static_cast<String>(bytes / 1048576.0) + " MB";
}
const String &getContentType(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
//Serial.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, 2021-08-15
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.4.2 / 2.5.2 / 2.6.3 / 2.7.4
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Dual
/******************************************************************
Copyright (c) 2018 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Betriebsstundenzähler sollte als Tab eingebunden werden.
// #include <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() { // Betriebstunden(minuten) beim Neustart einlesen
File file = SPIFFS.open("/operatingTime.txt", "r");
if (file) {
lastmin[0] = totalmin[0] = file.parseInt();
lastmin[1] = totalmin[1] = file.parseInt();
file.close();
}
}
void hobbsMeter(bool &state_0, bool &state_1) { // Aufrufen mit Relais Status
static uint32_t previousMillis[2];
uint32_t currentMillis {millis()};
if (currentMillis - previousMillis[0] >= 6e4) {
previousMillis[0] = currentMillis;
if (state_0 == 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 (currentMillis - previousMillis[1] >= 1728e5 && (totalmin[0] != lastmin[0] || totalmin[1] != lastmin[1])) { // aller 2 Tage Betriebsstunden in Datei schreiben
previousMillis[1] = currentMillis;
lastmin[0] = totalmin[0];
lastmin[1] = totalmin[1];
save();
}
}
String extra() { // Betriebsstundenanzeige admin.html
return "\", \"HourMeter1\":\"" + operatingTime(totalmin[0]) + "\", \"HourMeter2\":\"" + operatingTime(totalmin[1]);
}
String operatingTime(uint32_t &tmin) { // lesbare Anzeige der Betriebstunden(minuten)
char buf[11];
snprintf(buf, sizeof(buf), "%d,%d", tmin / 60, tmin / 6 % 10);
return buf;
}
void save() {
File file = SPIFFS.open("/operatingTime.txt", "w"); // Betriebstunden(minuten) speichern
if (file && freeSpace(100)) {
file.printf("%d\n%d\n", totalmin[0], totalmin[1]);
file.close();
}
}
Zeitlogger.ino
// ****************************************************************
// Sketch Esp8266 Zeitdatenlogger Dual Modular(Tab)
// created: Jens Fleischer, 2019-03-31
// last mod: Jens Fleischer, 2020-08-15
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.4.2 / 2.5.2 / 2.6.3 / 2.7.4
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Dual
/******************************************************************
Copyright (c) 2019 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Ereignisdatenspeicher sollte als Tab eingebunden werden.
// #include <FS.h> muss im Haupttab aufgerufen werden
// Der Lokalzeit Tab ist zum ausführen des Zeitdatenlogger erforderlich.
// Der Spiffs Tab ist zum ausführen erforderlich.
/**************************************************************************************/
void timeDataLogger(const uint8_t num, const String customer) { // Relais Nummer und Ereignisauslöser
static bool lastrelState[] {!aktiv, !aktiv};
if (relState[num] != lastrelState[num]) { // Prüft ob sich der Relais Status geändert hat.
char fileName[23];
snprintf(fileName, sizeof(fileName), "/Relais%d_%s.csv", 1 + num, MONTH_NAMES[tm.tm_mon]);
if (!SPIFFS.exists(fileName)) { // Logdatei für den aktuellen Monat anlegen falls nicht vorhanden.
File f = SPIFFS.open(fileName, "a");
if (f && freeSpace(40)) { // Prüft ob genügend Speicher frei ist.
f.printf("%s;An;Initiator;Aus;Initiator\n", MONTH_SHORT[tm.tm_mon]); // Kopfzeile schreiben
}
f.close();
char path[23];
snprintf(path, sizeof(path), "/Relais%d_%s.csv", 1 + num, MONTH_NAMES[(tm.tm_mon + 1) % 12]);
SPIFFS.remove(path); // Löscht die elf Monate alte Logdatei.
}
File f = SPIFFS.open(fileName, "a"); // Die Ereignisdaten für den aktuellen Monat speichern
if (f && freeSpace(40)) {
relState[num] == aktiv ? f.printf("%d.;%s;%s;", tm.tm_mday, localTime(), customer.c_str()) : f.printf("%s;%s;\n", localTime(), customer.c_str());
}
f.close();
}
lastrelState[num] = relState[num]; // Ersetzt den letzten Status durch den aktuellen Relais Status.
}
admin.html
<!DOCTYPE HTML> <!-- For more information visit: https://fipsok.de -->
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<title>ESP8266 Admin</title>
<style>
#add {
display: flex;
flex-flow: column;
align-items: flex-end;
width: max-content;
}
</style>
<script>
window.addEventListener('load', () => {
renew(),once();
let output = document.querySelector('#note');
let button = document.querySelectorAll('button');
let span = document.querySelectorAll('#right span');
button[0].addEventListener('click', () => {
window.location = '/spiffs.html';
});
button[1].addEventListener('click', () => {
window.location = '/';
});
button[2].addEventListener('click', check.bind(this, document.querySelector('input')));
button[3].addEventListener('click', re.bind(this, 'reconnect'));
button[4].addEventListener('click', () => {
if (confirm('Bist du sicher!')) re('restart');
});
function once(arg1,arg2) {
fetch('/admin/once', {
method: 'POST',
body: arg1
}).then( (resp) => {
return resp.json();
}).then( (obj) => {
output.innerHTML = '';
output.classList.remove('note');
document.querySelector('form').reset();
if (arg1 == undefined) myIv = window.setInterval(renew, 1000);
if (arg2 == 'reconnect') re(arg2);
span[3].innerHTML = obj['File'];
span[4].innerHTML = obj['Build'];
span[5].innerHTML = obj['SketchSize'];
span[6].innerHTML = obj['SketchSpace'];
span[7].innerHTML = obj['LocalIP'];
span[8].innerHTML = obj['Hostname'];
span[9].innerHTML = obj['SSID'];
span[10].innerHTML = obj['GatewayIP'];
span[11].innerHTML = obj['Channel'];
span[12].innerHTML = obj['MacAddress'];
span[13].innerHTML = obj['SubnetMask'];
span[14].innerHTML = obj['BSSID'];
span[15].innerHTML = obj['ClientIP'];
span[16].innerHTML = obj['DnsIP'];
span[17].innerHTML = obj['ResetReason'];
span[18].innerHTML = obj['CpuFreqMHz'] + " MHz";
span[19].innerHTML = obj['FreeHeap'];
span[20].innerHTML = obj['ChipSize'];
span[21].innerHTML = obj['ChipSpeed'] + " MHz";
span[22].innerHTML = obj['ChipMode'];
span[23].innerHTML = obj['IdeVersion'].replace(/(\d)(\d)(\d)(\d)/,obj['IdeVersion'][3]!=0 ? '$1.$3.$4' : '$1.$3.');
span[24].innerHTML = obj['CoreVersion'].replace(/_/g,'.');
span[25].innerHTML = obj['SdkVersion'];
span[26].innerHTML = obj['HourMeter1'] + " h";
span[27].innerHTML = obj['HourMeter2'] + " h";
}).catch(function(err) {
re();
});
}
function renew() {
fetch('admin/renew').then( (resp) => {
return resp.json();
}).then( (array) => {
array.forEach((x, i) => {
span[i].innerHTML = x
});
});
}
function check(inObj) {
!inObj.checkValidity() ? (output.innerHTML = inObj.validationMessage, output.classList.add('note')) : (once(inObj.value, 'reconnect'));
}
function re(arg) {
clearInterval(myIv);
fetch(arg);
output.classList.add('note');
if (arg == 'restart') {
output.innerHTML = 'Der Server wird neu gestartet. Die Daten werden in 15 Sekunden neu geladen.';
setTimeout(once, 15000);
}
else if (arg == 'reconnect'){
output.innerHTML = 'Die WiFi Verbindung wird neu gestartet. Daten werden in 10 Sekunden neu geladen.';
setTimeout(once, 10000);
}
else {
output.innerHTML = 'Es ist ein Verbindungfehler aufgetreten. Es wird versucht neu zu verbinden.';
setTimeout(once, 3000);
}
}
});
</script>
</head>
<body>
<h1>ESP8266 Admin Page</h1>
<main>
<aside id=left>
<span>Runtime ESP:</span>
<span>WiFi RSSI:</span>
<span>ADC/VCC:</span>
<span>Sketch Name:</span>
<span>Sketch Build:</span>
<span>SketchSize:</span>
<span>FreeSketchSpace:</span>
<span>IPv4 Address:</span>
<span>Hostname:</span>
<span>Connected to:</span>
<span>Gateway IP:</span>
<span>Channel:</span>
<span>MacAddress:</span>
<span>SubnetMask:</span>
<span>BSSID:</span>
<span>Client IP:</span>
<span>DnsIP:</span>
<span>Reset Ground:</span>
<span>CPU Freq:</span>
<span>FreeHeap:</span>
<span>FlashSize:</span>
<span>FlashSpeed:</span>
<span>FlashMode:</span>
<span>Arduino IDE Version:</span>
<span>Esp Core Version:</span>
<span>SDK Version:</span>
<span>Hour Meter 1:</span>
<span>Hour Meter 2:</span>
</aside>
<aside id=right>
<span>0</span>
<div>
<span></span>
dBm
</div>
<span>0</span>
<span>?</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>?</span>
<span>?</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>?</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<div id=add>
<span>0</span>
<span>0</span>
</div>
</aside>
</main>
<div>
<button>Spiffs</button>
<button>Startseite</button>
</div>
<div id=note></div>
<div>
<form>
<input placeholder=" neuer Hostname" pattern=([A-Za-z0-9-]{1,32}) title="Es dürfen nur Buchstaben
(a-z, A-Z), Ziffern (0-9) und Bindestriche (-) enthalten sein. Maximal 32 Zeichen" required>
<button type=button>Name Senden</button>
</form>
</div>
<div>
<button>WiFi Reconnect</button>
<button>ESP Restart</button>
</div>
</body>
</html>
In der "index.html" die Anzahl der Schaltzeiten (analog Sketch) einstellen. (2 bis 40)
index.html
<!DOCTYPE HTML> <!-- For more information visit: https://fipsok.de -->
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<title>Dual Zeitschaltuhr</title>
<style>
main, #bu, .tab button, input:checked + label {
color: #15dfdf;
}
div {
display: flex;
}
span {
padding: 0.5em;
}
input {
height: auto;
font-weight: bold;
background-color: inherit;
font-size: 3em;
color: inherit;
border: solid #555;
}
label {
font-size: .9em;
}
svg {
height: 4em;
}
* + [id*=ak]{
margin-top: .6em;
}
div+span {
margin-left: 3em;
}
input + label {
color: #777;
font-style: italic;
}
#tog0,#tog1 {
margin: .7em 1em;
cursor: pointer;
color: #777;
}
time {
text-shadow: 2px 2px 2px black;
font-size: 1.3em;
font-weight: bold;
margin: auto;
}
.note:after {
content: "Schaltzeiten gespeichert";
color: #777;
}
#bu {
border: outset #555;
}
.tab {
overflow: hidden;
}
.tab button {
background-color: #999;
border: none;
margin-top: 0em;
transition: 0.8s;
border-radius: .5em .5em 0 0;
}
.tab button:hover {
background-color: #666;
}
#bu, .tabcontent, .tab button.active {
background-color: #333;
}
.tabcontent {
display: block;
padding: .5em .7em .5em .1em;
box-shadow: 5px 3px 10px #4e4d4d;
}
.tabcontent [name^=bu] {
width: 2em;
cursor: pointer;
}
.none {
color: #777 !important;
}
.hide {
display: none;
}
@media only screen and (max-width: 600px) {
input {
font-size: 2.4em;
width: auto;
border: none;
}
.tab button,#bu {
width: 7em;
}
#tog1 {
margin: .7em 0 0 0;
}
div+span {
margin-left: .2em;
}
}
</style>
<script>
var count = 10; <!-- Anzahl Schaltzeiten (analog Sketch) einstellen 2 bis 40 -->
var d = document;
d.addEventListener('DOMContentLoaded', () => {
dom(), renew();
d.querySelector('#bu').addEventListener('click', () => {
let arr = [], formData = new FormData();
formData.append('sTime', Array.from(d.querySelectorAll('input[type=time]')).map(x => x.value != 0 ? x.value : 0));
for (let i = 0; i < count; i++) {
let x = 0;
d.querySelectorAll(`input[name=c${i}]`).forEach((el, i) => { if (el.checked) x = x | (1 << i) });
arr.push(x);
}
formData.append(`sDay`, arr);
send(formData);
});
d.querySelector('#tab1').addEventListener('click', openTab);
d.querySelector('#tab2').addEventListener('click', openTab);
d.querySelector('#tog0').addEventListener('click', renew);
d.querySelector('#tog1').addEventListener('click', renew);
for (var i = 0; i < count;) d.querySelector(`[name=bu${i++}]`).addEventListener('click', setActive);
},send(), setInterval(renew, 1000));
function dom() {
var buf = '';
for (var i = 0; i < count*2; i++) {
buf += `${i%2 ? `<span> -- </span>` : `<div id="ak${i/2}"><span name="bu${i/2}"></span>`}<input type="time" name="sz${i}" value="">${i%2 ? `</div><span id="t${i/2|0}">` : ""}`;
if (i%2) {
['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'].forEach(v => {
buf += `<input type="checkbox" name="c${(i-1)/2}"><label>${v} </label>`;
});
buf += "</span>";
}
if (i == (count%2 ? count : count-1)) {
d.querySelector('#ctab1').insertAdjacentHTML('beforeend', buf);
buf = '';
}if (i == count*2-1) d.querySelector('#ctab2').insertAdjacentHTML('beforeend', buf);
}
}
function setActive() {
let formData = new FormData();
formData.append(this.parentNode.id.substr(2, 5), this.textContent == 'ON' ? '0' : '1');
send(formData);
}
function send(arg) {
fetch('/timer', {
method: 'post',
body: arg
}).then(resp => {
if (resp.ok) {
if(arg && arg.has('sTime')) {
let el = d.querySelector('u').classList;
el.add('note');
setTimeout(() => {
el.remove('note');
}, 5e3);
}
}
return resp.json();
}).then(array => {
if (array.length > count) {
array.forEach((v, i) => {
if (i < count*2) d.querySelector(`[name=sz${i}]`).value = v;
if (i == count*2) getActive(v);
if (i > count*2) {
let el = d.getElementsByName(`c${i - count * 2 - 1}`);
for (let k in el) {
v & (1 << k) ? el[k].checked = true : el.checked = false;
}
}
});
}
else {
getActive(array);
}
});
}
function getActive(arg) {
for (var i = 0; i < count; i++) {
d.querySelector(`[name=bu${i}]`).textContent = (arg[i]%2 ? 'ON' : 'OFF');
let el = d.getElementById(`ak${i}`).classList;
arg[i]%2 ? el.remove("none") : el.add("none");
d.getElementById(`t${i}`).childNodes.forEach(v => {arg[i]%2 ? v.classList.remove("none") : v.classList.add("none")});
}
}
function openTab() {
let a = event.target.id.charAt(3)%2+1;
d.getElementById(`ctab${a}`).classList.add("hide");
d.getElementById(`tab${a}`).classList.remove("active");
d.getElementById('c' + event.target.id).classList.remove("hide");
event.target.classList.add("active")
}
function renew(ev) {
if (ev) ev = ev.currentTarget.id.slice(3, 4);
fetch(`timer?tog=${ev}`).then(resp => {
return resp.json();
}).then(array => {
for (var i = 0; i < 2; i++) {
d.getElementById(`body${i}`).style.fill=array[i] == 0 ? '' : '#ff0';
d.getElementById(`on${i}`).style.visibility=array[i] == 0 ? 'hidden' : 'visible';
}
d.querySelector('time').innerHTML = array[2];
});
}
</script>
</head>
<body>
<h2>Zeitschaltuhr</h2>
<main>
<div class="tab">
<button class="active" id="tab1">⏳ Relais 1</button>
<button id="tab2">⏳ Relais 2</button>
<time>00:00:00</time>
</div>
<div id="ctab1" class="tabcontent">
</div>
<div id="ctab2" class="tabcontent hide">
</div>
</main>
<div>
<button class="button" id="bu">⏰ Speichern</button>
<div id="tog0">
R1
<svg viewBox="0 0 486 486">
<use id="body0" href="#bulb"/>
<use id="on0" href="#beam"/>
</svg>
</div>
<div id="tog1">
R2
<svg viewBox="0 0 486 486">
<use id="body1" href="#bulb"/>
<use id="on1" href="#beam"/>
</svg>
</div>
</div>
<u></u>
<svg class="hide">
<path id="bulb" d="m256.5 160.8c0-7.4-6-13.5-13.5-13.5-47.6 0-86.4 38.7-86.4 86.4 0 7.4 6 13.5 13.5 13.5 7.4 0 13.5-6 13.5-13.5 0-32.8 26.7-59.4 59.4-59.4 7.5 0 13.5-6 13.5-13.5zm106.2 72.5c0 32.3-12.8 61.6-33.6 83.1-15.8 16.4-26 37.3-29.4 59.6-1.5 9.6-9.8 16.7-19.6 16.7h-74.3c-9.7 0-18.1-7-19.5-16.6-3.5-22.3-13.8-43.5-29.6-59.8-20.4-21.2-33.1-50-33.4-81.7-0.7-66.6 52.3-120.5 118.9-121 66.5-0.5 120.5 53.3 120.5 119.7zm-64.3 191.4v14.2c0 11.3-8.3 20.7-19.1 22.3l-3.5 12.9c-1.9 7-8.2 11.9-15.5 11.9h-34.7c-7.3 0-13.6-4.9-15.5-11.9l-3.4-12.9c-10.9-1.7-19.2-11-19.2-22.4v-14.2c0-7.6 6.1-13.7 13.7-13.7h83.5c7.6 0.1 13.7 6.2 13.7 13.8z"/>
<path id="beam" fill="#ff0" d="m376.57 341.98c-5.3-5.3-13.8-5.3-19.1 0s-5.3 13.8 0 19.1l33.5 33.5c2.6 2.6 6.1 3.9 9.5 3.9s6.9-1.3 9.5-3.9c5.3-5.3 5.3-13.8 0-19.1zm-262.8-224.8c2.6 2.6 6.1 3.9 9.5 3.9s6.9-1.3 9.5-3.9c5.3-5.3 5.3-13.8 0-19.1l-33.5-33.5c-5.3-5.3-13.8-5.3-19.1 0s-5.3 13.8 0 19.1zm253.3 4c3.4 0 6.9-1.3 9.5-3.9l33.5-33.5c5.3-5.3 5.3-13.8 0-19.1s-13.8-5.3-19.1 0l-33.5 33.5c-5.3 5.3-5.3 13.8 0 19.1 2.7 2.6 6.1 3.9 9.6 3.9zm-253.3 220.8-33.5 33.5c-5.3 5.3-5.3 13.8 0 19.1 2.6 2.6 6.1 3.9 9.5 3.9s6.9-1.3 9.5-3.9l33.5-33.5c5.3-5.3 5.3-13.8 0-19.1-5.2-5.3-13.8-5.3-19 0zm351.1-125.9h-47.3c-7.4 0-13.5 6-13.5 13.5 0 7.4 6 13.5 13.5 13.5h47.3c7.4 0 13.5-6 13.5-13.5 0-7.4-6-13.5-13.5-13.5zm-378.6 13.5c0-7.4-6-13.5-13.5-13.5h-47.3c-7.4 0-13.5 6-13.5 13.5 0 7.4 6 13.5 13.5 13.5h47.3c7.5 0 13.5-6 13.5-13.5zm158.9-158.9c7.4 0 13.5-6 13.5-13.5v-47.3c0-7.4-6-13.5-13.5-13.5s-13.5 6-13.5 13.5v47.3c0 7.5 6.1 13.5 13.5 13.5z"/>
</svg>
</body>
</html>
spiffs.html
<!DOCTYPE HTML> <!-- For more information visit: https://fipsok.de -->
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<title>Esp8266 Datei Manager</title>
<script>
var to = JSON.parse(localStorage.getItem('sortBy'));
document.addEventListener('DOMContentLoaded', () => {
list(to);
fs.addEventListener('change', () => {
for (var bytes = 0, j = 0; j < event.target.files.length; j++) bytes += event.target.files[j].size;
for (var out = `${bytes} Byte`, i = 0, circa = bytes / 1024; circa > 1; circa /= 1024) {
out = circa.toFixed(2) + [' KB', ' MB', ' GB'][i++];
}
if (bytes > free) {
si.innerHTML = `<li><b> ${out}</b><strong> Ungenügend Speicher frei</strong></li>`;
up.setAttribute('disabled', 'disabled');
}
else {
si.innerHTML = `<li><b>Dateigröße:</b> ${out}</li>`;
up.removeAttribute('disabled');
}
});
btn.addEventListener('click', () => {
if (!confirm(`Wirklich formatieren? Alle Daten gehen verloren.\nDu musst anschließend spiffs.html wieder laden.`)) event.preventDefault();
});
});
function list(arg){
let myList = document.querySelector('main');
fetch('list?sort='+arg).then( (response) => {
return response.json();
}).then((json) => {
myList.innerHTML = '';
for (var i = 0; i < json.length - 1; i++) {
let dir = `<li><a href="${json[i].name}">${json[i].name}</a><small> ${json[i].size}</small>`;
dir += `<a href="${json[i].name}"download="${json[i].name}"> Download </a>or <a href="?delete=/${json[i].name}">Delete </a>`;
myList.insertAdjacentHTML('beforeend', dir);
}
myList.insertAdjacentHTML('beforeend', `<li><b id="so">${to ? '▼' : '▲'} SPIFFS</b> belegt ${json[i].usedBytes} von ${json[i].totalBytes}`);
document.querySelectorAll('[href*=delete]').forEach(node => {
node.addEventListener('click', () => {
if (!confirm('Sicher!')) event.preventDefault();
});
});
free = json[i].freeBytes;
so.addEventListener('click', () => {
list(to=++to%2);
localStorage.setItem('sortBy', JSON.stringify(to));
});
});
}
</script>
</head>
<body>
<h2>ESP8266 Datei Manager</h2>
<form action="/upload" method="POST" enctype="multipart/form-data">
<input id="fs" type="file" name="upload[]" multiple>
<input id="up" type="submit" value="Upload" disabled>
</form>
<div>
<span id="si"></span>
<main></main>
</div>
<form action="/format" method="POST"><input id="btn" type="submit" value="Format SPIFFS"></form>
</body>
</html>
style.css
/* For more information visit:https://fipsok.de */
body {
font-family: sans-serif;
background-color: #87cefa;
display: flex;
flex-flow: column;
align-items: center;
}
h1,h2 {
color: #e1e1e1;
text-shadow: 2px 2px 2px black;
}
li {
background-color: #feb1e2;
list-style-type: none;
margin-bottom: 10px;
padding: 2px 5px 1px 0;
box-shadow: 5px 5px 5px rgba(0,0,0,0.7);
}
li a:first-child, li b {
background-color: #8f05a5;
font-weight: bold;
color: white;
text-decoration:none;
padding: 2px 5px;
text-shadow: 2px 2px 1px black;
cursor:pointer;
}
li strong {
color: red;
}
input {
height:35px;
font-size:14px;
}
h1 + main {
display: flex;
}
aside {
display: flex;
flex-direction: column;
padding: 0.2em;
}
#left {
align-items:flex-end;
text-shadow: 0.5px 0.5px 1px #757474;
}
.note {
background-color: #fecdee;
padding: 0.5em;
margin-top: 1em;
text-align: center;
max-width: 320px;
border-radius: 0.5em;
}
[type=submit] {
height:40px;
font-size: 16px;
}
[value*=Format] {
margin-top: 1em;
box-shadow: 5px 5px 5px rgba(0,0,0,0.7);
}
button {
height:40px;
width:130px;
background-color: #7bff97;
font-size:16px;
margin-top: 1em;
box-shadow: 5px 5px 5px rgba(0,0,0,0.7);
}
form [title] {
background-color: skyblue;
font-size: 16px;
width: 125px;
}
du schreibst echt super Sketches und ich habe auch schon einige bei mir in Verwendung. Sind alle etwas an meine Bedürfnisse angepasst aber manchmal langt mein "Krankenpfleger"-Wissen leider nicht aus, alles zu verstehen oder eine Lösung zu finden.
Du hast ja die Runtime-Funktion zum Anzeigen der Laufzeit. Aber bei mir hängt sich manchmal der ESP auf und dann geht gar nichts mehr.
Ich habe das mit der Watchdog-Funktion versucht, da ich aber einem Nodemcu verwende sagt die Arduino-Oberfläche immer Watchdog-Library nicht vorhanden. Kann ich das auch irgendwie anders lösen, also z.B. mit deiner Runtime-Funktion?
Vielen Dank schon mal.
Christoph
Antwort:
Bitte wende dich mit deinem Sketch an ein Arduino Forum.
Dort wird dir sicher geholfen.
Gruß Fips
Wurde bei der Software auch dieser Aspekt berücksichtigt oder ist die ZSU auf Steckdose ausgelegt
Antwort:
Mit Akku habe ich noch nichts gebaut.
Stromsparen geht meines Wissens nur mit Deepsleep und das passt nun mal so gar nicht zum Webserver.
Gruß Fips
Antwort:
Ach, die Sterne sind doch nur eine Spielerei des Kommentar Skript.
Danke fürs Feedback!
Gruß Fips
Habe schon mehrere Zeitschaltuhren aktiv. Aktuell suche ich nach einer Lösung die Zeitschaltuhr mit einem LOW aktiv und eine HIGH aktiv Relais gleichzeitig zu nutzen. Gibt es da eine Möglichkeit? Im Programm habe ich leider nichts gefunden was ich ändern könnte.
Mit freundlichen Grüßen
Niklas
Antwort:
Tausch den Code am Ende der Funktion "dualTimerSwitch()" aus.
Gruß Fips
Erst mal vielen Dank für die Bereitstellung der Zeitschaltuhr.
Diese Uhr Funktioniert einwandfrei!
Ich möchte es nutzen für die Steuerung des Außenbereichs, wie Pool Pumpe, Garten Licht, Brunnen u.s.w.
Was ich leider nach vielen Tagen versuchen nicht hinbekommen habe, ist diese Zeitschaltuhr auf 4 zu erweitern.
Gibt es eine mglichkeit es fertig herunter zu laden ?
Danke
Gruß Lars
Antwort:
Nein!
Ich hatte bisher noch keinen Bedarf die Zeitschaltuhr zu erweitern.
Gruß Fips
Momentan hilft nur das Auskommentieren der sich wiederholenden NTP-Synchronisation. Das ist natürlich keine Dauerlösung.
Werde morgen mal weiteres ausprobieren...
Danke
Gruß Tom
Antwort:
Versionen die auf Null enden sind bisher nicht zu empfehlen.
2.6.0. wurde nach 5 Tagen durch 2.6.1 ersetzt.
Von 2.7.0 zur 2.7.1 waren es auch nur 6 Tage.
Gruß Fips
Der "Esp8266 WiFi NTP Uhrzeit" Sketch von der Startseite läuft einwandfrei.
Mein Problem mit dem "Esp8266 Wochen Zeitschaltuhr Dual" Sketch besteht dagegen weiterhin, bin noch auf der Suche. Scheinbar zeigt er ab dem 2. NTP-Sync die UTC Zeit an.
P.S. Die vier Schalter (von zwei erweitert) laufen auch schon, bald werden die Tabs von 2 auf 4 erweitert... Freue mich.
Danke nochmal für das Teilen der super Vorlage!!!
Gruß Tom
Antwort:
Dann bau doch "getNtpServer()" bei dir ein.
Welche EspCoreVersion verwendest du?
Gruß Fips
bei Deiner aktuellen "Esp8266 Wochen Zeitschaltuhr Dual" habe ich noch einen Fehler. Beim ersten NTP-Sync erscheint die korrekte Uhrzeit. Beim zweiten Abgleich (hier Standard nach 24h) stellt sich die Uhr zwei Stunden zurück und verbleibt für den Rest der Laufzeit zwei Stunden hinterher. Kann man schön beobachten, indem man das NTP-Sync-Interval heruntersetzt.
Hast du das Phänomen auch?
Gruß Tom
Antwort:
Nein!
Bei mir läuft der Sketch mit EspCoreVersion 2.7.1 auf dem Sonoff Dual seid, aktuell, 96 Tagen hervorragend.
Nimm mal den "Esp8266 WiFi NTP Uhrzeit" Sketch von der Startseite, dieser beinhaltet eine Abfrage ob die Zeit vom Server geholt wurde.
Oder erhöhe mal probehalber das "delay(250);" in "setupTime()".
Gruß Fips
theoretisch besteht bei mir die Idee Deine "Esp8266 Wochen Zeitschaltuhr Dual" um 2 weitere Schalter (also Birnen) zu erweitern.
Leider bin ich praktisch an der Grenze angekommen, wo ich keine Idee mehr habe, was ich im Programm noch anpassen muss. Vier Birnen sind da, aber die beiden zusätzlichen sind weiterhin ohne Funktion. Theoretisch sollte das nicht so schwer sein, anders als mit den Timer-Tabs.
Hast Du eine Idee oder einen Weg es mit wenig Aufwand zu realisieren, oder habe ich das falsch eingeschätzt, und das, was ich da vor habe, ist ebenfalls nicht so einfach?
Gruß Tom
Antwort:
Glaub mir, den Timer Tab auf vier Geräte zu erweiten ist einfacher als die Webseite anzupassen.
Vermutlich hast du die Komplexität und die erforderliche Zeit für eine Erweiterung unterschätzt.
Gruß Fips
Unter Werkzeuge/Erase Flash: "All Flash Contents" einstellen hat geholfen. Bin begeistert!!
Antwort:
Vor einem Sketch Update zurückstellen um die Dateien im Spiffs zu erhalten.
Gruß Fips