Sonoff Dual zur Steuerung der Aussenbeleuchtung mittels 10W und 30W Led Strahler.
SonoffDual_2021.ino
// ****************************************************************
// Sketch Esp8266 Webserver Modular(Tab)
// created: Jens Fleischer, 2018-05-16
// last mod: Jens Fleischer, 2020-12-26
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.7.2 - 2.7.4
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Switch, Sonoff Dual
/******************************************************************
Copyright (c) 2018 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Der WebServer Tab ist der Haupt Tab mit "setup" und "loop".
// #include <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>
struct tm tm;
ESP8266WebServer server(80);
const String sketchName() { // Dateiname für den Admin Tab ab EspCoreVersion 2.6.0
char file[sizeof(__FILE__)] = __FILE__;
char * pos = strchr(file, '.'); *pos = '\0';
return file;
}
void setup() {
Serial.begin(19200); // Sonoff Dual braucht diese Baudrate für die Relais
setupFS();
connectWifi();
admin();
setupTime();
sonoffDual();
setupTimerSwitch();
setupHobbsMeter();
ArduinoOTA.begin();
server.begin();
}
void loop() {
ArduinoOTA.handle();
server.handleClient();
if (millis() < 0x2FFF || millis() > 0xFFFFF0FF) runtime(); // Die Funktion "runtime()" wird nur für den Admin Tab gebraucht.
static bool once {0};
if (millis() > 54e4 && !once++) connectWifi(); // Wifi Verbindung einmalig nach 9 Minuten neu starten für Stromausfall.
static uint32_t previousMillis;
uint32_t currentMillis {millis()};
if (currentMillis - previousMillis >= 100) {
previousMillis = currentMillis;
localTime();
dualTimerSwitch();
}
}
Admin.ino
// ****************************************************************
// Sketch Esp8266 Admin IPv6 Modular(Tab)
// created: Jens Fleischer, 2020-01-26
// last mod: Jens Fleischer, 2021-06-09
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.6.0 - 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 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("/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("/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 + "\", \"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;
}
Connect.ino
// ****************************************************************
// Sketch Sonoff Dual Connect Modular(Tab) mit optischer Anzeige
// created: Jens Fleischer, 2018-06-16
// last mod: Jens Fleischer, 2020-12-21
// ****************************************************************
// Hardware: Sonoff Dual
// Software: Esp8266 Arduino Core 2.4.2 - 2.7.4
// Getestet auf: 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> muss im Haupttab aufgerufen werden
// Die Funktionalität des ESP8266 Webservers ist erforderlich.
// Die Funktion "connectWifi();" muss im Setup eingebunden werden.
// Dieser Connect Tab ist speziell für das Sonoff Dual.
/**************************************************************************************/
void connectWifi() { // Funktionsaufruf "connectWifi();" muss im Setup eingebunden werden.
const char* ssid = "Netzwerkname"; // Darf bis zu 32 Zeichen haben.
const char* password = "PasswortvomNetzwerk"; // Mindestens 8 Zeichen jedoch nicht länger als 64 Zeichen.
constexpr uint8_t LEDPIN = 13; // Oneboard Led Sonoff Dual (blue)
uint8_t i = 0;
pinMode(LEDPIN, OUTPUT);
WiFi.persistent(false); // Auskommentieren wenn Netzwerkname und Passwort in den Flash geschrieben werden sollen.
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(50);
digitalWrite(LEDPIN, millis() % 500 >= 250);
if (++i > 240) ESP.restart();
}
digitalWrite(LEDPIN, HIGH);
}
Dualschaltuhr.ino
// ****************************************************************
// Sketch Esp8266 Zeitschaltuhr Dual Modular(Tab)
// created: Jens Fleischer, 2019-03-08
// last mod: Jens Fleischer, 2020-12-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 2.4.2 / 2.5.2 / 2.6.3 / 2.7.4
// 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 Wochenzeitschaltuhr 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.
// Der Lokalzeit Tab ist zum ausführen der Zeitschaltuhr einzubinden.
// Die Funktion "setupSchaltUhr();" muss im Setup aufgerufen werden.
// Zum schalten muss die Funktion "dualSchaltuhr();" im loop(); aufgerufen werden.
/**************************************************************************************/
constexpr auto ACTIVE = HIGH; // LOW für LOW aktive Relais oder HIGH für HIGH aktive (zB. SSR, Mosfet) einstellen
constexpr auto COUNT = 12; // Anzahl Schaltzeiten (analog Html Dokument) einstellen 2 bis 40
char switchTime[COUNT * 2][6];
byte switchActive[COUNT];
byte wday[COUNT];
bool relState[] = {!ACTIVE, !ACTIVE};
void setupTimerSwitch() {
File file = LittleFS.open(existFolder("Config") + "/dtime.dat", "r");
if (file) { // Einlesen aller Daten falls die Datei im LittleFS vorhanden ist.
file.read(switchActive, sizeof(switchActive));
file.read(wday, sizeof(wday));
while (file.read() != '\n');
for (auto i = 0; i < COUNT * 2; i++) {
file.readBytesUntil('\n', switchTime[i], sizeof(switchTime[i]));
}
file.close();
} else { // Sollte die Datei nicht existieren
for (auto i = 0; i < COUNT; i++) {
switchActive[i] = 1; // werden alle Schaltzeiten
wday[i] = ~wday[i]; // und alle Wochentage aktiviert.
}
}
server.on("/timer", HTTP_POST, []() {
if (server.args() == 1) {
switchActive[server.argName(0).toInt()] = server.arg(0).toInt();
toSave();
String temp = "\"";
for (auto i = 0; i < COUNT; i++) {
temp += switchActive[i];
}
temp += "\"";
server.send(200, "application/json", temp);
}
if (server.hasArg("sTime")) {
char str[COUNT * 14];
strcpy (str, server.arg("sTime").c_str());
char* ptr = strtok(str, ",");
byte i = 0, j = 0;
while (ptr != NULL) {
strcpy (switchTime[i++], ptr);
ptr = strtok(NULL, ",");
}
if (server.arg("sDay")) {
i = 0;
strcpy (str, server.arg("sDay").c_str());
char* ptr = strtok(str, ",");
while (ptr != NULL) {
wday[i++] = atoi(ptr);
ptr = strtok(NULL, ",");
}
toSave();
}
else {
server.send(400, "");
}
}
String temp = "[";
for (auto i = 0; i < COUNT * 2; i++) {
if (temp != "[") temp += ',';
temp += (String)"\"" + switchTime[i] + "\"";
}
temp += ",\"";
for (auto i = 0; i < COUNT; i++) {
temp += switchActive[i];
}
for (auto& elem : 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] == ACTIVE) + "\",\"" + static_cast<String>(relState[1] == ACTIVE) + "\",\"" + localTime() + "\"]");
});
}
void toSave() {
File file = LittleFS.open(existFolder("Config") + "/dtime.dat", "w");
if (file) {
file.write(switchActive, sizeof(switchActive));
file.write(wday, sizeof(wday));
for (auto i = 0; i < COUNT * 2; i++) {
file.printf("\n%s", switchTime[i]);
}
file.close();
}
}
void dualTimerSwitch() {
static uint8_t lastmin {60}, lastState[] {ACTIVE, ACTIVE};
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 (switchActive[i / 2] && !strcmp(switchTime[i], buf)) {
if (wday[i / 2] & (1 << (tm.tm_wday ? tm.tm_wday - 1 : 6))) {
i < (COUNT % 2 ? COUNT + 1 : COUNT) ? ({relState[0] = i % 2 ? !ACTIVE : ACTIVE; timeDataLogger(0, "Programm");}) : ({relState[1] = i % 2 ? !ACTIVE : ACTIVE; 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];
}
sonoffRelay_1();
sonoffRelay_2();
}
}
LittleFS.ino
// ****************************************************************
// Sketch Esp8266 Filesystem Manager spezifisch sortiert Modular(Tab)
// created: Jens Fleischer, 2020-06-08
// last mod: Jens Fleischer, 2020-10-31
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.7.0 - 2.7.4
// Geprüft: von 1MB bis 2MB Flash
// Getestet auf: Nodemcu
/******************************************************************
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([]() {
if (!handleFile(server.urlDecode(server.uri())))
server.send(404, "text/plain", "FileNotFound");
});
}
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, int>;
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());
}
if (!ran) dirList.emplace_back(dir.fileName(), "", 0);
}
else {
dirList.emplace_back("", dir.fileName(), dir.fileSize());
}
}
dirList.sort([](const records & f, const records & l) { // Dateien sortieren
if (server.arg(0) == "1") {
return get<2>(f) > get<2>(l);
} else {
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)) + "\"}";
}
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 (server.hasArg("new")) {
String folderName {server.arg("new")};
for (auto& c : {34, 37, 38, 47, 58, 59, 92}) for (auto& e : folderName) if (e == c) e = 95; // Ersetzen der nicht erlaubten Zeichen
LittleFS.mkdir(folderName);
}
if (server.hasArg("sort")) return handleList();
if (server.hasArg("delete")) {
deleteRecursive(server.arg("delete"));
sendResponce();
return true;
}
if (!LittleFS.exists("fs.html")) server.send(200, "text/html", LittleFS.begin() ? HELPER : WARNING); // ermöglicht das hochladen der fs.html
if (path.endsWith("/")) path += "index.html";
return LittleFS.exists(path) ? ({File f = LittleFS.open(path, "r"); server.streamFile(f, mime::getContentType(path)); f.close(); true;}) : 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();
}
void sendResponce() {
server.sendHeader("Location", "fs.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";
}
String existFolder( String const& foldername) {
if (!LittleFS.exists(foldername)) LittleFS.mkdir(foldername);
return foldername;
}
Lokalzeit.ino
// ****************************************************************
// Sketch Esp8266 SNTP Lokalzeit Modular(Tab)
// created: Jens Fleischer, 2020-10-10
// last mod: Jens Fleischer, 2020-12-26
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.7.2 - 2.7.4
// 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> oder #include <ESP8266WiFi.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.
/**************************************************************************************/
constexpr uint32_t SYNC_INTERVAL = 12; // NTP Sync Interval in Stunden einstellen
const char* const PROGMEM MONTH_SHORT[] = {"Jan", "Feb", "Mrz", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"};
void setupTime() { // Es ist kein NTP Server erforderlich wenn der Timestamp vom DHCP-Server(Fritz Box) geholt werden kann.
setTZ("CET-1CEST,M3.5.0/02,M10.5.0/03"); // Zeitzone einstellen https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
}
uint32_t sntp_update_delay_MS_rfc_not_less_than_15000() { // SNTP Update Intervall einstellen
return SYNC_INTERVAL * 36e5;
}
char* localTime() {
static char buf[9]; // je nach Format von "strftime" eventuell anpassen
static time_t previous;
time_t now = time(&now);
if (now != previous) {
previous = now;
localtime_r(&now, &tm);
strftime (buf, sizeof(buf), "%T", &tm); // http://www.cplusplus.com/reference/ctime/strftime/
}
return buf;
}
SonoffDuo.ino
// ****************************************************************
// Sketch Esp8266 Sonoff Dual Modular(Tab)
// created: Jens Fleischer, 2018-06-15
// last mod: Jens Fleischer, 2020-12-26
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Sonoff Dual
// Software: Esp8266 Arduino Core 2.7.2 - 2.7.4
// Getestet auf: 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 Sonoff Dual sollte als Tab eingebunden werden.
// #include <ESP8266WebServer.h> muss im Haupttab aufgerufen werden.
// Die Funktionalität des ESP8266 Webservers ist erforderlich.
// Die Funktion "sonoffDual();" muss im Setup aufgerufen werden.
/**************************************************************************************/
void sonoffDual() {
server.on("/status", []() { // sendet den Schaltzustand an die Webseite
server.enableCORS(true);
server.send(200, "application/json", "[\"" + static_cast<String>(relState[0]) + "\",\"" + static_cast<String>(relState[1]) + "\"]");
});
server.on("/hand10W", []() { // Taster vom Sketch Heizung
relState[0] = !relState[0];
timeDataLogger(0, server.client().remoteIP().toString()); // Funktionsaufruf Zeitdatenlogger
server.send(200, "text/plain", "10W OK Heizung");
});
server.on("/hand30W", []() { // Taster vom Sketch Heizung
relState[1] = !relState[1];
timeDataLogger(1, server.client().remoteIP().toString()); // Funktionsaufruf Zeitdatenlogger
server.send(200, "text/plain", "30W OK Heizung");
});
ArduinoOTA.onStart([]() {
save(); // Betriebstunden(Minuten) speichern
Serial.write(0xA0);
Serial.write(0x04);
Serial.write(0x00); // beide Relais ausschalten
Serial.write(0xA1);
Serial.write("\n");
Serial.flush();
});
}
void sonoffRelay_1() {
Serial.write(0xA0);
Serial.write(0x04);
if (relState[0]) {
relState[1] ? Serial.write(0x03) : Serial.write(0x01);
}
else {
relState[1] ? Serial.write(0x06) : Serial.write(0x00);
}
Serial.write(0xA1);
Serial.write("\n");
Serial.flush();
}
void sonoffRelay_2() {
Serial.write(0xA0);
Serial.write(0x04);
if (relState[1]) {
relState[0] ? Serial.write(0x03) : Serial.write(0x02);
}
else {
relState[0] ? Serial.write(0x05) : Serial.write(0x00);
}
Serial.write(0xA1);
Serial.write("\n");
Serial.flush();
}
Stundenzaehler.ino
// ****************************************************************
// Sketch Esp8266 Betriebsstundenzähler Dual Modular(Tab)
// created: Jens Fleischer, 2018-11-25
// last mod: Jens Fleischer, 2020-12-21
// 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 <LittleFS.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 = LittleFS.open(existFolder("Config") + "/hobbs.json", "r");
if (file) {
char buf[file.size()];
file.readBytes(buf, sizeof buf);
lastmin[0] = totalmin[0] = atoi(strtok(buf, "{\":runtimeOne"));
lastmin[1] = totalmin[1] = atoi(strtok(NULL, "\":,runtimeTwo"));
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 == ACTIVE) totalmin[0]++; // Betriebstundenzähler Relais 1 wird um eine Minute erhöht
if (state_1 == ACTIVE) 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[9];
snprintf(buf, sizeof(buf), "%d,%d", tmin / 60, tmin / 6 % 10);
return buf;
}
void save() {
File file = LittleFS.open(existFolder("Config") + "/hobbs.json", "w"); // Betriebstunden(minuten) speichern
if (file) {
file.printf(R"({"runtimeOne":"%u","runtimeTwo":"%u"})", 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-12-21
// 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 <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 (relState[num] != lastrelState[num]) { // Prüft ob sich der Relais Status geändert hat.
char fileName[17];
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 f = LittleFS.open(existFolder("Data") + fileName, "a");
if (f) {
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_SHORT[(tm.tm_mon + 1) % 12]);
LittleFS.remove(path); // Löscht die elf Monate alte Logdatei.
}
File f = LittleFS.open(existFolder("Data") + fileName, "a"); // Die Ereignisdaten für den aktuellen Monat speichern
if (f) {
relState[num] == ACTIVE ? 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>
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['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['HeapFrag'] + "%";
span[21].innerHTML = obj['ChipSize'];
span[22].innerHTML = obj['ChipSpeed'] + " MHz";
span[23].innerHTML = obj['ChipMode'];
span[24].innerHTML = obj['IdeVersion'].replace(/(\d)(\d)(\d)(\d)/,obj['IdeVersion'][3]!=0 ? '$1.$3.$4' : '$1.$3.');
span[25].innerHTML = obj['CoreVersion'].replace(/_/g,'.');
span[26].innerHTML = obj['SdkVersion'];
span[27].innerHTML = obj['HourMeter1'] + " h";
span[28].innerHTML = obj['HourMeter2'] + " h";
} 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>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>Heap Fragmentation:</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>
<span>0</span>
<div id="add">
<span>0</span>
<span>0</span>
</div>
</aside>
</main>
<div>
<button>Filesystem</button>
<button>Home</button>
</div>
<div id="note"></div>
<div>
<form>
<input placeholder="new 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">Submit</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(`Wirklich formatieren? Alle Daten gehen verloren.\nDu musst anschließend fs.html wieder laden.`)) event.preventDefault()});
});
async function list(by, to = '/'){
let resp = await fetch(`?sort=${by}`);
let json = await resp.json();
let myList = document.querySelector('main'), noted = '';
myList.innerHTML = '<nav><input type="radio" id="/" name="group"><label for="/"> 📁</label><span id="cr">+📁</nav></span><span id="si"></span>';
document.querySelector('form').setAttribute('action', `/upload?f=${to}`);
for (var i = 0; i < json.length - 1; i++) {
let dir = '', f = json[i].folder, n = json[i].name;
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 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">${by ? '▼' : '▲'} LittleFS</b> 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(by = ++by % 2, to);
localStorage.setItem('sortBy', JSON.stringify(by));
});
fs.addEventListener('change', e => {
for (var bytes = 0, i = 0; i < e.target.files.length; i++) bytes += e.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');
}
});
let node = document.querySelectorAll('main input');
node.forEach(n => { if (n.id === to) n.setAttribute('checked', 'checked')});
node.forEach(n => {
n.addEventListener('change', e => {
if (e.target.checked) {
to = e.target.id
document.querySelector('form').setAttribute('action', `/upload?f=${to}`);
}
});
});
document.querySelectorAll('[href^="?delete=/"]').forEach(node => { node.addEventListener('click', () => { if (!confirm('Sicher!')) event.preventDefault()})});
document.querySelectorAll('main input').forEach(n => { if (n.id === to) n.setAttribute('checked', 'checked')});
}
</script>
</head>
<body>
<h2>ESP8266 Filesystem Manager</h2>
<form method="post" enctype="multipart/form-data">
<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>
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 = 12; <!-- 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">⏳ LED 1</button>
<button id="tab2">⏳ LED 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">
10W
<svg viewBox="0 0 486 486">
<use id="body0" href="#bulb"/>
<use id="on0" href="#beam"/>
</svg>
</div>
<div id="tog1">
30W
<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>
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';
}