Update: 2024-05-05
Esp8266 Zeitschaltuhr
Dual Zeitschaltuhr mit NTP Zeitsynchronisation
Automatischer Wechsel zwischen Sommer und Normalzeit
Betriebsstundenzähler für die Angeschlossenen Verbraucher
Schalten bei Dämmerung
Countdown Timer
Verbrauchsanzeige in Kilowattstunden
Ereignisdatenspeicher für Schaltzeiten und Auslöser
Zugriffsschutz mittels HTTP-Authentifizierung
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 Zeitschaltuhr ist für LOW und HIGH aktive Relais, Solid State Relais oder Mosfet geeignet.
Dies muss vor dem Hochladen einmalig im Sketch, im Tab Schaltuhr.ino, eingestellt werden.
Es lassen sich bis zu 50 Ein-/Aus-Schaltzeiten für jedes angeschlossene Gerät einstellen.
Dies kann vor dem Hochladen im Tab Schaltuhr.ino eingestellt werden.
Rechts neben den Button zum manuellen Ein-/Ausschalten des Ausgangs befindet sich die optische Schaltzustandsanzeige.

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

Es wird für jeden Monat eine neue CSV Datei angelegt, in der die Schaltzeiten und der Initiator des Schaltvorgangs gespeichert werden.
Ansicht Exel (formatiert)
Apr |
An |
Initiator |
Aus |
Initiator |
1. |
04:40:00 |
Uhrzeit |
04:50:00 |
Uhrzeit |
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 |
Uhrzeit |
04:40:00 |
Uhrzeit |
3. |
04:38:00 |
Uhrzeit |
04:40:00 |
Uhrzeit |
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. |
07:47:09 |
Sonnenstand |
07:53:21 |
Sonnenstand |
5. |
08:38:07 |
Timer |
08:40:17 |
Timer |
Mindestvorraussetzung EspCore 3.0.0
Im Boardverwalter der Arduino IDE lässt sich die EspCoreVersion einstellen.


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

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

Wird der Kurzzeittimer gestartet werden alle anderen Schaltzeiten für den entsprechenden Verbraucher deaktiviert.
Nach Ablauf oder durch pausieren wird der vorhergehende Zustand (Auto aktiv/Auto inaktiv) wieder hergestellt.
Befindet sich der Kurzzeittimer im Modus Pause kann er durch Klick/Touch der Schaltfläche Pause fortgesetzt oder durch Start neu gestartet werden.

Anleitung zur Inbetriebnahme
Zuerst im "Connect" Tab deine Wlan Zugangsdaten eingeben, anschliesend im "Schaltuhr" Tab die Konstante "ACTIVE" auf HIGH oder LOW setzen.
"constexpr auto ACTIVE = LOW;" Je nachdem welche Komponente du an den Ausgängen betreiben möchtest.
Du kannst die Anzahl der Schaltzeiten pro Relais an deinen Bedarf anpassen. Diese werden im "Schaltuhr" Tab definiert. "constexpr uint8_t RECORDS = 20;"
Im Tab Sonnenlauf den Längen- und Breitengrad deines Standortes eintragen.
Eventuell den NTP Zeitserver und die Zeitzone, für deinen Standort, in der "Lokalzeit.ino" ändern. Anschließend den Sketch hochladen.
Im Seriellen Monitor wird die IP des ESP.. angezeigt. "deineIP/fs.html" Kopiere diese URL in die Adresszeile deines Browsers und verbinde dich mit deinem ESP8266.
Falls sich im LittleFS (Speicher) des Esp8266 noch keine "fs.html" befindet wird ein kleiner Helfer zu deinem Browser gesendet.
Mit diesem kannst du die
"fs.html"
und die
"style.css" hochladen.
Jetzt wird der Filesystem Manager angezeigt, mit dem du noch die
"admin.html" , das
Favicon und die
"index.html"
in den Speicher deines Esp... uploaden musst. Klick/Touch nun auf die index.html um zur Zeitschaltuhr zu kommen.
Der optionale Zugriffsschutz mittels HTTP-Authentifizierung lässt sich durch Ändern der Variable "constexpr bool PROTECT {1};" einschalten.
Wer möchte kann sich unten den Code ansehen und selbst in Dateien kopieren oder aber das Ganze als Archiv downloaden.
Zeitschaltuhr_Dual.ino
// ****************************************************************
// Sketch Esp8266 Dualzeitschaltuhr Modular(Tab)
// created: Jens Fleischer, 2019-03-08
// last mod: Jens Fleischer, 2024-05-05
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 3.0.0 - 3.1.2
// Getestet auf: Nodemcu, Wemos D1 Mini Pro
/******************************************************************
Copyright (c) 2019 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Der WebServer Tab ist der Haupt Tab mit "setup" und "loop".
// #include <LittleFS.h> #include <ESP8266WebServer.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP8266 Webservers ist erforderlich.
// "server.onNotFound()" darf nicht im Setup des ESP8266 Webserver stehen.
// Inklusive Arduino OTA-Updates (Erfordert freien Flash-Speicher )
/**************************************************************************************/
#include <ESP8266WebServer.h>
#include <ArduinoOTA.h> // https://arduino-esp8266.readthedocs.io/en/latest/ota_updates/readme.html
#include <LittleFS.h>
#include <time.h>
constexpr bool PROTECT {0}; // HTML-Basisauthentifizierung aktivieren. (1 = aktiviert)
const char* www_username = "admin"; // Die Anmeldeinformationen für HTML Seiten müssen im Browser eingegeben werden.
const char* www_password = "esp8266";
enum Device : bool {Device_I, Device_II};
struct tm tm;
ESP8266WebServer server(80);
String sketchName() { // Dateiname für den Admin Tab
char file[sizeof(__FILE__)] = __FILE__;
char * pos = strrchr(file, '.'); *pos = '\0';
return file;
}
void setup() {
Serial.begin(115200);
delay(100);
Serial.printf(PSTR("\n\nSketchname: %s\nBuild: %s\t\tIDE: %d.%d.%d\n%s\n\n"),
(__FILE__), (__TIMESTAMP__), ARDUINO / 10000, ARDUINO % 10000 / 100, ARDUINO % 10, ESP.getFullVersion().c_str());
setupFS();
connectWifi();
admin();
setupTime();
setupTimerSwitch();
setupHobbsMeter();
ArduinoOTA.onStart([]() {
toSave(); // vor dem 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};
if (constexpr uint8_t interval {100}; millis() - previousMillis >= interval) {
previousMillis += interval;
localTime();
sunRun();
dualTimerSwitch();
}
}
Admin.ino
// ****************************************************************
// Arduino IDE Tab Esp8266 Admin IPv6
// created: Jens Fleischer, 2020-01-26
// last mod: Jens Fleischer, 2024-05-05
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.6.0 - 3.1.2
// Geprüft: von 1MB bis 16MB Flash
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Switch, Sonoff Dual
/******************************************************************
Copyright (c) 2020 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Admin sollte als Tab eingebunden werden.
// #include <LittleFS.h> #include <ESP8266WebServer.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP8266 Webservers ist erforderlich.
// Die LittleFS.ino muss im ESP8266 Webserver enthalten sein
// Funktion "admin();" muss im setup() nach setupFS() aber vor dem Verbindungsaufbau aufgerufen werden.
// Die Funktion "runtime();" muss mindestens zweimal innerhalb 49 Tage aufgerufen werden.
// Entweder durch den Client(Webseite) oder zur Sicherheit im "loop();"
// In der IDE unter Werkzeuge auf lwip Variant: ;"v2 IPv6 Lower Memory;" einstellen.
/**************************************************************************************/
#include <AddrList.h>
const char* const PROGMEM flashChipMode[] = {"QIO", "QOUT", "DIO", "DOUT", "Unbekannt"};
void admin() { // Funktionsaufruf "admin();" muss im Setup eingebunden werden
File file = LittleFS.open(existFolder("Config") + "/config.json", "r");
if (file) {
String newhostname = file.readStringUntil('\n');
if (newhostname != "") {
WiFi.hostname(newhostname.substring(1, newhostname.length() - 1));
file.close();
ArduinoOTA.setHostname(WiFi.hostname().c_str());
}
}
server.on("/admin/renew", handlerenew);
server.on("/admin/once", handleonce);
server.on("/reconnect", []() {
server.send(304, "message/http");
WiFi.reconnect();
});
server.on("/restart", []() {
server.send(304, "message/http");
toSave(); //Wenn Werte vor dem Neustart gespeichert werden sollen
ESP.restart();
});
}
//Es kann entweder die Spannung am ADC-Pin oder die Modulversorgungsspannung (VCC) ausgegeben werden.
/*
void handlerenew() { // Um die am ADC-Pin anliegende externe Spannung zu lesen, verwende analogRead (A0)
server.send(200, "application/json", "[\"" + runtime() + "\",\"" + WiFi.RSSI() + "\",\"" + analogRead(A0) + "\"]"); // Json als Array
}
*/
ADC_MODE(ADC_VCC);
void handlerenew() { // Zum Lesen der Modulversorgungsspannung (VCC), verwende ESP.getVcc()
server.send(200, "application/json", "[\"" + runtime() + "\",\"" + WiFi.RSSI() + "\",\"" + ESP.getVcc() / 1024.0 + " V" + "\"]");
}
void handleonce() {
String ipv6Local, ipv6Global;
#if LWIP_IPV6
for (auto &a : addrList) {
if (a.isV6()) a.isLocal() ? ipv6Local = a.toString() : ipv6Global = a.toString();
}
#endif
if (server.arg(0) != "") {
WiFi.hostname(server.arg(0));
File f = LittleFS.open(existFolder("Config") + "/config.json", "w"); // Datei zum schreiben öffnen
f.printf("\"%s\"\n", WiFi.hostname().c_str());
f.close();
}
String temp = "{\"File\":\"" + sketchName() + "\", \"Build\":\"" + __DATE__ + " " + __TIME__ + "\", \"SketchSize\":\"" + formatBytes(ESP.getSketchSize()) +
"\", \"SketchSpace\":\"" + formatBytes(ESP.getFreeSketchSpace()) + "\", \"LocalIP\":\"" + WiFi.localIP().toString() + "\", \"IPv6l\":\"" + ipv6Local +
"\", \"IPv6g\":\"" + ipv6Global + "\", \"Hostname\":\"" + WiFi.hostname() + "\", \"SSID\":\"" + WiFi.SSID() + "\", \"GatewayIP\":\"" + WiFi.gatewayIP().toString() +
"\", \"Channel\":\"" + WiFi.channel() + "\", \"MacAddress\":\"" + WiFi.macAddress() + "\", \"SubnetMask\":\"" + WiFi.subnetMask().toString() +
"\", \"BSSID\":\"" + WiFi.BSSIDstr() + "\", \"ClientIP\":\"" + server.client().remoteIP().toString() + "\", \"DnsIP\":\"" + WiFi.dnsIP().toString() +
"\", \"ResetReason\":\"" + ESP.getResetReason() + "\", \"CpuFreqMHz\":\"" + F_CPU / 1000000 + "\", \"FreeHeap\":\"" + formatBytes(ESP.getFreeHeap()) +
"\", \"HeapFrag\":\"" + ESP.getHeapFragmentation() + "\", \"ChipSize\":\"" + formatBytes(ESP.getFlashChipSize()) +
"\", \"ChipSpeed\":\"" + ESP.getFlashChipSpeed() / 1000000 + "\", \"ChipMode\":\"" + flashChipMode[ESP.getFlashChipMode()] + "\", \"IdeVersion\":\"" + ARDUINO +
"\", \"C++Version\":\"" + __cplusplus % 10000 / 100 + "\", \"CoreVersion\":\"" + ESP.getCoreVersion() + "\", \"SdkVersion\":\"" + ESP.getSdkVersion() + "\"}";
server.send(200, "application/json", temp); // Json als Objekt
}
String runtime() {
static uint8_t rolloverCounter;
static uint32_t previousMillis;
uint32_t currentMillis {millis()};
if (currentMillis < previousMillis) rolloverCounter++; // prüft millis() auf Überlauf
previousMillis = currentMillis;
uint32_t sec {(0xFFFFFFFF / 1000) * rolloverCounter + (currentMillis / 1000)};
char buf[20];
snprintf(buf, sizeof(buf), "%*.d %.*s %02d:%02d:%02d", sec < 86400 ? 0 : 1, sec / 86400, sec < 86400 ? 0 : sec >= 172800 ? 4 : 3, "Tage", sec / 3600 % 24, sec / 60 % 60, sec % 60);
return buf;
}
In diesem Tab sind deine Wlan Zugangsdaten einzutragen.
Connect.ino
// ****************************************************************
// Arduino IDE Tab Esp8266 Connect STA mit optischer Anzeige
// created: Jens Fleischer, 2018-04-08
// last mod: Jens Fleischer, 2020-12-28
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.4.2 - 3.1.2
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Dual
/******************************************************************
Copyright (c) 2018 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Connect STA sollte als Tab eingebunden werden.
// #include <ESP8266WebServer.h> oder #include <ESP8266WiFi.h> muss im Haupttab aufgerufen werden
// Die Funktion "connectWifi();" muss im Setup eingebunden werden.
/**************************************************************************************/
//#define CONFIG // Einkommentieren wenn der ESP dem Router die IP mitteilen soll.
#define NO_SLEEP // Auskommentieren wenn der Nodemcu den deep sleep Modus nutzt.
const char* ssid = "Netzwerkname"; // << 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 connectWifi() {
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(PSTR("\nVerbindung zum AP fehlgeschlagen !\n\n"));
ESP.restart();
}
}
Serial.println("\nVerbunden mit: " + WiFi.SSID());
Serial.printf(PSTR("\nGib diese URL in deinem Browser ein: %s/fs.html\n\n"), WiFi.localIP().toString().c_str());
}
In diesem Tab einstellen ob LOW oder HIGH aktiv geschaltet wird.
Und die Anzahl der Schaltzeiten einstellen. (1 bis 100).
Dualschaltuhr.ino
// ****************************************************************
// Arduino IDE Tab Esp8266 Zeitschaltuhr Dual Modular
// created: Jens Fleischer, 2019-03-08
// last mod: Jens Fleischer, 2024-01-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
// 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 3.0.0 - 3.1.2
// Getestet auf: Nodemcu, Wemos D1 Mini Pro
/******************************************************************
Copyright (c) 2019 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Dualschaltuhr 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.
/**************************************************************************************/
#include "shorttimer.h"
#include <tuple>
#include <vector>
constexpr auto ACTIVE = LOW; // LOW für LOW aktive Relais oder HIGH für HIGH aktive (zB. SSR, Mosfet) einstellen
constexpr uint8_t DEVICE_PIN[] = {D5, D6}; // Pin für Device einstellen
constexpr uint8_t RECORDS = 20; // Anzahl Schaltzeiten festlegen 1 - 100
bool pinState[] = {!ACTIVE, !ACTIVE};
using namespace std;
using Subset = tuple<bool, uint8_t, int16_t, int16_t>;
vector<Subset> device;
uint16_t sunTime[4];
struct {
uint16_t sun;
bool lock[2];
} option;
ShortTimer timer1, timer2;
void setupTimerSwitch() {
for (const auto& pin : DEVICE_PIN) digitalWrite(pin, !ACTIVE), pinMode(pin, OUTPUT);
device.reserve(RECORDS);
Subset t = make_tuple(1, 127, -1, -1);
File file = LittleFS.open(existFolder("Config") + "/dtime.dat", "r");
if (file) { // Einlesen aller Daten falls die Datei im LittleFS vorhanden ist.
file.read(reinterpret_cast<uint8_t*>(&option), sizeof(option));
for (uint8_t i {0}; i < device.capacity(); i++) {
file.read(reinterpret_cast<uint8_t*>(&t), sizeof(Subset));
device.emplace_back(t);
}
file.close();
}
else {
Serial.println(F("Die \"dtime.dat\" ist nicht vorhanden!"));
for (uint8_t i {0}; i < device.capacity(); i++) device.push_back(t);
}
server.on("/timer", HTTP_POST, []() {
if (server.hasArg("dTime")) {
device.clear();
Subset t = make_tuple(0, 0, 0, 0);
char str[server.arg("dTime").length()];
strcpy (str, server.arg("dTime").c_str());
char* ptr = strtok(str, "[\"");
for (auto i {0}; ptr != NULL; i++, i %= 4) {
if (strcmp(ptr, ",")) {
if (i == 0) {
get<0>(t) = atoi(ptr);
}
else if (i == 1) {
get<1>(t) = atoi(ptr);
}
else if (i == 2) {
get<2>(t) = atoi(ptr);
}
else if (i == 3) {
get<3>(t) = atoi(ptr);
device.emplace_back(t);
}
}
ptr = strtok(NULL, "\",[]");
}
printer();
}
String temp = "[";
for (char buf[8]; auto &t : device) {
temp == "[" ? temp += "[" : temp += ",[";
temp += "\"" + static_cast<String>(get<0>(t)) + "\",";
temp += "\"" + static_cast<String>(get<1>(t)) + "\",";
snprintf(buf, sizeof(buf), "%02d:%02d", (get<2>(t) / 100 % 100), (get<2>(t) % 100));
temp += "\"" + static_cast<String>(buf) + "\",";
snprintf(buf, sizeof(buf), "%02d:%02d", (get<3>(t) / 100 % 100), (get<3>(t) % 100));
temp += "\"" + static_cast<String>(buf) + "\"]";
}
temp += "]";
server.send(200, "application/json", temp);
});
server.on("/timer", HTTP_GET, []() {
char buf[60];
if (!server.hasArg("time")) {
if (server.hasArg("switch")) {
if (server.arg("switch") == "0" ) {
timer1.stop();
pinState[Device_I] = !pinState[Device_I]; // Pin Status manuell ändern
timeDataLogger(Device_I, server.client().remoteIP().toString()); // Funktionsaufruf Zeitdatenlogger
}
else if (server.arg("switch") == "1" ) {
timer2.stop();
pinState[Device_II] = !pinState[Device_II]; // Pin Status manuell ändern
timeDataLogger(Device_II, server.client().remoteIP().toString()); // Funktionsaufruf Zeitdatenlogger
}
}
else if (server.hasArg("lock")) {
if (server.arg("lock") == "0" && !timer1.timeIsRun()) {
option.lock[Device_I] = !option.lock[Device_I]; // alle Schaltzeiten deaktivieren/aktivieren
}
else if (server.arg("lock") == "1" && !timer2.timeIsRun()) {
option.lock[Device_II] = !option.lock[Device_II]; // alle Schaltzeiten deaktivieren/aktivieren
}
printer();
}
else if (server.hasArg("run")) {
if (server.arg("run") == "0") {
if (server.hasArg("start")) {
timer1.start(server.arg("start").toInt());
}
else if (server.hasArg("pause")) {
timer1.pause();
}
}
else {
if (server.hasArg("start")) {
timer2.start(server.arg("start").toInt());
}
if (server.hasArg("pause")) {
timer2.pause();
}
}
}
else if (server.hasArg("sun")) {
if (server.hasArg("select")) {
option.sun = server.arg("select").toInt();
printer();
}
snprintf(buf, sizeof(buf), "[[\"%02d:%02d\",\"%02d:%02d\",\"%02d:%02d\",\"%02d:%02d\"],\"%d\"]",
sunTime[0] / 100 % 100, sunTime[0] % 100, sunTime[1] / 100 % 100, sunTime[1] % 100,
sunTime[2] / 100 % 100, sunTime[2] % 100, sunTime[3] / 100 % 100, sunTime[3] % 100, option.sun);
return server.send(200, "application/json", buf);
}
}
snprintf(buf, sizeof(buf), PSTR(R"(["%d","%d","%d","%d",["%02d:%02d","%02d:%02d","%d","%d"],"%s"])"),
pinState[Device_I] == ACTIVE, pinState[Device_II] == ACTIVE, option.lock[Device_I], option.lock[Device_II],
timer1.timeLeft() / 60, timer1.timeLeft() % 60, timer2.timeLeft() / 60, timer2.timeLeft() % 60, timer1.timeIsRun(), timer2.timeIsRun(), localTime());
server.send(200, "application/json", buf);
});
}
void printer() {
//for (uint8_t i {0}; const auto &e : device) Serial.printf(PSTR("Subset %d: %d, %d, %d, %d\n"), ++i, get<0>(e), get<1>(e), get<2>(e), get<3>(e));
File file = LittleFS.open(existFolder("Config") + "/dtime.dat", "w");
if (file) {
file.write(reinterpret_cast<const uint8_t*>(&option), sizeof(option));
for (const auto &e : device) {
file.write(reinterpret_cast<const uint8_t*>(&e), sizeof(e));
}
file.close();
}
}
void dualTimerSwitch() {
static uint8_t lastmin {CHAR_MAX}, lastState[] {!ACTIVE, !ACTIVE};
hobbsMeter(pinState[Device_I], pinState[Device_II]); // Funktionsaufruf Betriebsstundenzähler mit Pin Status
if (static bool timerFlag, statusFlag; timer1.timeIsRun()) {
pinState[Device_I] = ACTIVE;
timeDataLogger(Device_I, "Timer");
if (!timerFlag) statusFlag = option.lock[Device_I];
option.lock[Device_I] = true; // alle Schaltzeiten sperren
timerFlag = true;
}
else {
if (timerFlag) {
pinState[Device_I] = !ACTIVE;
timeDataLogger(Device_I, "Timer");
option.lock[Device_I] = statusFlag; // vorherigen Zustand wiederherstellen
}
timerFlag = false;
}
if (static bool timerFlag, statusFlag; timer2.timeIsRun()) {
pinState[Device_II] = ACTIVE;
timeDataLogger(Device_II, "Timer");
if (!timerFlag) statusFlag = option.lock[Device_II];
option.lock[Device_II] = true; // alle Schaltzeiten sperren
timerFlag = true;
}
else {
if (timerFlag) {
pinState[Device_II] = !ACTIVE;
timeDataLogger(Device_II, "Timer");
option.lock[Device_II] = statusFlag; // vorherigen Zustand wiederherstellen
}
timerFlag = false;
}
if (tm.tm_min != lastmin) {
lastmin = tm.tm_min;
const uint16_t currentTime = tm.tm_hour * 100 + tm.tm_min;
for (uint8_t i{0}; auto &t : device) {
if (get<bool>(t) && (get<1>(t) & (1 << (tm.tm_wday ? tm.tm_wday - 1 : 6)))) {
if (i < (device.size() / 2) && !option.lock[Device_I]) {
if (get<2>(t) == currentTime) pinState[Device_I] = ACTIVE;
if (get<3>(t) == currentTime) pinState[Device_I] = !ACTIVE;
if (pinState[Device_I] != lastState[Device_I]) timeDataLogger(Device_I, "Uhrzeit");
}
else if (i >= (device.size() / 2) && !option.lock[Device_II]) {
if (get<2>(t) == currentTime) pinState[Device_II] = ACTIVE;
if (get<3>(t) == currentTime) pinState[Device_II] = !ACTIVE;
if (pinState[Device_II] != lastState[Device_II]) timeDataLogger(Device_II, "Uhrzeit");
}
}
i++;
}
sunSwitch (currentTime);
}
if (pinState[Device_I] != lastState[Device_I] || pinState[Device_II] != lastState[Device_II]) { // Relais schalten wenn sich der Status geändert hat
for (auto i{0}; auto &state : pinState) {
lastState[i] = state;
digitalWrite(DEVICE_PIN[i], state);
Serial.printf("Relais %d %s\n", 1 + i, digitalRead(DEVICE_PIN[i]) == ACTIVE ? "an" : "aus");
i++;
}
}
}
void sunSwitch (const uint16_t &cTime) { // Pin Status zum Sonnenstand ändern
bool laststate[] = {pinState[Device_I], pinState[Device_II]};
for (uint8_t i{0}; auto &t : sunTime) {
if (t == cTime && !option.lock[Device_I]) {
if (option.sun & (1 << i * 2)) pinState[Device_I] = !ACTIVE;
if (option.sun & (1 << (i * 2 + 1))) pinState[Device_I] = ACTIVE;
if (pinState[Device_I] != laststate[0]) timeDataLogger(Device_I, "Sonnenstand");
}
if (t == cTime && !option.lock[Device_II]) {
if (option.sun & (1 << (i * 2 + 8))) pinState[Device_II] = !ACTIVE;
if (option.sun & (1 << (i * 2 + 9))) pinState[Device_II] = ACTIVE;
if (pinState[Device_II] != laststate[1]) timeDataLogger(Device_II, "Sonnenstand");
}
i++;
}
}
LittleFS.ino
// ****************************************************************
// Arduino IDE Tab Esp8266 Filesystem Manager spezifisch sortiert Modular
// created: Jens Fleischer, 2020-06-08
// last mod: Jens Fleischer, 2024-05-05
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 3.0.0 - 3.1.2
// Geprüft: von 1MB bis 16MB Flash
// Getestet auf: Nodemcu, Wemos D1 Mini Pro
/******************************************************************
Copyright (c) 2020 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Diese Version von LittleFS sollte als Tab eingebunden werden.
// #include <LittleFS.h> #include <ESP8266WebServer.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP8266 Webservers ist erforderlich.
// "server.onNotFound()" darf nicht im Setup des ESP8266 Webserver stehen.
// Die Funktion "setupFS();" muss im Setup aufgerufen werden.
/**************************************************************************************/
#include <list>
#include <tuple>
const char WARNING[] PROGMEM = R"(<h2>Der Sketch wurde mit "FS:none" kompilliert!)";
const char HELPER[] PROGMEM = R"(<form method="POST" action="/upload" enctype="multipart/form-data">
<input type="file" name="[]" multiple><button>Upload</button></form>Lade die fs.html hoch.)";
void setupFS() { // Funktionsaufruf "setupFS();" muss im Setup eingebunden werden
LittleFS.begin();
server.on("/format", formatFS);
server.on("/upload", HTTP_POST, sendResponce, handleUpload);
server.onNotFound([](String path = server.urlDecode(server.uri())) {
if (!handleFile(path)) server.send(404, "text/html", F("Not Found: ") + path);
});
}
bool handleList() { // Senden aller Daten an den Client
FSInfo fs_info; LittleFS.info(fs_info); // Füllt FSInfo Struktur mit Informationen über das Dateisystem
Dir dir = LittleFS.openDir("/");
using namespace std;
using records = tuple<String, String, size_t, time_t>;
list<records> dirList;
while (dir.next()) { // Ordner und Dateien zur Liste hinzufügen
if (dir.isDirectory()) {
uint8_t ran {0};
Dir fold = LittleFS.openDir(dir.fileName());
while (fold.next()) {
ran++;
dirList.emplace_back(dir.fileName(), fold.fileName(), fold.fileSize(), fold.fileTime());
}
if (!ran) dirList.emplace_back(dir.fileName(), "", 0, 0);
}
else {
dirList.emplace_back("", dir.fileName(), dir.fileSize(), dir.fileTime());
}
}
dirList.sort([](const records & f, const records & l) { // Dateien sortieren
if (server.arg(0) == "1") { // nach Größe
return get<2>(f) > get<2>(l);
} else if (server.arg(0) == "2") { // nach Zeit
return get<3>(f) > get<3>(l);
} else { // nach Name
for (uint8_t i = 0; i < 31; i++) {
if (tolower(get<1>(f)[i]) < tolower(get<1>(l)[i])) return true;
else if (tolower(get<1>(f)[i]) > tolower(get<1>(l)[i])) return false;
}
return false;
}
});
dirList.sort([](const records & f, const records & l) { // Ordner sortieren
if (get<0>(f)[0] != 0x00 || get<0>(l)[0] != 0x00) {
for (uint8_t i = 0; i < 31; i++) {
if (tolower(get<0>(f)[i]) < tolower(get<0>(l)[i])) return true;
else if (tolower(get<0>(f)[i]) > tolower(get<0>(l)[i])) return false;
}
}
return false;
});
String temp = "[";
for (auto& t : dirList) {
if (temp != "[") temp += ',';
temp += "{\"folder\":\"" + get<0>(t) + "\",\"name\":\"" + get<1>(t) + "\",\"size\":\"" + formatBytes(get<2>(t)) + "\",\"time\":\"" + get<3>(t) + "\"}";
}
temp += ",{\"usedBytes\":\"" + formatBytes(fs_info.usedBytes) + // Berechnet den verwendeten Speicherplatz
"\",\"totalBytes\":\"" + formatBytes(fs_info.totalBytes) + // Zeigt die Größe des Speichers
"\",\"freeBytes\":\"" + (fs_info.totalBytes - fs_info.usedBytes) + "\"}]"; // Berechnet den freien Speicherplatz
server.send(200, "application/json", temp);
return true;
}
void deleteRecursive(const String &path) {
if (LittleFS.remove(path)) {
LittleFS.open(path.substring(0, path.lastIndexOf('/')) + "/", "w");
return;
}
Dir dir = LittleFS.openDir(path);
while (dir.next()) {
deleteRecursive(path + '/' + dir.fileName());
}
LittleFS.rmdir(path);
}
bool handleFile(String &path) {
if (!LittleFS.exists("fs.html")) server.send(200, "text/html", LittleFS.begin() ? HELPER : WARNING); // ermöglicht das hochladen der fs.html
if (!(PROTECT && !authenticated())) {
if (server.hasArg("new")) {
for (auto& c : {34, 37, 38, 47, 58, 59, 92}) for (auto& e : server.arg("new")) if (e == c) return sendResponce(); // Abbrechen bei nicht erlaubten Zeichen
LittleFS.mkdir(server.arg("new"));
}
if (server.hasArg("sort")) return handleList();
if (server.hasArg("delete")) {
deleteRecursive(server.arg("delete"));
return sendResponce();
}
if (path.endsWith("/")) path += "index.html";
File f = LittleFS.open(path, "r");
String eTag = String(f.getLastWrite(), HEX); // Verwendet den Zeitstempel der Dateiänderung, um den ETag zu erstellen.
if (server.header("If-None-Match") == eTag) {
server.send(304);
return true;
}
server.sendHeader("ETag", eTag);
return LittleFS.exists(path) ? server.streamFile(f, mime::getContentType(path)) : false;
}
return false;
}
void handleUpload() { // Dateien ins Filesystem schreiben
static File fsUploadFile;
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
if (upload.filename.length() > 31) { // Dateinamen kürzen
upload.filename = upload.filename.substring(upload.filename.length() - 31, upload.filename.length());
}
printf(PSTR("handleFileUpload Name: /%s\n"), upload.filename.c_str());
fsUploadFile = LittleFS.open(server.arg(0) + "/" + server.urlDecode(upload.filename), "w");
} else if (upload.status == UPLOAD_FILE_WRITE) {
printf(PSTR("handleFileUpload Data: %u\n"), upload.currentSize);
fsUploadFile.write(upload.buf, upload.currentSize);
} else if (upload.status == UPLOAD_FILE_END) {
printf(PSTR("handleFileUpload Size: %u\n"), upload.totalSize);
fsUploadFile.close();
}
}
void formatFS() { // Formatiert das Filesystem
LittleFS.format();
sendResponce();
}
bool sendResponce() {
server.sendHeader("Location", "fs.html");
server.send(303, "message/http");
return true;
}
const String formatBytes(size_t const& bytes) { // lesbare Anzeige der Speichergrößen
return bytes < 1024 ? static_cast<String>(bytes) + " Byte" : bytes < 1048576 ? static_cast<String>(bytes / 1024.0) + " KB" : static_cast<String>(bytes / 1048576.0) + " MB";
}
String existFolder( String const& foldername) {
if (!LittleFS.exists(foldername)) LittleFS.mkdir(foldername);
return foldername;
}
bool authenticated() {
if (server.authenticate(www_username, www_password)) return true;
server.requestAuthentication(DIGEST_AUTH, "Anmeldung erforderlich", "Authentifizierung fehlgeschlagen!");
return false;
}
Lokalzeit.ino
// ****************************************************************
// Arduino IDE Tab Esp8266 SNTP Lokalzeit Modular
// created: Jens Fleischer, 2020-10-10
// last mod: Jens Fleischer, 2020-12-25
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.6.0 - 3.1.2
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Switch, Sonoff Dual
/******************************************************************
Copyright (c) 2020 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Lokalzeit sollte als Tab eingebunden werden.
// #include <ESP8266WebServer.h> muss im Haupttab aufgerufen werden.
// Funktion "setupTime();" muss im setup() nach dem Verbindungsaufbau aufgerufen werden.
// Automatische Umstellung zwischen Sommer- und Normalzeit anhand der Zeitzone.
/**************************************************************************************/
#include <time.h>
constexpr 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_SHORT[] = {"Jan", "Feb", "Mrz", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"};
void setupTime() { // deinen NTP Server einstellen (von 0 - 5 aus obiger Liste) alternativ lassen sich durch Komma getrennt bis zu 3 Server angeben
configTime("CET-1CEST,M3.5.0,M10.5.0/3", NTP_SERVER[1]); // Zeitzone einstellen https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
server.on("/time", []() {
server.send(200, "application/json", localTime());
});
}
uint32_t sntp_update_delay_MS_rfc_not_less_than_15000() { // Optionale Funktion, für den Individuellen SNTP Update Intervall. Standart ist jede Stunde.
return SYNC_INTERVAL * 36e5; // SNTP-Aktualisierungsverzögerung ändern.
}
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/
}
return buf;
}
shorttimer.h
// ****************************************************************
// Esp8266 Klasse ShortTimer
// created: Jens Fleischer, 2024-01-18
// last mod: Jens Fleischer, 2024-01-18
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 3.0.0 -3.1.2
// Getestet auf: Nodemcu, Wemos D1 Mini Pro
/******************************************************************
Copyright (c) 2024 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.
*******************************************************************/
class ShortTimer {
public:
void start(uint16_t time_sec) {
_duration = _ms(time_sec);
_paused = false;
if (_duration > 0) {
_startTime = millis();
_expired = false;
}
}
void pause() {
if (_paused) {
start(_pausedTime);
}
else {
_pausedTime = timeLeft();
_paused = true;
}
}
void stop() {
_expired = true;
_paused = false;
}
bool timeIsRun() {
if (_paused) return false;
bool result = (!_expired && !((millis() - _startTime) >= _duration));
if (!result) _expired = true;
return result;
}
uint16_t timeLeft() {
if (_paused) return _pausedTime;
if (!timeIsRun()) return 0;
return _sec(_duration - (millis() - _startTime));
}
private:
constexpr uint32_t _ms(uint16_t x) {
return x * 1000;
}
constexpr uint16_t _sec(uint32_t x) {
return x * 0.001;
}
uint32_t _duration{0};
uint32_t _startTime{0};
uint16_t _pausedTime{0};
bool _expired{true};
bool _paused{false};
};
In diesem Tab den Längen- und Breitengrad deines Standortes angeben.
Sonnenlauf.ino
// ****************************************************************
// Arduino IDE Tab Esp8266 Sonnenlauf Modular
// source: https://lexikon.astronomie.info/zeitgleichung/neu.html
// created: Jens Fleischer, 2019-01-05
// last mod: Jens Fleischer, 2023-01-21
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.4.2 - 3.1.2
// Getestet auf: Nodemcu, Wemos D1 Mini Pro
/******************************************************************
Copyright (c) 2018 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Sonnenlauf sollte als Tab eingebunden werden.
// Der Lokalzeit Tab (bzw. "struct tm") ist zum ausführen erforderlich.
// Gib zunächst den Längen- und Breitengrad deines Ortes an.
// https://www.laengengrad-breitengrad.de/
/**************************************************************************************/
constexpr double LONGITUDE = 12.348239; // Geographische Länge
constexpr double LATITUDE = 51.346030; // Geographische Breite
void sunRun() {
static uint8_t lastday, lastdst;
if (tm.tm_mday != lastday || tm.tm_isdst != lastdst) { // Sonnenlauf für Tage mit Zeitumstellung zweimal Täglich berechnen
lastday = tm.tm_mday;
lastdst = tm.tm_isdst;
const double w = LATITUDE * DEG_TO_RAD;
double JD = julianDate(1900 + tm.tm_year, 1 + tm.tm_mon, tm.tm_mday);
double T = (JD - 2451545.0) / 36525.0;
double DK;
double EOT = calculateEOT(DK, T);
double h = -0.833333333333333 * DEG_TO_RAD; // Sonenaufgang/Sonnenuntergang
double differenceTime = 12.0 * acos((sin(h) - sin(w) * sin(DK)) / (cos(w) * cos(DK))) / PI;
sunTime[1] = outputFormat((12.0 - differenceTime - EOT) - LONGITUDE / 15.0 + (_timezone * -1) / 3600 + tm.tm_isdst);
sunTime[2] = outputFormat((12.0 + differenceTime - EOT) - LONGITUDE / 15.0 + (_timezone * -1) / 3600 + tm.tm_isdst);
h = -6.0 * DEG_TO_RAD; // Bürgerliche Dämmerung
differenceTime = 12.0 * acos((sin(h) - sin(w) * sin(DK)) / (cos(w) * cos(DK))) / PI;
sunTime[0] = outputFormat((12.0 - differenceTime - EOT) - LONGITUDE / 15.0 + (_timezone * -1) / 3600 + tm.tm_isdst);
sunTime[3] = outputFormat((12.0 + differenceTime - EOT) - LONGITUDE / 15.0 + (_timezone * -1) / 3600 + tm.tm_isdst);
}
}
double julianDate (int y, int m, int d) { // Gregorianischer Kalender
if (m <= 2) {
m = m + 12;
y = y - 1;
}
int gregorian = (y / 400) - (y / 100) + (y / 4); // Gregorianischer Kalender
return 2400000.5 + 365.0 * y - 679004.0 + gregorian + (30.6001 * (m + 1)) + d + 12.0 / 24.0;
}
double InPi(double x) {
int n = x / TWO_PI;
x = x - n * TWO_PI;
if (x < 0) x += TWO_PI;
return x;
}
double calculateEOT(double &DK, double T) {
double RAm = 18.71506921 + 2400.0513369 * T + (2.5862e-5 - 1.72e-9 * T) * T * T;
double M = InPi(TWO_PI * (0.993133 + 99.997361 * T));
double L = InPi(TWO_PI * ( 0.7859453 + M / TWO_PI + (6893.0 * sin(M) + 72.0 * sin(2.0 * M) + 6191.2 * T) / 1296.0e3));
double e = DEG_TO_RAD * (23.43929111 + (-46.8150 * T - 0.00059 * T * T + 0.001813 * T * T * T) / 3600.0); // Neigung der Erdachse
double RA = atan(tan(L) * cos(e));
if (RA < 0.0) RA += PI;
if (L > PI) RA += PI;
RA = 24.0 * RA / TWO_PI;
DK = asin(sin(e) * sin(L));
RAm = 24.0 * InPi(TWO_PI * RAm / 24.0) / TWO_PI;
double dRA = RAm - RA;
if (dRA < -12.0) dRA += 24.0;
if (dRA > 12.0) dRA -= 24.0;
dRA = dRA * 1.0027379;
return dRA ;
}
uint16_t outputFormat(double sunTime) {
if (sunTime < 0) sunTime += 24;
else if (sunTime >= 24) sunTime -= 24;
int8_t decimal = 60 * (sunTime - static_cast<int>(sunTime)) + 0.5;
int8_t predecimal = sunTime;
if (decimal >= 60) {
decimal -= 60;
predecimal++;
}
else if (decimal < 0) {
decimal += 60;
predecimal--;
if (predecimal < 0) predecimal += 24;
}
return predecimal * 100 + decimal;
}
Stundenzaehler.ino
// ****************************************************************
// Arduino IDE Tab Esp8266 Betriebsstundenzähler Dual
// created: Jens Fleischer, 2018-11-25
// last mod: Jens Fleischer, 2024-01-21
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.6.0 - 3.1.2
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Dual
/******************************************************************
Copyright (c) 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 <LittleFS.h> muss im Haupttab aufgerufen werden.
// Die Funktion "setupHobbsMeter();" muss im Setup aufgerufen werden.
/**************************************************************************************/
uint16_t watt[2];
uint32_t totalmin[2];
void setupHobbsMeter() {
File file = LittleFS.open(existFolder("Config") + "/hobbs.json", "r"); // Betriebstunden(minuten) beim Neustart einlesen
if (file) {
char buf[file.size()];
file.readBytes(buf, sizeof buf);
totalmin[Device_I] = atoi(strtok(buf, "{\"runtimeOne:"));
totalmin[Device_II] = atoi(strtok(NULL, "\",runtimeTwo:"));
watt[Device_I] = atoi(strtok(NULL, "\",wattageOne:"));
watt[Device_II] = atoi(strtok(NULL, "\",wattageTwo:"));
file.close();
}
server.on("/hobbs", HTTP_GET, []() {
uint32_t power[] {0, 0};
if (server.hasArg("device")) {
server.arg("device") == "0" ? watt[Device_I] = server.arg(1).toInt() : watt[Device_II] = server.arg(1).toInt();
toSave();
}
if (server.hasArg("reset")) {
server.arg(0) == "0" ? totalmin[Device_I] = 0 : totalmin[Device_II] = 0; // Betriebsstundenzähler zurücksetzen
toSave();
}
power[Device_I] = (watt[Device_I] * totalmin[Device_I]) / 6000;
power[Device_II] = (watt[Device_II] * totalmin[Device_II]) / 6000;
char buf[72];
snprintf(buf, sizeof(buf), PSTR("[\"%d,%d\",\"%d,%d\",\"%d\",\"%d\",\"%d,%d\",\"%d,%d\"]"),
totalmin[Device_I] / 60, totalmin[Device_I] / 6 % 10, totalmin[Device_II] / 60, totalmin[Device_II] / 6 % 10,
watt[Device_I], watt[Device_II], power[Device_I] / 10, power[Device_I] % 10, power[Device_II] / 10, power[Device_II] % 10);
server.send(200, "application/json", buf);
});
}
void hobbsMeter(const bool &state_0, const bool &state_1) { // Aufrufen mit Pin Status
static uint32_t lastmin[] {0, 0}, previousMillis[] {0, 0};
uint32_t currentMillis {millis()};
if (currentMillis - previousMillis[0] >= 6e4) {
previousMillis[0] = currentMillis;
if (state_0 == ACTIVE) totalmin[Device_I]++; // Betriebstundenzähler Verbraucher 1 wird um eine Minute erhöht
if (state_1 == ACTIVE) totalmin[Device_II]++; // Betriebstundenzähler Verbraucher 2 wird um eine Minute erhöht
}
if (currentMillis - previousMillis[1] >= 864e5 && (totalmin[Device_I] != lastmin[Device_I] || totalmin[Device_II] != lastmin[Device_II])) { // einmal am Tage Betriebsstunden in Datei schreiben wenn sich der Wert geändert hat
previousMillis[1] = currentMillis;
lastmin[Device_I] = totalmin[Device_I];
lastmin[Device_II] = totalmin[Device_II];
toSave();
}
}
void toSave() {
File file = LittleFS.open(existFolder("Config") + "/hobbs.json", "w"); // Betriebstunden(minuten) speichern
if (file) {
file.printf(R"({"runtimeOne":"%u","runtimeTwo":"%u","wattageOne":"%d","wattageTwo":"%d"})", totalmin[Device_I], totalmin[Device_II], watt[Device_I], watt[Device_II]);
file.close();
}
}
Zeitlogger.ino
// ****************************************************************
// Arduino IDE Tab Esp8266 Zeitdatenlogger Dual Modular
// 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.6.0 -3.1.2
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Dual
/******************************************************************
Copyright (c) 2019 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Ereignisdatenspeicher sollte als Tab eingebunden werden.
// #include <LittleFS.h> muss im Haupttab aufgerufen werden
// Der Lokalzeit Tab ist zum ausführen des Zeitdatenlogger erforderlich.
// Der LittleFS Tab ist zum ausführen erforderlich.
/**************************************************************************************/
void timeDataLogger(const uint8_t num, const String customer) { // Relais Nummer und Ereignisauslöser
static bool lastrelState[] {!ACTIVE, !ACTIVE};
if (pinState[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_SHORT[tm.tm_mon]);
if (!LittleFS.exists(existFolder("Data") + fileName)) { // Logdatei für den aktuellen Monat anlegen falls nicht vorhanden.
File file = LittleFS.open(existFolder("Data") + fileName, "a");
if (file) { // Prüft ob die Datei geöffnet wurde.
file.printf("%s;An;Initiator;Aus;Initiator\n", MONTH_SHORT[tm.tm_mon]); // Kopfzeile schreiben
}
file.close();
char path[23];
snprintf(path, sizeof(path), "/Relais%d_%s.csv", 1 + num, MONTH_SHORT[(tm.tm_mon + 1) % 12]);
LittleFS.remove(path); // Löscht die elf Monate alte Logdatei.
}
File file = LittleFS.open(existFolder("Data") + fileName, "a"); // Die Ereignisdaten für den aktuellen Monat speichern
if (file) {
pinState[num] == ACTIVE ? file.printf("%d.;%s;%s;", tm.tm_mday, localTime(), customer.c_str()) : file.printf("%s;%s;\n", localTime(), customer.c_str());
}
file.close();
}
lastrelState[num] = pinState[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>
<script>
addEventListener('load', () => {
renew(), once();
let output = document.querySelector('#note');
let btn = document.querySelectorAll('button');
let span = document.querySelectorAll('#right span');
btn[0].addEventListener('click', () => {
location = '/fs.html';
});
btn[1].addEventListener('click', () => {
location = '/';
});
btn[2].addEventListener('click', check.bind(this, document.querySelector('input')));
btn[3].addEventListener('click', re.bind(this, 'reconnect'));
btn[4].addEventListener('click', () => {
if (confirm('Bist du sicher!')) re('restart');
});
async function once(val = '',arg) {
try {
let resp = await fetch('/admin/once', { method: 'POST', body: val});
let obj = await resp.json();
output.innerHTML = '';
output.classList.remove('note');
document.querySelector('form').reset();
if (val.length == 0) myIv = setInterval(renew, 1000);
if (arg == 'reconnect') re(arg);
span[3].innerHTML = obj['File'];
span[4].innerHTML = obj['Build'];
span[5].innerHTML = obj['SketchSize'];
span[6].innerHTML = obj['SketchSpace'];
span[7].innerHTML = obj['LocalIP'];
span[8].innerHTML = obj['IPv6l'] ? obj['IPv6l'] : 'inaktiv';
span[9].innerHTML = obj['IPv6g'] ? obj['IPv6g'] : 'inaktiv';
span[10].innerHTML = obj['Hostname'];
span[11].innerHTML = obj['SSID'];
span[12].innerHTML = obj['GatewayIP'];
span[13].innerHTML = obj['Channel'];
span[14].innerHTML = obj['MacAddress'];
span[15].innerHTML = obj['SubnetMask'];
span[16].innerHTML = obj['BSSID'];
span[17].innerHTML = obj['ClientIP'];
span[18].innerHTML = obj['DnsIP'];
span[19].innerHTML = obj['ResetReason'];
span[20].innerHTML = obj['CpuFreqMHz'] + " MHz";
span[21].innerHTML = obj['FreeHeap'];
span[22].innerHTML = obj['HeapFrag'] + "%";
span[23].innerHTML = obj['ChipSize'];
span[24].innerHTML = obj['ChipSpeed'] + " MHz";
span[25].innerHTML = obj['ChipMode'];
span[26].innerHTML = obj['IdeVersion'].replace(/(\d)(\d)(\d)(\d)/,obj['IdeVersion'][3]!=0 ? '$1.$3.$4' : '$1.$3.');
span[27].innerHTML = obj['C++Version'];
span[28].innerHTML = obj['CoreVersion'].replace(/_/g,'.');
span[29].innerHTML = obj['SdkVersion'];
Object.keys(obj).forEach(val => {
if (obj[val].length > 25) document.querySelectorAll(`[data-${val}]`).forEach(el => {el.classList.add('ip')});
});
} catch(err) {
re();
}
}
async function renew() {
const resp = await fetch('admin/renew');
const array = await resp.json();
array.forEach((v, i) => {span[i].innerHTML = v});
}
function check(inObj) {
!inObj.checkValidity() ? (output.innerHTML = inObj.validationMessage, output.classList.add('note')) : (once(inObj.value, 'reconnect'));
}
function re(arg = '') {
clearInterval(myIv);
fetch(arg);
output.classList.add('note');
if (arg == 'restart') {
output.innerHTML = 'Der Server wird neu gestartet. Die Daten werden in 15 Sekunden neu geladen.';
setTimeout(once, 15000);
}
else if (arg == 'reconnect'){
output.innerHTML = 'Die WiFi Verbindung wird neu gestartet. Daten werden in 10 Sekunden neu geladen.';
setTimeout(once, 10000);
}
else {
output.innerHTML = 'Es ist ein Verbindungfehler aufgetreten. Es wird versucht neu zu verbinden.';
setTimeout(once, 3000);
}
}
});
</script>
</head>
<body>
<h1>ESP8266 Admin Page</h1>
<main>
<aside id="left">
<span>Runtime ESP:</span>
<span>WiFi RSSI:</span>
<span>ADC/VCC:</span>
<span>Sketch Name:</span>
<span>Sketch Build:</span>
<span>SketchSize:</span>
<span>FreeSketchSpace:</span>
<span>IPv4 Address:</span>
<span data-ipv6l>Link-Local:</span>
<span data-ipv6g>IPv6:</span>
<span>Hostname:</span>
<span>Connected to:</span>
<span>Gateway IP:</span>
<span>Channel:</span>
<span>MacAddress:</span>
<span>SubnetMask:</span>
<span>BSSID:</span>
<span data-clientip>Client IP:</span>
<span>DnsIP:</span>
<span>Reset Ground:</span>
<span>CPU Freq:</span>
<span>FreeHeap:</span>
<span>Heap Fragmentation:</span>
<span>FlashSize:</span>
<span>FlashSpeed:</span>
<span>FlashMode:</span>
<span>Arduino IDE Version:</span>
<span>C++ Version:</span>
<span>Esp Core Version:</span>
<span>SDK Version:</span>
</aside>
<aside id="right">
<span>0</span>
<div>
<span></span>
dBm
</div>
<span>0</span>
<span>?</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span data-ipv6l>0</span>
<span data-ipv6g>0</span>
<span>?</span>
<span>?</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span data-clientip>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
</aside>
</main>
<div>
<button>Filesystem</button>
<button>Startseite</button>
</div>
<div id="note"></div>
<div>
<form>
<input placeholder="neuer Hostname" pattern="([A-Za-z0-9\-]{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>
fs.html
<!DOCTYPE HTML> <!-- For more information visit: https://fipsok.de -->
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<title>Filesystem Manager</title>
<script>
document.addEventListener('DOMContentLoaded', () => {
list(JSON.parse(localStorage.getItem('sortBy')));
btn.addEventListener('click', () => {
if (!confirm(`Alle Daten gehen verloren.\nDu musst anschließend fs.html wieder laden.`)) event.preventDefault();
});
});
async function list(to){
let resp = await fetch(`?sort=${to}`);
let json = await resp.json();
let myList = document.querySelector('main'), noted = '';
myList.innerHTML = '<nav><input type="radio" id="/" name="group" checked="checked"><label for="/"> 📁</label><span id="cr">+📁</nav></span><span id="si"></span>';
for (var i = 0; i < json.length - 1; i++) {
let dir = '', f = json[i].folder, n = json[i].name, t = new Date(json[i].time*1000).toLocaleString();
if (f != noted) {
noted = f;
dir = `<nav><input type="radio" id="${f}" name="group"><label for="${f}"></label> 📁 ${f} <a href="?delete=/${f}">🗑️</a></nav>`;
}
if (n != '') dir += `<li><a title="Geändert: ${t}" href="${f}/${n}">${n}</a><small> ${json[i].size}</small><a href="${f}/${n}"download="${n}"> Download</a> or<a href="?delete=${f}/${n}"> Delete</a>`;
myList.insertAdjacentHTML('beforeend', dir);
}
myList.insertAdjacentHTML('beforeend', `<li><b id="so">▼ ${to ? to > 1 ? 'Time' : 'Size' : 'A - Z'}</b> LittleFS belegt ${json[i].usedBytes.replace(".00", "")} von ${json[i].totalBytes.replace(".00", "")}`);
var free = json[i].freeBytes;
cr.addEventListener('click', () => {
document.getElementById('no').classList.toggle('no');
});
so.addEventListener('click', () => {
list(to=++to%3);
localStorage.setItem('sortBy', JSON.stringify(to));
});
document.addEventListener('change', (e) => {
if (e.target.id == 'fs') {
for (var bytes = 0, i = 0; i < event.target.files.length; i++) bytes += event.target.files[i].size;
for (var output = `${bytes} Byte`, i = 0, circa = bytes / 1024; circa > 1; circa /= 1024) output = circa.toFixed(2) + [' KB', ' MB', ' GB'][i++];
if (bytes > free) {
si.innerHTML = `<li><b> ${output}</b><strong> Ungenügend Speicher frei</strong></li>`;
up.setAttribute('disabled', 'disabled');
}
else {
si.innerHTML = `<li><b>Dateigröße:</b> ${output}</li>`;
up.removeAttribute('disabled');
}
}
document.querySelectorAll(`input[type=radio]`).forEach(el => { if (el.checked) document.querySelector('form').setAttribute('action', '/upload?f=' + el.id)});
});
document.querySelectorAll('[href^="?delete=/"]').forEach(node => {
node.addEventListener('click', () => {
if (!confirm('Sicher!')) event.preventDefault();
});
});
}
</script>
</head>
<body>
<h2>ESP8266 Filesystem Manager</h2>
<form method="post" enctype="multipart/form-data" action="/upload?f=/">
<input id="fs" type="file" name="up[]" multiple>
<button id="up" disabled>Upload</button>
</form>
<form id="no" class="no" method="POST">
<input name="new" placeholder="Ordner Name" pattern="[^\x22\/%&\\:;]{0,31}[^\x22\/%&\\:;\s]{1}" title="Zeichen “ % & / : ; \ sind nicht erlaubt." required="">
<button>Create</button>
</form>
<main></main>
<form action="/format" method="POST">
<button id="btn">Format LittleFS</button>
</form>
</body>
</html>
Die Webseite zur Dual Zeitschaltuhr.
index.html
<!DOCTYPE HTML> <!-- For more information visit: https://fipsok.de -->
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<title>Dual Timer</title>
<style>
main {
background: linear-gradient(to right, #3b82a5 ,#333 5%, #333 95%, #3b82a5);
padding:1em 1.5em;
border-radius: 2em;
}
button, input, header, footer, [data-power], [data-sun], [data-timer]{
background-color: #333 !important;
}
main, button, input:checked+label {
color: #15dfdf;
}
button, header div>*, [data-on], [data-run], span[title] {
cursor: pointer;
}
time, .timer, .sun{
font-size: 1.5em;
padding: 0;
}
input, section>div:not([data-box]), [data-run], [title] {
border: solid #555;
}
header {
position: sticky;
top: 0;
z-index: 1;
padding-bottom: .4em;
}
header div {
justify-content: space-between;
}
div:not([data-box], section) {
display: flex;
align-items: center;
}
div+[type="checkbox"] {
margin-left: 1em;
}
div+span {
display: flex;
justify-content: space-evenly;
margin-left: 1.7em;
}
span {
padding: 0.5em;
}
svg {
width: 3em;
}
time {
font-weight: bold;
}
[type=time] {
min-width: 37%;
}
input {
height: auto;
width: auto;
font-size: 3em;
font-weight: bold;
color: inherit;
padding: 0;
}
label {
font-style: italic;
color: #777;
}
button {
border: outset #999;
width: 30%;
margin: 0;
box-shadow: none;
border-radius: 2em;
}
footer button {
width: 49%;
margin-left: .2em;
}
[data-on] {
width: 2em;
}
#tip {
position: sticky;
top: 50%;
z-index: 3;
height: 0;
width: 20em;
}
[data-sun]{
align-items: flex-end !important;
}
[data-power], [data-timer], [data-sun] {
position: sticky;
flex-direction: column;
justify-content: center;
top: 6.2em;
height: 9em;
margin-bottom: .3em;
z-index: 1;
}
[data-timer] {
z-index: 2;
}
[data-power] input {
font-size: 1em;
width: 2.6em;
text-align: end;
}
[data-timer] input {
width: 1.5em;
text-align: center;
font-size: 2em;
}
output {
cursor: default;
border: solid #f00;
margin: .5em;
padding: .3em;
}
footer {
position: sticky;
bottom: .1em;
padding: .7em 0 .7em 0;
}
.dark, .dim {
opacity: .5;
}
.edit, .noedit, .reset {
justify-content: center;
width: 100%;
background-color: red;
color: #fff;
padding: 1em;
border-radius: .5em;
}
.edit:after {
content: 'Eingabe gespeichert';
}
.noedit:after {
content: 'Bitte, halte dich an das Format!';
}
.reset:after {
content: 'Zähler gelöscht';
}
.greyed, .out{
color: #666 !important;
}
.none {
display: none !important;
}
@media only screen and (max-width: 600px) {
input {
font-size: 2.2em;
}
}
</style>
<script> "use strict";
const $ = document.querySelector.bind(document);
const $$ = document.querySelectorAll.bind(document);
document.addEventListener('DOMContentLoaded', async () => {
let resp = await fetch('/timer', {method: 'post'});
let array = await resp.json();
let buf = '<div data-timer="1" class="none"></div><div data-sun="1" class="none"></div><div data-power="1" class="none"></div>';
for (let i = 0; i < array.length; i++) {
buf += '<div data-box><div><span data-on>ON</span><input type="time">\u00A0--\u00A0<input type="time"></div><span>';
['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'].forEach(d => {buf += `<input type="checkbox" checked><label>${d}</label>`;});
buf += '</span></div>';
if (i == parseInt(array.length/2-1)) {
$('[data-index="1"]').insertAdjacentHTML('beforeend', buf);
buf = '<div data-timer="2" class="none"></div><div data-sun="2" class="none"></div><div data-power="2" class="none"></div>';
}
if (i == array.length-1) $('[data-index="2"]').insertAdjacentHTML('beforeend', buf);
}
buf = '';
['Morgendämmerung', 'Sonnenaufgang', 'Sonnenuntergang', 'Abenddämmerung'].forEach(s => {
buf += `<span>${s} <span></span><input type="checkbox"><label>Aus</label><input type="checkbox"><label>Ein</label></span>`;
});
$$('[data-sun]').forEach(el => {el.insertAdjacentHTML('beforeend', buf);});
buf = '<span> Betriebsstunden <strong></strong></span><span>Verbrauch bei <input title="bis zu 4 Ziffern" pattern="[0-9]{1,4}"> Watt <output></output></span><span title="Reset">❌ Zähler zurücksetzen</span>';
$$('[data-power]').forEach(el => {el.insertAdjacentHTML('beforeend', buf);});
const box = '<input placeholder="00" pattern="[0-5]?[0-9]" title=" 0-59 ">'
for (let i = 0; i <= 1; i++) {
buf = `<div><span>Min ${box}</span><span>${box} Sek</span></div><div><output></output><span data-run="run=${i}&start=">⯈ Start</span><span data-run="run=${i}&pause=">⏸ Pause</span></div>`;
$(`[data-timer="${i+1}"]`).insertAdjacentHTML('beforeend', buf);
buf = '<button>ON</button><svg viewBox="0 0 12 14"><polygon points="10.421,6.754 6.498,6.75 12.058,2.357 9.734,2.357 1.687,8.436 5.584,8.436 0,14.02"></polygon></svg>';
$('header div').insertAdjacentHTML('beforeend', buf);
}
fill(array);
renew(), renew('sun'), setInterval(renew, 1000);
$('main').classList.remove('none');
$$('[data-timer="1"] input').forEach((el, i) => {let item = JSON.parse(localStorage.getItem('data-timer="1"')); !(i%2) ? el.value = parseInt(item/60) : el.value = item%60;});
$$('[data-timer="2"] input').forEach((el, i) => {let item = JSON.parse(localStorage.getItem('data-timer="2"')); !(i%2) ? el.value = parseInt(item/60) : el.value = item%60;});
let btn = $$('button');
btn[0].addEventListener('click', renew.bind(this, 'switch=0'));
btn[1].addEventListener('click', renew.bind(this, 'switch=1'));
btn[2].addEventListener('click', show);
btn[3].addEventListener('click', save);
btn[4].addEventListener('click', renew.bind(this, 'lock='));
$('header div').addEventListener('click', () => location = '/fs.html');
$$('[data-on]').forEach(el => {el.addEventListener('click', save)});
$('.timer').addEventListener('click', () => $('section:not(.none) [data-timer]').classList.toggle('none'));
$('.sun').addEventListener('click', () => $('section:not(.none) [data-sun]').classList.toggle('none'));
$('time').addEventListener('click', () => {if (!$('section:not(.none) [data-power]').classList.toggle('none')) hours();});
let node = $$('[data-sun] input');
node.forEach((el, i) => {
el.addEventListener('change', (event) => {
!(i%2) ? node[i+1].checked = false : node[i-1].checked = false;
let x = 0;
node.forEach((el, i) => {if (el.checked) x = x | (1 << i)});
renew('sun=0&select=' + x);
setStyle();
});
});
$$('[data-run]').forEach(el => {el.addEventListener('click', function f(){
let x=0;
$$(`[data-timer="${el.closest('[data-timer]').dataset.timer}"] [pattern]`).forEach((el, i) => {!el.checkValidity() ? out('noedit') : !(i%2) ? x=el.value*60 : x+=el.value*1;});
localStorage.setItem(`data-timer="${el.closest('[data-timer]').dataset.timer}"`, JSON.stringify(x));
renew(this.dataset.run+x);
});
});
$$('[title=Reset]').forEach((el, i) => {el.addEventListener('click', () => {if (confirm('Bist du sicher!')) hours(`reset=${i}`);});});
$$('[data-power] input').forEach((el, i) => {el.addEventListener('blur', () => {
!el.checkValidity() ? out('noedit') : hours(`device=${i}&watt=${parseInt(el.value)}`);
});
});
function show() {
this.innerText == 'Relais 1' ? btn[2].innerHTML = 'Relais 2' : btn[2].innerHTML = 'Relais 1';
$$('section').forEach(el => el.classList.toggle('none'));
setStyle();
btn[4].innerHTML = $('section:not(.none) [data-sun]').classList.contains('greyed') ? '✖ Auto inaktiv' : '⏳ Auto aktiv';
}
async function renew(arg = 'time') {
if (event) event.stopPropagation();
if (arg == 'lock=') arg += $('section:not(.none)').dataset.index-1;
const resp = await fetch(`timer?${arg}`);
const array = await resp.json();
if (arg.startsWith('sun')) {
resp.ok&&arg.startsWith('sun=')&&out('edit');
$$('[data-sun] span span').forEach((el, i) => {el.innerHTML = i<4 ? array[0][i] : array[0][i-4];});
$$('[data-sun] input').forEach((el, i) => {array[1] & (1 << i) ? el.checked = true : el.checked = false});
}else {
$$('polygon').forEach((el, i) => {el.style.fill = array[i] == 0 ? '#eee' : '#ff0'});
btn[0].innerHTML = array[0]*1 ? '✋ R1 ON' : '✋ R1 OFF';
btn[1].innerHTML = array[1]*1 ? '✋ R2 ON' : '✋ R2 OFF';
btn[4].innerHTML = array[parseInt($('section:not(.none)').dataset.index)+1]*1 ? '✖ Auto inaktiv' : '⏳ Auto aktiv';
$$('[data-index="1"] div:not([data-timer="1"],[data-timer="1"] div,[data-power]),[data-index="1"] label').forEach(el => {array[2] == 0 ? el.classList.remove('greyed') : el.classList.add('greyed')});
$$('[data-index="2"] div:not([data-timer="2"],[data-timer="2"] div,[data-power]),[data-index="2"] label').forEach(el => {array[3] == 0 ? el.classList.remove('greyed') : el.classList.add('greyed')});
$$('[data-timer] output').forEach((el, i) => {i == 0 ? el.innerHTML = array[4][0] : el.innerHTML = array[4][1]});
array[4][2] == '1' ? $('[data-timer="1"]').dataset.ison = '' : delete $('[data-timer="1"]').dataset.ison;
array[4][3] == '1' ? $('[data-timer="2"]').dataset.ison = '' : delete $('[data-timer="2"]').dataset.ison;
$('time').innerHTML = array[5]
}
setStyle();
}
});
function setStyle(check = 0) {
$$('section:not(.none) [data-sun] input').forEach(el => {if (el.checked) ++check;});
check ? $('.sun').classList.remove('dark') : $('.sun').classList.add('dark');
'ison' in $('section:not(.none) [data-timer]').dataset ? $('.timer').classList.remove('dark') : $('.timer').classList.add('dark');
$('section:not(.none) [data-sun]').classList.contains('greyed') ? $('.sun').classList.add('dim') : $('.sun').classList.remove('dim');
}
function out(arg) {
let el = $('#info').classList;
el.add(arg);
setTimeout(() => {el.remove(arg);}, 4e3);
}
function fill(arr) {
$$('[data-box]').forEach((e, i) => {
let c = e.querySelector('[data-on]');
c.textContent = (arr[i][0]*1 ? 'ON' : 'OFF');
e.querySelectorAll('[type=time]').forEach((el, j) => {el.value = arr[i][2+j]});
e.querySelectorAll('[type=checkbox]').forEach((el, j) => {arr[i][1] & (1 << j) ? el.checked = true : el.checked = false});
e.querySelectorAll('div, label').forEach(el => {c.textContent == 'ON' ? el.classList.remove('out') : el.classList.add('out')});
});
}
async function save() {
if (this.type !== 'submit') this.textContent == 'ON' ? this.textContent = 'OFF' : this.textContent = 'ON';
let form = new FormData(), data = [];
$$('[data-box]').forEach(e => {
let x = 0, arr = [e.querySelector('[data-on]').textContent == 'ON' ? '1' : '0'];
e.querySelectorAll('[type=checkbox]').forEach((el, i) => {if (el.checked) x = x | (1 << i)});
arr.push(x.toString());
e.querySelectorAll('[type=time]').forEach(el => {arr.push(el.value != 0 ? el.value.replace(':','') : -1)});
data.push(arr);
});
form.append('dTime', JSON.stringify(data));
const resp = await fetch('/timer', {method: 'post', body: form});
resp.ok&&out('edit');
const json = await resp.json();
fill(json);
}
async function hours(arg = '') {
const resp = await fetch(`hobbs?${arg}`);
resp.ok&&arg.startsWith('reset')&&out('reset');
resp.ok&&arg.startsWith('device')&&out('edit');
const json = await resp.json();
$$('strong').forEach((el, i) => {el.innerHTML = json[i] + ' h'});
$$('[data-power] input').forEach((el, i) => {el.value = json[i+2]});
$$('[data-power] output').forEach((el, i) => {el.innerHTML = json[i+4] + ' kWh'});
}
</script>
</head>
<body>
<main class="none">
<header>
<div></div>
<div>
<button>Relais 1</button>
<span class="timer dark">⏲️</span>
<span class="sun dark">☀️</span>
<time>00:00:00</time>
</div>
</header>
<div id="tip"><div id="info"></div></div>
<section data-index="1"></section>
<section data-index="2" class="none"></section>
<footer><button>⏰Zeiten Speichern</button><button></button></footer>
</main>
</body>
</html>
style.css
/* For more information visit:https://fipsok.de */
body {
font-family: sans-serif;
background-color: #87cefa;
display: flex;
flex-flow: column;
align-items: center;
}
h1,h2 {
color: #e1e1e1;
text-shadow: 2px 2px 2px black;
}
li {
background-color: #feb1e2;
list-style-type: none;
margin-bottom: 10px;
padding: 2px 5px 1px 0;
box-shadow: 5px 5px 5px rgba(0,0,0,0.7);
}
li a:first-child, li b {
background-color: #8f05a5;
font-weight: bold;
color: white;
text-decoration:none;
padding: 2px 5px;
text-shadow: 2px 2px 1px black;
cursor:pointer;
}
li strong {
color: red;
}
input {
height:35px;
font-size:14px;
padding-left: .3em;
}
label + a {
text-decoration: none;
}
h1 + main {
display: flex;
}
aside {
display: flex;
flex-direction: column;
padding: 0.2em;
}
button {
height:40px;
width:130px;
font-size:16px;
margin-top: 1em;
box-shadow: 5px 5px 5px rgba(0,0,0,0.7);
}
div button {
background-color: #7bff97;
}
nav {
display: flex;
align-items: baseline;
justify-content: space-between;
}
#left {
align-items:flex-end;
text-shadow: 0.5px 0.5px 1px #757474;
}
#cr {
font-weight: bold;
cursor:pointer;
font-size: 1.5em;
}
#up {
width: auto;
}
.note {
background-color: #fecdee;
padding: 0.5em;
margin-top: 1em;
text-align: center;
max-width: 320px;
border-radius: 0.5em;
}
.no {
display: none;
}
form [title] {
background-color: skyblue;
font-size: 1em;
width: 120px;
}
form:nth-of-type(2) {
margin-bottom: 1em;
}
[value*=Format] {
margin-top: 1em;
box-shadow: 5px 5px 5px rgba(0,0,0,0.7);
}
[name="group"] {
display: none;
}
[name="group"] + label {
font-size: 1.5em;
margin-right: 5px;
}
[name="group"] + label::before {
content: "\002610";
}
[name="group"]:checked + label::before {
content: '\002611\0027A5';
}
@media only screen and (max-width: 500px) {
.ip {
right: 6em;
position: relative;
}
aside {
max-width: 50vw;
}
}
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
Sketchname: Wochenzeitschaltuhr8266.ino
Build: Mon Jul 6 10:35:38 2020 IDE: 1.8.2
SDK:2.2.2-dev(38a443e)/Core:2.7.1=20701000/lwIP:STABLE-2_1_2_RELEASE/glue:1.2-30-g92add50/BearSSL:5c771be
SPIFFS gestartet!
1 sek
2 sek
3 sek
4 sek
Verbunden mit: Graffiti7530
Esp8266 IP: 192.168.178.30
Fehlermeldung Webserver not found 192.168.178.30/spiffs.html.
Bitte um Hilfe
Antwort:
Meist hilft es den Spiffs zu formatieren.
192.168.178.30/format
Dauert ungefähr 25 Sekunden.
Oder!
Unter Werkzeuge/Erase Flash: "All Flash Contents" einstellen.
Gruß Fips