Update: 2020-08-04

Holzheizung SL 18K mit Pufferspeicher und Esp8266 Nodemcu Komforterweiterung

Dieser Sketch funktioniert ist aber in Bearbeitung.

Download Projekt

Heizung_3.1.ino

// ****************************************************************
// Sketch Esp8266 Heizung Modular(Tab)
// created: Jens Fleischer, 2020-02-11
// last mod: Jens Fleischer, 2020-08-03
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.7.0 - 2.7.4
// 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.
*******************************************************************/
// Der WebServer Tab ist der Haupt Tab mit "setup" und "loop".
// #include <FS.h> #include <ESP8266WebServer.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP8266 Webservers ist erforderlich.
// "server.onNotFound()" darf nicht im Setup des ESP8266 Webserver stehen.
// Inklusive Arduino OTA-Updates (Erfordert 512K Spiffs)
/**************************************************************************************/

#include <ESP8266WebServer.h>
#include <ArduinoOTA.h>
#include <FS.h>
#include <DallasTemperature.h>    //https://github.com/milesburton/Arduino-Temperature-Control-Library Version 3.8.0

ESP8266WebServer server(80);

const uint8_t OPERAND {128};
const uint16_t BRIGHTNESS = 855;
bool minmax {true};                     // Merker für Min oder Max // Beim neu aufspielen minmax einstellen. minmax Speicher leer true - Speicher voll false
bool pushStatusAddWood {false};         // Status für Push Nachricht Holz nachlegen
bool releaseAddWood {false};            // Globale Freigabe Holz nachlegen
bool testpushStatusHeated;

//#define DEBUGGING                       // Einkommentieren für die Serielle Ausgabe

#ifdef DEBUGGING
#define DEBUG_B(...) Serial.begin(__VA_ARGS__)
#define DEBUG_P(...) Serial.println(__VA_ARGS__)
#define DEBUG_F(...) Serial.printf(__VA_ARGS__)
#else
#define DEBUG_B(...)
#define DEBUG_P(...)
#define DEBUG_F(...)
#endif

struct Ds18b20 {
  DeviceAddress address;
  int16_t currentMin;
  int16_t currentMax;
  int16_t alwaysMin;
  int16_t alwaysMax;
  int16_t current;
  char minTime[9];               // Zeitpunkt für minimale Temperaturen
  char maxTime[9];               // Zeitpunkt für maximale Temperaturen
  char alwaysMinTime[20];        // Zeitpunkt für Langzeit Min Temperaturen
  char alwaysMaxTime[20];        // Zeitpunkt für Langzeit Max Temperaturen
};

struct Collection {
  Ds18b20 KES;            // Heizkessel
  Ds18b20 HVL;            // Haus Vorlauf
  Ds18b20 HRL;            // Haus Rücklauf
  Ds18b20 SWW;            // Pufferspeicher Warmwasser
  Ds18b20 SVL;            // Pufferspeicher Vorlauf
  Ds18b20 SRL;            // Pufferspeicher Rücklauf
  Ds18b20 ATE;            // Aussentemperatur
};

struct Collection sensors { {{0x28, 0xFF, 0x2E, 0x17, 0x81, 0x16, 0x04, 0x42}, INT16_MAX, INT16_MIN, INT16_MAX, INT16_MIN, 0, "", "", "", ""},
  {{0x28, 0xFF, 0xEA, 0x8C, 0x30, 0x17, 0x04, 0xD9}, INT16_MAX, INT16_MIN, INT16_MAX, INT16_MIN, 0, "", "", "", ""},
  {{0x28, 0xff, 0xd7, 0xfb, 0x24, 0x17, 0x03, 0xc8}, INT16_MAX, INT16_MIN, INT16_MAX, INT16_MIN, 0, "", "", "", ""},
  {{0x28, 0xFF, 0x29, 0x17, 0x81, 0x16, 0x04, 0x13}, INT16_MAX, INT16_MIN, INT16_MAX, INT16_MIN, 0, "", "", "", ""},
  {{0x28, 0xFF, 0x03, 0x18, 0x81, 0x16, 0x04, 0xCC}, INT16_MAX, INT16_MIN, INT16_MAX, INT16_MIN, 0, "", "", "", ""},
  {{0x28, 0xFF, 0xF6, 0x1E, 0x81, 0x16, 0x04, 0xB1}, INT16_MAX, INT16_MIN, INT16_MAX, INT16_MIN, 0, "", "", "", ""},
  {{0x28, 0xff, 0xa3, 0xdc, 0x24, 0x17, 0x03, 0x4c}, INT16_MAX, INT16_MIN, INT16_MAX, INT16_MIN, 0, "", "", "", ""},
};

struct timevalues {
  char minmaxTime[9];
  char fileName[15];
  char timeOfDay[9];
  char timeStamp[20];
  char mixing[25];
} lt;

void toSave(Collection &device);
void toRead(Collection &device);

String sketchName() {                             // Dateiname für den Admin Tab
  char file[sizeof(__FILE__)] = __FILE__;
  char * pos = strchr(file, '.'); *pos = '\0';
  return file;
}

void setup() {
  DEBUG_B(115200);
  delay(100);
  DEBUG_F("\n\nSketchname: %s\nBuild: %s\t\tIDE: %d.%d.%d\n%s\n\n",
          (__FILE__), (__TIMESTAMP__), ARDUINO / 10000, ARDUINO % 10000 / 100, ARDUINO % 100 / 10 ? ARDUINO % 100 : ARDUINO % 10, ESP.getFullVersion().c_str());
  ArduinoOTA.begin();
  spiffs();
  admin();
  wifiConnect();
  initLcd();
  setupTime();
  tempsetup();
  setupHtml();
  setupButton();
  delay(250);
  localTime();
  toRead();

  server.begin();
  ArduinoOTA.onStart([]() {
    toSave();                              // Min/Max Werte speichern bevor OTA Update
  });
}

void loop() {
  ArduinoOTA.handle();
  server.handleClient();
  if (millis() < 0x2FFF || millis() > 0xFFFFF0FF) runtime();
  static bool once {0};
  if (millis() > 54e4 && !once++) wifiConnect();          // Wifi Verbindung einmalig nach 9 Minuten neu starten für Stromausfall
  localTime();
  static uint32_t previousMillis[] {0, 0};
  uint32_t currentMillis {millis()};
  if (currentMillis - previousMillis[0] >= 250) {
    getTemp();
    previousMillis[0] = currentMillis;
  }
  currentMillis = millis();
  if (currentMillis - previousMillis[1] >= 100) {
    process();
    previousMillis[1] = currentMillis;
  }
  getButton();
  thingspeak();
}

Admin.ino

// ****************************************************************
// Sketch Esp8266 Admin Modular(Tab)
// created: Jens Fleischer, 2020-01-26
// last mod: Jens Fleischer, 2020-05-02
// 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 <FS.h> #include <ESP8266WebServer.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP8266 Webservers ist erforderlich.
// Die Spiffs.ino muss im ESP8266 Webserver enthalten sein
// Funktion "admin();" muss im setup() nach spiffs() aber vor dem Verbindungsaufbau aufgerufen werden.
// Die Funktion "runtime();" muss mindestens zweimal innerhalb 49 Tage aufgerufen werden.
// Entweder durch den Client(Webseite) oder zur Sicherheit im "loop();"
// IDE unter Werkzeuge auf lwip Variant: ;"v2 IPv6 Lower Memory;"
/**************************************************************************************/

#include <AddrList.h>

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

void admin() {                          // Funktionsaufruf "admin();" muss im Setup eingebunden werden
  File file = SPIFFS.open("/config.json", "r");
  if (file) {
    String newhostname = file.readStringUntil('\n');
    if (newhostname != "") {
      WiFi.hostname(newhostname.substring(1, newhostname.length() - 1));
      file.close();
      ArduinoOTA.setHostname(WiFi.hostname().c_str());
    }
  }
  server.on("/admin/renew", handlerenew);
  server.on("/admin/once", handleonce);
  server.on("/reconnect", []() {
    server.send(304, "message/http");
    WiFi.reconnect();
  });
  server.on("/restart", []() {
    server.send(304, "message/http");
    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() + "\"]");     // Json als Array
  }
*/
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 = SPIFFS.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 Überlauf
  previousMillis = currentMillis;
  uint32_t sec {(0xFFFFFFFF / 1000) * rolloverCounter + (currentMillis / 1000)};
  char buf[20];
  snprintf(buf, sizeof(buf), "%*.d %.*s %02d:%02d:%02d", sec < 86400 ? 0 : 1, sec / 86400, sec < 86400 ? 0 : sec >= 172800 ? 4 : 3, "Tage", sec / 3600 % 24, sec / 60 % 60, sec % 60);
  return buf;
}

Connect.ino

// ****************************************************************
// Sketch Esp8266 Connect Modular(Tab) mit optischer Anzeige
// created: Jens Fleischer, 2018-04-08
// last mod: Jens Fleischer, 2019-04-14
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.4.2 / 2.5.2 / 2.6.3 / 2.7.4
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Dual
/******************************************************************
  Copyright (c) 2018 Jens Fleischer. All rights reserved.

  This file is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.
  This file is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Connect sollte als Tab eingebunden werden.
// #include <ESP8266WebServer.h> oder #include <ESP8266WiFi.h> muss im Haupttab aufgerufen werden
// Die Funktion "Connect();" muss im Setup eingebunden werden.
/**************************************************************************************/

//#define CONFIG           // Einkommentieren wenn der ESP dem Router die IP mitteilen soll.
#define NO_SLEEP           // Auskommentieren wenn der Nodemcu den deep sleep Modus nutzt.

  const char* ssid = "Netzwerkname";             // << kann bis zu 32 Zeichen haben
  const char* password = "PasswortvomNetzwerk";  // << mindestens 8 Zeichen jedoch nicht länger als 64 Zeichen

#ifdef CONFIG
IPAddress staticIP(192, 168, 178, 99);      // statische IP des NodeMCU ESP8266
IPAddress gateway(192, 168, 178, 1);        // IP-Adresse des Router
IPAddress subnet(255, 255, 255, 0);         // Subnetzmaske des Netzwerkes
IPAddress dns(192, 168, 178, 1);            // DNS Server
#endif

void wifiConnect() {        // Funktionsaufruf "Connect();" muss im Setup eingebunden werden
  byte i = 0;
  //WiFi.disconnect();      // nur erforderlich wenn Esp den AP Modus nicht verlassen will
  WiFi.persistent(false);   // auskommentieren wenn Netzwerkname oder Passwort in den Flash geschrieben werden sollen
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
#ifdef CONFIG
  WiFi.config(staticIP, gateway, subnet, dns);
#endif
  while (WiFi.status() != WL_CONNECTED) {
#ifdef NO_SLEEP
    pinMode(LED_BUILTIN, OUTPUT);     // OnBoardLed Nodemcu, Wemos D1 Mini Pro
    digitalWrite(LED_BUILTIN, 0);
#endif
    delay(500);
    digitalWrite(LED_BUILTIN, 1);
    delay(500);
    DEBUG_F(" %d sek\n", i);
    if (++i > 9) {
      DEBUG_P("\nVerbindung zum AP fehlgeschlagen !\n\n");
      ESP.restart();
    }
  }
  DEBUG_P("\nVerbunden mit: " + WiFi.SSID());
  DEBUG_P("Esp8266 IP: " + WiFi.localIP().toString());
}

Debug.ino

void printDevice(Ds18b20 &device) {
  DEBUG_F("Akt: %.2f\n", static_cast<float>(device.current) / OPERAND);
  DEBUG_F("Min: %.2f\n", static_cast<float>(device.alwaysMin) / OPERAND);
  DEBUG_F("Min: %s\n", device.alwaysMinTime);
  DEBUG_F("Max: %.2f\n", static_cast<float>(device.alwaysMax) / OPERAND);
  DEBUG_F("Max: %s\n", device.alwaysMaxTime);
}

void debug() {
  DEBUG_P("KES");
  printDevice(sensors.KES);
  DEBUG_P("HVL");
  printDevice(sensors.HVL);
  DEBUG_P("HRL");
  printDevice(sensors.HRL);
  DEBUG_P("SWW");
  printDevice(sensors.SWW);
  DEBUG_P("SVL");
  printDevice(sensors.SVL);
  DEBUG_P("SRL");
  printDevice(sensors.SRL);
  DEBUG_P("ATE");
  printDevice(sensors.ATE);
}

Html.ino

void setupHtml() {                     // HTML Startseite
  server.on("/heater/current", handleCurrent);
  server.on("/heater/detail", handledetail);
  server.on("/led", led);
  server.on("/test", test);
}

String buildJson(const char* name, Ds18b20 &device) {
  String temp = "\"],\"";
  temp += name;
  temp += "\":[\"";
  temp += device.minTime;
  temp += "\",\"";
  temp += device.maxTime;
  temp += "\",\"";
  temp += device.alwaysMinTime;
  temp += "\",\"";
  temp += device.alwaysMaxTime;
  temp += "\",\"";
  temp += device.currentMin;
  temp += "\",\"";
  temp += device.currentMax;
  temp += "\",\"";
  temp += device.alwaysMin;
  temp += "\",\"";
  temp += device.alwaysMax;
  return temp;
}

void handledetail() {
  String temp = "{\"status\":[\"";
  temp += minmax;
  temp += "\",\"";
  temp += lt.minmaxTime;
  temp += "\",\"";
  temp += pushStatusAddWood;
  temp += "\",\"";
  temp += digitalRead(LED_BUILTIN);
  temp += "\",\"";
  temp += lt.mixing;
  temp += buildJson((char*)"KES", sensors.KES);
  temp += buildJson((char*)"HVL", sensors.HVL);
  temp += buildJson((char*)"HRL", sensors.HRL);
  temp += buildJson((char*)"SWW", sensors.SWW);
  temp += buildJson((char*)"SVL", sensors.SVL);
  temp += buildJson((char*)"SRL", sensors.SRL);
  temp += buildJson((char*)"ATE", sensors.ATE);
  temp += "\"]}";
  DEBUG_P("Json: " + temp);
  serverResponse(temp);
}

void handleCurrent() {
  String temp = "{\"SWW\":\"";
  temp += sensors.SWW.current;
  temp += "\", \"SVL\":\"";
  temp += sensors.SVL.current;
  temp += "\", \"SRL\":\"";
  temp += sensors.SRL.current;
  temp += "\", \"KES\":\"";
  temp += sensors.KES.current;
  temp += "\", \"HVL\":\"";
  temp += sensors.HVL.current;
  temp += "\", \"HRL\":\"";
  temp += sensors.HRL.current;
  temp += "\"}";
  serverResponse(temp);
}

void test() {
  String temp = "{\"pushStatusHeated\":\"";
  temp += testpushStatusHeated;
  temp += "\", \"pushStatusAddWood\":\"";
  temp += pushStatusAddWood;
  temp += "\", \"minmax\":\"";
  temp += minmax;
  temp += "\", \"releaseAddWood\":\"";
  temp += releaseAddWood;
  temp += "\"}";
  serverResponse(temp);
}

void serverResponse(String &temp) {
  server.sendHeader("Cache-Control", "no-cache, no-store");
  server.sendHeader("Access-Control-Allow-Origin", "*");
  server.send(200, "application/json", temp);
}

void led() {
  digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));    // LED umschalten
  handledetail();
}

HttpClient.ino

// ****************************************************************
// Sketch HttpClient Modular(Tab)
// created: Jens Fleischer, 2018-06-29
// last mod: Jens Fleischer, 2019-12-29
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: ESP8266
// D3 = GPIO0  Anschluss Taster vom GPIO0 auf GND
// D4 = GPIO2  Anschluss Taster vom GPIO2 auf GND
// Software: Esp8266 Arduino Core 2.5.2 - 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 HttpClient sollte als Tab eingebunden werden.
// #include <ESP8266WebServer.h> oder #include <ESP8266WiFi.h> muss im Haupttab aufgerufen werden
// Zum schalten muss die Funktion "getbutton();" im loop(); aufgerufen werden.
/**************************************************************************************/

const uint8_t BUTTON[] {D4, D3};         // Pin für Taster einstellen

void setupButton() {
  for (auto &pin : BUTTON)  pinMode(pin, INPUT_PULLUP);
}
void getButton() {
  bool currentStatus[2] {0, 0};
  static bool previousStatus[2] {0, 0};
  static uint32_t debounceMillis;
  uint32_t currentMillis {millis()};
  if (currentMillis - debounceMillis >= 50) {       // 50 ms Taster Entprellzeit
    debounceMillis = currentMillis;
    for (auto i = 0; i < 2; i++) {
      currentStatus[i] = !digitalRead(BUTTON[i]);
      if (!currentStatus[i] && currentStatus[i] != previousStatus[i]) {
        httpClientLight(i);
      }
      previousStatus [i] = currentStatus[i];
    }
  }
}

#include <ESP8266HTTPClient.h>

void httpClientLight(const byte &message) {
  const char* URL {"http://192.168.178.38/timer?tog="};        // URL des ESP... für Aussenbeleuchtung
  if (WiFi.status() == WL_CONNECTED) {
    WiFiClient client;
    HTTPClient http;
    DEBUG_F("\nFunktion: %s meldet in Zeile: %d -> HttpClient Begin\n", __PRETTY_FUNCTION__, __LINE__);
    DEBUG_P((String)URL + message);
    http.begin(client, (String)URL + message);
    int16_t httpCode = http.GET();
    if (httpCode > 0) {
      DEBUG_P("HTTP / 1.1 " + (String)httpCode);
      if (httpCode == HTTP_CODE_OK) {
        String payload = http.getString();
        DEBUG_P("Antwort: " + payload);
        bool smallSize = payload.substring(2, payload.length()).toInt();
        bool largeSize = payload.substring(6, payload.length()).toInt();
        showStatusLight(smallSize, largeSize);
      }
    }
    else {
      DEBUG_P("Fehler: " + http.errorToString(httpCode));
    }
    http.end();
  }
}

LCD.ino

// ****************************************************************
// Sketch Esp8266 LCD Modular(Tab)
// created: Jens Fleischer, 2020-02-11
// last mod: Jens Fleischer, 2020-05-13
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266, LCD MODUL 4X20
// Software: Esp8266 Arduino Core 2.4.2 - 2.7.4
// 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.
*******************************************************************/

#include <LiquidCrystal_I2C.h>    // Version 1.1.3 https://github.com/marcoschwartz/LiquidCrystal_I2C.git

LiquidCrystal_I2C lcd(0x3F, 20, 4);

void initLcd() {
  lcd.init();
  lcd.backlight();
}

void showTime() {
  lcd.setCursor(12, 3);
  lcd.printf("%s", lt.timeOfDay);                          // Uhrzeit auf LCD ausgeben
}

void showTemperatures() {                  // aktuelle Temperaturen auf LCD ausgeben
  lcd.setCursor(0, 0);
  lcd.printf("WW %.2f%cC", static_cast<float>(sensors.SWW.current) / OPERAND, 0xDF);
  lcd.setCursor(0, 1);
  lcd.printf("VL %.2f%cC", static_cast<float>(sensors.SVL.current) / OPERAND, 0xDF);
  lcd.setCursor(0, 2);
  lcd.printf("RL %.2f%cC", static_cast<float>(sensors.SRL.current) / OPERAND, 0xDF);
  lcd.setCursor(0, 3);
  if (sensors.KES.current < 12800) {
    lcd.printf("KE %.2f%cC", static_cast<float>(sensors.KES.current) / OPERAND, 0xDF);
  }
  else {
    lcd.printf("KE %.1f%cC", static_cast<float>(sensors.KES.current) / OPERAND, 0xDF);
  }
  lcd.setCursor(11, 0);
  lcd.printf("HV %.1f%cC", static_cast<float>(sensors.HVL.current) / OPERAND, 0xDF);
  if (!pushStatusAddWood) {
    lcd.setCursor(11, 1);
    lcd.printf("HR %4.1f%cC", 1202.0 / OPERAND, 0xDF); // Konstanter Wert, Thermometer noch nicht angeschlossen
  }
  lcd.setCursor(11, 2);
  lcd.printf("AT %4.1f%cC", static_cast<float>(sensors.ATE.current) / OPERAND, 0xDF);
}

void showStatusAddWood(uint16_t &threshold) {
  static bool previousStatus {false};
  lcd.setCursor(11, 1);
  if (pushStatusAddWood) {
    lcd.printf("FN %.1f%c", static_cast<float>(threshold) / OPERAND, 0xDF); // Freigabe Nachlegen und Benarichtigungstemperatur auf LCD anzeigen
  }
  else {
    if (pushStatusAddWood != previousStatus) {
      lcd.printf("       ");                                                    // Ausgabe auf LCD löschen
    }
  }
  previousStatus = pushStatusAddWood;
}

//void showStatusAddWood(uint16_t &threshold) {
//  static bool previousStatus {false};
//  static bool previousThreshold;
//  if (pushStatusAddWood != previousStatus || threshold != previousThreshold) {
//    lcd.setCursor(11, 1);
//    if (pushStatusAddWood) {
//      lcd.printf("FN %.1f%c", static_cast<float>(threshold) / OPERAND, 0xDF); // Freigabe Nachlegen und Benarichtigungstemperatur auf LCD anzeigen
//    }
//    else {
//      lcd.printf("       ");                                                    // Ausgabe auf LCD löschen
//    }
//    previousStatus = pushStatusAddWood;
//    previousThreshold = threshold;
//  }
//}

void showStatusLight(bool &smallSize, bool &largeSize) {   // Anzeige Licht auf LCD ausgeben
  lcd.setCursor(10, 0);
  smallSize ? lcd.printf("%c", (char)255) : lcd.printf(" ") ;
  //  lcd.setCursor(11, 2);
  //  smallSize ? lcd.printf("10 ON") : lcd.printf("     ") ;
  DEBUG_P(smallSize ? "10 Watt an" : "10 Watt aus");
  lcd.setCursor(17, 2);
  largeSize ? lcd.printf("30 ON") : lcd.printf("     ") ;
  DEBUG_P(largeSize ? "30 Watt an" : "30 Watt aus");
}

Localtime.ino

// ****************************************************************
// Sketch Esp8266 Lokalzeit Modular(Tab)
// created: Jens Fleischer, 2018-07-10
// last mod: Jens Fleischer, 2019-05-04
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.6.0 - 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.
*******************************************************************/
// Diese Version von Lokalzeit sollte als Tab eingebunden werden.
// #include <ESP8266WebServer.h> oder #include <ESP8266WiFi.h> muss im Haupttab aufgerufen werden.
// Funktion "setupTime();" muss im setup() nach dem Verbindungsaufbau aufgerufen werden.
/**************************************************************************************/

#include <time.h>

struct tm tm;

const char* const PROGMEM ntpServer[] = {"fritz.box", "de.pool.ntp.org", "at.pool.ntp.org", "ch.pool.ntp.org", "ptbtime1.ptb.de", "europe.pool.ntp.org"};
const char* const PROGMEM dayNames[] = {"Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"};
const char* const PROGMEM dayShortNames[] = {"So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"};
const char* const PROGMEM monthNames[] = {"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"};
const char* const PROGMEM monthShortNames[] = {"Jan", "Feb", "Mrz", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"};

void setupTime() {
  configTime("CET-1CEST,M3.5.0/02,M10.5.0/03", ntpServer[0]);   ;   // Zeitzone einstellen https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
}                                                                   // deinen NTP Server einstellen (von 0 - 5 aus obiger Liste)

const char* fileName() {
  snprintf(lt.fileName, sizeof(lt.fileName), "/%s.dat", monthNames[tm.tm_mon]);
  return lt.fileName;
}

void localTime() {
  static time_t lastsec {0};
  time_t now = time(&now);
  localtime_r(&now, &tm);
  if (tm.tm_sec != lastsec) {
    lastsec = tm.tm_sec;
    strftime (lt.timeOfDay, sizeof(lt.timeOfDay), "%T", &tm);   // http://www.cplusplus.com/reference/ctime/strftime/
    strftime (lt.timeStamp, sizeof(lt.timeStamp), "%T %d.%m.%Y", &tm);   // http://www.cplusplus.com/reference/ctime/strftime/
    showTime();
    if (!(time(&now) % 86400)) {                       // einmal am Tag die Zeit vom NTP Server holen o. jede Stunde "% 3600" aller zwei "% 7200"
      setupTime();
    }
  }
}

Push.ino

// ****************************************************************
// Sketch Esp8266 PushBullet Modular(Tab)
// created: Jens Fleischer, 2018-06-26
// last mod: Jens Fleischer, 2020-05-02
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.7.0 - 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.
*******************************************************************/
// Diese Version von PushBullet sollte als Tab eingebunden werden.
// #include <ESP8266WebServer.h> oder #include <ESP8266WiFi.h> muss im Haupttab aufgerufen werden
// Die Übergabe der Nachricht an "pushbullet(string oder String-Object);" erfolgt als char oder String.
// Die Funktion "Connect();" muss im Setup eingebunden sein.
/**************************************************************************************/

#include <WiFiClientSecure.h>     // für PushBullet

WiFiClientSecure secureClient;

bool pushbullet(const char* message) {                                 // Push Nachricht senden
  const char* PushBulletAPIKEY = {"Key vom Pushbullet-Konto"}          // Konto Schlüssel einfügen http://pushbullet.com
  const uint16_t timeout {1500};                                       // Zeit für Wartezeit auf Antwort in Millisekunden einstellen
  const char* HOST {"api.pushbullet.com"};
  uint32_t broadcastingTime {millis()};
  secureClient.setInsecure();
  if (!secureClient.connect(HOST, 443)) {
    DEBUG_P("Pushbullet Verbindung fehlgeschlagen !");
    return false;
  }
  else {
    String messagebody = "{\"type\": \"note\", \"title\": \"Push vom ESP\", \"body\": \"" + (String)message + " " + lt.timeStamp + "\"}";
    secureClient.printf(PSTR("POST /v2/pushes HTTP/1.1\r\nHost: %s\r\nAuthorization: Bearer %s\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n%s\r\n"),
                        HOST, PushBulletAPIKEY, messagebody.length(), messagebody.c_str());
    DEBUG_P("Message gesendet");
  }

  while (!secureClient.available()) {
    if (millis() - broadcastingTime > timeout) {
      DEBUG_P("Pushbullet Client Timeout !");
      secureClient.stop();
      return false;
    }
  }

  while (secureClient.available()) {               //Emfängt Antwort
    DEBUG_F("Pushbullet Antwort nach: %4ld ms\n", millis() - broadcastingTime); // zeigt die Zeit bis zur Antwort --> passe den Timeout entsprechend an
    String line = secureClient.readStringUntil('\n');
    DEBUG_F("Funktion: %s meldet in Zeile: %d Responce: %s\n", __PRETTY_FUNCTION__, __LINE__, line.c_str());
    if (line.startsWith("HTTP/1.1 200 OK")) {
      secureClient.stop();
      return true;
    }
  }
  return false;
}

Spiffs.ino

// ****************************************************************
// Sketch Esp8266 Dateiverwaltung spezifisch Sortiert Modular(Tab)
// created: Jens Fleischer, 2020-08-03
// last mod: Jens Fleischer, 2020-08-03
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.7.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 Spiffs sollte als Tab eingebunden werden.
// #include <FS.h> #include <ESP8266WebServer.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP8266 Webservers ist erforderlich.
// "server.onNotFound()" darf nicht im Setup des ESP8266 Webserver stehen.
// Die Funktion "spiffs();" muss im Setup aufgerufen werden.
/**************************************************************************************/

#include <list>

const char WARNING[] PROGMEM = R"(<h2>Der Sketch wurde mit "no SPIFFS" kompilliert!)";
const char HELPER[] PROGMEM = R"(<form method="POST" action="/upload" enctype="multipart/form-data">
<input type="file" name="[]" multiple><button>Upload</button></form>Lade die spiffs.html hoch.)";

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

void handleList() {                           // Senden aller Daten an den Client
  FSInfo fs_info;  SPIFFS.info(fs_info);      // Füllt FSInfo Struktur mit Informationen über das Dateisystem
  Dir dir = SPIFFS.openDir("/");              // Auflistung aller im Spiffs vorhandenen Dateien
  typedef std::pair<String, int> prop;
  std::list<prop> dirList;                                                               // Liste anlegen
  while (dir.next()) dirList.emplace_back(dir.fileName().substring(1), dir.fileSize());  // Liste füllen
  dirList.sort([](const prop & f, const prop & l) {                                      // Liste sortieren
    if (server.arg(0) == "1") {
      return f.second > l.second;
    } else {
      for (uint8_t i = 0; i < 30; i++) {
        if (tolower(f.first[i]) < tolower(l.first[i])) return true;
        else if (tolower(f.first[i]) > tolower(l.first[i])) return false;
      }
      return false;
    }
  });
  String temp = "[";
  for (auto& p : dirList) {
    if (temp != "[") temp += ',';
    temp += "{\"name\":\"" + p.first + "\",\"size\":\"" + formatBytes(p.second) + "\"}";
  }
  temp += ",{\"usedBytes\":\"" + formatBytes(fs_info.usedBytes * 1.05) + "\"," +             // Berechnet den verwendeten Speicherplatz + 5% Sicherheitsaufschlag
          "\"totalBytes\":\"" + formatBytes(fs_info.totalBytes) + "\",\"freeBytes\":\"" +    // Zeigt die Größe des Speichers
          (fs_info.totalBytes - (fs_info.usedBytes * 1.05)) + "\"}]";                        // Berechnet den freien Speicherplatz + 5% Sicherheitsaufschlag
  server.send(200, "application/json", temp);
}

bool handleFile(String&& path) {
  if (server.hasArg("delete")) {
    SPIFFS.remove(server.arg("delete"));                   // Datei löschen
    sendResponce();
    return true;
  }
  if (!SPIFFS.exists("/spiffs.html"))server.send(200, "text/html", SPIFFS.begin() ? HELPER : WARNING);     // ermöglicht das hochladen der spiffs.html
  if (path.endsWith("/")) path += "index.html";
  return SPIFFS.exists(path) ? ({File f = SPIFFS.open(path, "r"); server.streamFile(f, mime::getContentType(path)); f.close(); true;}) : false;
}

void handleUpload() {                                      // Dateien ins SPIFFS schreiben
  static File fsUploadFile;                                // enthält den aktuellen Upload
  HTTPUpload& upload = server.upload();
  if (upload.status == UPLOAD_FILE_START) {
    if (upload.filename.length() > 30) {
      upload.filename = upload.filename.substring(upload.filename.length() - 30, upload.filename.length());  // Dateinamen auf 30 Zeichen kürzen
    }
    printf(PSTR("handleFileUpload Name: /%s\n"), upload.filename.c_str());
    fsUploadFile = SPIFFS.open("/" + 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 formatSpiffs() {                                      // Formatiert den Speicher
  SPIFFS.format();
  sendResponce();
}

void sendResponce() {
  server.sendHeader("Location", "spiffs.html");
  server.send(303, "message/http");
}

const String formatBytes(size_t const& bytes) {            // lesbare Anzeige der Speichergrößen
  return bytes < 1024 ? static_cast<String>(bytes) + " Byte" : bytes < 1048576 ? static_cast<String>(bytes / 1024.0) + " KB" : static_cast<String>(bytes / 1048576.0) + " MB";
}

bool freeSpace(uint16_t const& printsize) {                // Funktion um beim speichern in Logdateien zu prüfen ob noch genügend freier Platz verfügbar ist.
  FSInfo fs_info;   SPIFFS.info(fs_info);                  // Füllt FSInfo Struktur mit Informationen über das Dateisystem
  return (fs_info.totalBytes - (fs_info.usedBytes * 1.05) > printsize) ? true : false;
}

void lightDataRecorder() {
  static bool logState {false}, previouslogState {false};
  logState = analogRead(A0) < BRIGHTNESS ? false : true;
  if (logState != previouslogState) {
    DEBUG_P("====== LIGHT DATA LOGGING =======");
    File f = SPIFFS.open("/lightdata.txt", "a");
    if (f && freeSpace(100)) {
      logState ? f.printf("Licht an %s", lt.timeStamp) : f.printf(" aus %s\n", lt.timeOfDay);
    }
    f.close();
  }
  previouslogState = logState;
}

void archiveProcess(char record[]) {
  File f = SPIFFS.open("/ablauf.txt", "a");                        // Nur zum Debuggen
  if (f && freeSpace(250)) {
    f.printf("%s %s\n", lt.timeStamp, record);
    f.close();
  }
}

bool toRead() {                                                    // Einlesen aller Daten falls die Datei im Spiffs vorhanden ist.
  archiveProcess((char*)" READ DATA LOGGING Start");
  File f = SPIFFS.open(fileName(), "r");
  if (f && freeSpace(512)) {
    DEBUG_P("====== READ DATA LOGGING =======");
    f.read(reinterpret_cast<byte*>(&sensors), sizeof(sensors));    // Deserialisierung
    f.close();
    return true;
  }
  return false;
}

bool toSave() {
  archiveProcess((char*)" SAVE DATA LOGGING Start");
  File f = SPIFFS.open(fileName(), "w");                        // Datei zum schreiben öffnen
  if (f && freeSpace(535)) {
    DEBUG_P("====== SAVE DATA LOGGING =======");
    f.write(reinterpret_cast<byte*>(&sensors), sizeof(sensors));
    f.close();
    return true;
  }
  return false;
}

Temperatur.ino

// ****************************************************************
// Sketch Esp8266 Temperatur Modular(Tab)
// created: Jens Fleischer, 2020-02-11
// last mod: Jens Fleischer, 2020-04-08
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266, DS18b20
// Software: Esp8266 Arduino Core 2.4.2 - 2.7.4
// 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.
*******************************************************************/

const byte ONE_WIRE_PIN = D6;     // Pin für Ds18b20 einstellen

OneWire oneWire(ONE_WIRE_PIN);
DallasTemperature MaximDs18b20(&oneWire);

void tempsetup() {
  MaximDs18b20.begin();
  MaximDs18b20.setWaitForConversion(false);
  MaximDs18b20.requestTemperatures();
  server.on("/minmax", []() {
    minmax ? archiveProcess((char*)"Max return Button") : archiveProcess((char*)"Min return Button");
    minmax ? setMaxGroup() : setMinGroup();           // setzt MIN MAX Werte zurück
    minmax = !minmax;
    handledetail();
  });
  server.on("/statusAddWood", []() {                                        // sperrt die Push Nachricht zum Holz nachlegen
    pushStatusAddWood ? archiveProcess((char*)"Push nachlegen false Button") : archiveProcess((char*)" Push nachlegen true Button");
    pushStatusAddWood = !pushStatusAddWood;
    releaseAddWood = false;
    handledetail();
  });
}

void measure(Ds18b20 &device) {
  int16_t raw = MaximDs18b20.getTemp(device.address);
  if (raw != DEVICE_DISCONNECTED_RAW) {
    device.current = raw;
    if (device.current < device.currentMin) {                                  //Min Wert ermitteln
      device.currentMin = device.current;
      strcpy(device.minTime, lt.timeOfDay);
    }
    if (device.current > device.currentMax) {                                  //Max Wert ermitteln
      device.currentMax = device.current;
      strcpy(device.maxTime, lt.timeOfDay);
    }
    if (device.currentMin < device.alwaysMin) {                                //Langzeit Min Werte ermitteln
      device.alwaysMin = device.currentMin;
      strcpy(device.alwaysMinTime, lt.timeStamp);
    }
    if (device.currentMax > device.alwaysMax) {                                //Langzeit Max Werte ermitteln
      device.alwaysMax = device.currentMax;
      strcpy(device.alwaysMaxTime, lt.timeStamp);
    }
  }
  MaximDs18b20.requestTemperaturesByAddress(device.address);
}

void getTemp() {
  static uint8_t passCounter;
  switch (++passCounter) {
    case 1:
      measure(sensors.KES);
      break;
    case 2:
      measure(sensors.SWW);
      break;
    case 3:
      measure(sensors.SVL);
      break;
    case 4:
      measure(sensors.SRL);
      break;
    case 5:
      measure(sensors.HVL);
      break;
    case 6:
      measure(sensors.ATE);
      break;
    case 7:
      measure(sensors.HRL);
      break;
    default:
      showTemperatures();        // Temperatur auf LCD ausgeben
      //debug();
      passCounter = 0;
  }
}

void setMin(Ds18b20 &device) {                          // Min Werte zurücksetzen
  device.currentMin = device.current;
  strcpy(device.minTime, lt.timeOfDay);
}

void setMinGroup() {
  setMin(sensors.KES);
  setMin(sensors.HVL);
  setMin(sensors.HRL);
  setMin(sensors.SWW);
  setMin(sensors.SVL);
  setMin(sensors.SRL);
  strcpy(lt.minmaxTime, lt.timeOfDay);
}

void setMax(Ds18b20 &device) {                          // Max Werte zurücksetzen
  device.currentMax = device.current;
  strcpy(device.maxTime, lt.timeOfDay);
}

void setMaxGroup() {
  setMax(sensors.KES);
  setMax(sensors.HVL);
  setMax(sensors.HRL);
  setMax(sensors.SWW);
  setMax(sensors.SVL);
  setMax(sensors.SRL);
  strcpy(lt.minmaxTime, lt.timeOfDay);
}

/*-------- Erkennung ob angeheizt code ----------*/
void heated(bool &pushStatusHeated, uint16_t &klimaComp) {
  if (minmax && (sensors.KES.current > sensors.KES.currentMin + 5 * OPERAND - klimaComp)) {           //Erkennt das angeheitzt wurde //klimaCompensation -> 0 warm 1 kalt 2 sehr kalt
    setMaxGroup();                     //Max Werte zurücksetzen
    archiveProcess((char*)"Push anheizen false - Push nachlegen true - Ofen angeheizt - Max return");
    minmax = !minmax;
    pushStatusAddWood = true;                                                  //gibt Push Nachlegen frei wenn angeheizt wurde
    pushStatusHeated = true;                                                  // sperrt Push anheizen
  }
}

/*-------- Push Nachlegen Kessel code ----------*/
void addWood(uint16_t &threshold, uint16_t &klimaComp) {
  // Rücklauf unter 81°
  // Kessel fällt auf 75,5° + z Grad
  // Merken wenn Rücklauf über 81° freigabe löschen

  if (pushStatusAddWood && (sensors.SRL.current < 80 * OPERAND + klimaComp / 2)) { // klimaCompensation -> 0 warm 1 kalt 2 sehr kalt
    if (sensors.SRL.current < 61 * OPERAND) threshold = 9664;                                       // entspricht 75.5C
    else if (sensors.SRL.current < 70 * OPERAND) threshold = 9792;                                  // entspricht 76.5C
    else if (sensors.SRL.current < 74 * OPERAND) threshold = 9920;                                  // entspricht 77.5C
    else if (sensors.SRL.current < 76 * OPERAND) threshold = sensors.SRL.current + 512;             // entspricht SRL + 4C
    else if (sensors.SRL.current >= 76 * OPERAND) threshold = sensors.SRL.current + 640;            // entspricht SRL + 5C
    if (releaseAddWood && (sensors.KES.current <= threshold)) {
      releaseAddWood = !releaseAddWood;
      if (analogRead(A0) < BRIGHTNESS) {                                       //nur wenn Licht aus
        if (pushbullet("Holz nachlegen")) {                                    //Push Nachricht wenn Kesseltemperatur auf z gefallen
          File f = SPIFFS.open("/nachlegen.txt", "a");                         // Datei zum schreiben öffnen
          if (f && freeSpace(100)) {
            f.printf("%s SRL %.2f KES %.2f Z %.2f\n", lt.timeStamp,
                     static_cast<float>(sensors.SRL.current) / OPERAND, static_cast<float>(sensors.KES.current) / OPERAND, static_cast<float>(threshold) / OPERAND);
          }
          f.close();
          archiveProcess((char*)"Push nachlegen gesendet");
        }
        else {
          archiveProcess((char*)"Push nachlegen Senden fehlgeschlagen");
        }
      }
    }
    if (!releaseAddWood && (sensors.KES.current > threshold + 1 * OPERAND)) {
      releaseAddWood = !releaseAddWood;
    }
  }
  if ((releaseAddWood && (sensors.SRL.current >= 80 * OPERAND + klimaComp)) || (releaseAddWood && (sensors.SRL.current + 15 * OPERAND) < sensors.SRL.currentMax)) {        // sperrt Nachlegen bis zum nächsten Anheizen
    archiveProcess((char*)"Push nachlegen false");
    releaseAddWood = false;
    pushStatusAddWood = false;
  }
}

/*-------- Push Anheizen Kessel code ----------*/
void heatUp(bool &pushStatusHeated) {
  if (!pushStatusHeated && sensors.SVL.current >= sensors.SWW.current - 1 * OPERAND && sensors.SWW.current < 73 * OPERAND) {       // 17.12.18 von 2 auf 1
    if (analogRead(A0) < BRIGHTNESS && sensors.SRL.currentMax > (sensors.SRL.current + 15 * OPERAND)) {       //nur wenn Licht aus
      if (pushbullet("Anheizen")) {
        pushStatusHeated = !pushStatusHeated;                                 // wird von Ofen aus freigegeben
        //        File f = SPIFFS.open("/anheizen.txt", "a");                           // Datei zum schreiben öffnen
        //        if (f && freeSpace(400)) {
        //          f.printf("%s WW %s SVL %s Max SVL %s SRL %s Max SRL %s HVL %s\n", lt.timeStamp, ds[2].Temperature, ds[3].Temperature, ds[3].Tempmax, ds[0].Temperature, ds[0].Tempmax, ds[4].Temperature);
        //        }
        //        f.close();
        archiveProcess((char*)"Push Anheizen gesendet");
      }
      else {
        archiveProcess((char*)"Push Anheizen Senden fehlgeschlagen");
      }
    }
  }
}

/*-------- Erkennung Ofen aus code ----------*/
void heaterOff(bool &pushStatusHeated, uint16_t &klimaComp) {
  if (!minmax && sensors.SVL.currentMax > (sensors.SVL.current + 7 * OPERAND - klimaComp)) {       // klimaCompensation -> 0 warm 1 kalt 2 sehr kalt
    setMinGroup();                                                                   // Min Werte zurücksetzen
    archiveProcess((char*)"Push anheizen true - Ofen aus - Min return");
    minmax = !minmax;
    pushStatusHeated = false;                                                       // gibt Push anheizen frei
  }
}

/*-------- Abfrage Speicher gemischt Funktion ----------*/
void storageMixed() {
  static unsigned long startzeit1, startzeit2;
  static int16_t previousValue = INT16_MAX;
  static byte counter {0};
  static bool flag {true};                   // Hilfsmerker für Speicher mischen
  if (minmax) {                              // minmax (true)kommt von Ofen aus, wird durch angeheizt (false), gibt Speicher gemischt frei ist global
    if (flag) {                              // Abfrage Speicher gemischt
      if (millis() - startzeit1 >= 60000) {
        startzeit1 = millis();
        previousValue = sensors.SVL.current;
      }
      if (sensors.SVL.current > previousValue + OPERAND) {
        counter ++;
        flag = false;
        snprintf(lt.mixing, sizeof(lt.mixing), "%d x gemischt um %s", counter, lt.timeStamp);   // Mischen ist Global
        archiveProcess(lt.mixing);
      }
    }
    else {
      if (millis() - startzeit2 >= 240000) {
        startzeit2 = millis();
        previousValue = sensors.SVL.current;
      }
      if (sensors.SVL.current < previousValue - OPERAND / 2) {
        flag = true;
        previousValue = INT16_MAX;
      }
    }
  }
  else {                 // minmax (false)kommt von angeheizt, wird durch Ofen aus (true), gibt Speicher gemischt frei ist global
    counter = 0;
    lt.mixing[0] = {'\0'};       // Mischen ist Global
  }
}

void getKlima(uint16_t &klimaComp) {
  static uint8_t previousDay {UCHAR_MAX};
  if (tm.tm_hour == 2 && tm.tm_wday != previousDay) {
    previousDay = tm.tm_wday;
    switch (sensors.ATE.current) {
      case 15 ... 44:
        klimaComp = 0;
        break;
      case 0 ... 14:
        klimaComp = OPERAND;
        break;
      default:
        klimaComp = OPERAND * 2;
    }
    toSave();             // Min Max Werte speichern
  }
}


void process() {
  static bool pushStatusHeated {false};
  static uint16_t klimaComp {0};
  static uint16_t threshold;
  static uint8_t passCounter;
  testpushStatusHeated = pushStatusHeated;

  switch (++passCounter) {
    case 1:
      heatUp(pushStatusHeated);
      break;
    case 2:
      heated(pushStatusHeated, klimaComp);
      break;
    case 3:
      addWood(threshold, klimaComp);
      break;
    case 4:
      heaterOff(pushStatusHeated, klimaComp);
      break;
    case 5:
      storageMixed();
      break;
    case 6:
      showStatusAddWood(threshold);
      break;
    default:
      lightDataRecorder();
      getKlima(klimaComp);
      passCounter = 0;
  }
}

Thingspeak.ino

// ****************************************************************
// Sketch Thingspeak Modul(Tab) nicht blockierend
// created: Jens Fleischer, 2018-04-08
// last mod: Jens Fleischer, 2020-02-11
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.4.2 - 2.7.4
// Getestet auf: Nodemcu
/******************************************************************
  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.
*******************************************************************/

WiFiClient client;

bool thingspeak() {                                         // Funktionsaufruf "thingspeak();" muss in loop eingebunden sein
  static bool error {false}, response {true};
  const uint16_t HTTPPORT {80};
  const char* HOST {"184.106.153.149"};
  const char* TS_KEY = {"Geheimer ApiKey"};                 // trage deinen ApiKey von https://thingspeak.com
  const uint16_t TIMEOUT {1600};                            // Zeitraum für Fehlerrückgabe in Millisekunden einstellen
  const uint32_t INTERVAL {1000UL * 16};                    // Interval in Sekunden einstellen
  static unsigned long previousMillis {0};                  // Sendebeginn einstellen (" 0 - interval" = sofort, "0" = nach Ablauf von Interval)

  /*************** Thinkspeak senden *********************/

  uint32_t currentMillis {millis()};
  if (currentMillis - previousMillis >= INTERVAL || error) {       // senden im Interval und erneut im Fehlerfall
    error = false;                // oder bei erfolgreicher Antwort
    previousMillis = currentMillis;
    if (!client.connect(HOST, HTTPPORT)) {
      DEBUG_P("Thingspeak Verbindung fehlgeschlagen !");
      error = true;
      return false;
    }
    else {
      char buf[146];
      snprintf(buf, sizeof(buf), "&field1=%.2f&field2=%.2f&field3=%.2f&field4=%.2f&field5=%.2f&field6=%.2f&field7=%.2f&field8=%.2f", 
      static_cast<float>(sensors.SWW.current) / OPERAND, static_cast<float>(sensors.SVL.current) / OPERAND, static_cast<float>(sensors.KES.current) / OPERAND,
      static_cast<float>(sensors.SRL.current) / OPERAND, static_cast<float>(sensors.SWW.current) / OPERAND, static_cast<float>(sensors.SVL.current) / OPERAND,
      static_cast<float>(sensors.KES.current) / OPERAND, static_cast<float>(sensors.SRL.current) / OPERAND);
      DEBUG_P(buf);
      client.printf("POST /update?key=%s%s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", TS_KEY, buf, HOST);
      response = false;
      return true;
    }
  }

  /************** Thinkspeak auf Anwort prüfen *****************/

  if (!response) {
    if (millis() - previousMillis > TIMEOUT) {
      DEBUG_P("Thingspeak Client Timeout !");
      error = true;
      client.stop();
      return false;
    }
    if (client.available()) {               // einlesen der Antwort
      String line = client.readStringUntil('\n');
      line.trim();
      DEBUG_F("Funktion: %s meldet in Zeile: %d Responce: %s\n", __PRETTY_FUNCTION__, __LINE__, line.c_str());
      if (line == "HTTP/1.1 200 OK") {    // wenn Antwort mit "HTTP/1.1 200 OK" beginnt war das Senden erfolgreich
        client.stop();
        response = true;
      }
    }
  }
  return true;
}

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>
	  @media only screen and (max-width: 500px) {
	    .ip {
	      right: 6em;
		  position: relative;
	    }
	    aside {
	      max-width: 50vw;
	    }
	  }
	  </style>
      <script>
        window.addEventListener('load', () => {
		  renew(),once();
		  let output = document.querySelector('#note');
		  let button = document.querySelectorAll('button');
		  let span = document.querySelectorAll('#right span');
          button[0].addEventListener('click', () => {
            window.location = '/spiffs.html';
          });
          button[1].addEventListener('click', () => {
            window.location = '/';
          });
          button[2].addEventListener('click', check.bind(this, document.querySelector('input')));
		  button[3].addEventListener('click', re.bind(this, 'reconnect'));
		  button[4].addEventListener('click', () => {
            if (confirm('Bist du sicher!')) re('restart');
          });
          function once(arg1,arg2) {
            fetch('/admin/once', {
              method: 'POST',
              body: arg1
            }).then( (resp) => {
              return resp.json();
            }).then( (obj) => {
              output.innerHTML = '';
              output.classList.remove('note');
              document.querySelector('form').reset();
              if (arg1 == undefined) myIv = setInterval(renew, 1000);
              if (arg2 == 'reconnect') re(arg2);
			  span[3].innerHTML = obj['File'];
              span[4].innerHTML = obj['Build'];
              span[5].innerHTML = obj['SketchSize'];
              span[6].innerHTML = obj['SketchSpace'];
              span[7].innerHTML = obj['LocalIP'];
              span[8].innerHTML = obj['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['CoreVersion'].replace(/_/g,'.');
              span[28].innerHTML = obj['SdkVersion'];
            }).catch(function(err) {
              re();
            });
          }
          function renew() {
            fetch('admin/renew').then( (resp) => {
              return resp.json();
            }).then( (array) => {
			  array.forEach((x, i) => {
                span[i].innerHTML = x
              });
            });
          }
          function check(inObj) {
            !inObj.checkValidity() ? (output.innerHTML = inObj.validationMessage, output.classList.add('note')) : (once(inObj.value, 'reconnect'));
          }
          function re(arg) {
            clearInterval(myIv);
            fetch(arg);
            output.classList.add('note');
            if (arg == 'restart') {
              output.innerHTML = 'Der Server wird neu gestartet. Die Daten werden in 15 Sekunden neu geladen.';
              setTimeout(once, 15000);
            } 
            else if (arg == 'reconnect'){
              output.innerHTML = 'Die WiFi Verbindung wird neu gestartet. Daten werden in 10 Sekunden neu geladen.';
              setTimeout(once, 10000);
            }
            else {
              output.innerHTML = 'Es ist ein Verbindungfehler aufgetreten. Es wird versucht neu zu verbinden.';
              setTimeout(once, 3000);
            }
          }
        });
      </script>
    </head>
    <body>
      <h1>ESP8266 Admin Page</h1>
      <main>
         <aside id="left">
            <span>Runtime ESP:</span>
            <span>WiFi RSSI:</span>
            <span>ADC/VCC:</span>
            <span>Sketch Name:</span>
            <span>Sketch Build:</span>
            <span>SketchSize:</span>
            <span>FreeSketchSpace:</span>
            <span>IPv4 Address:</span>
			<span>Link-Local:</span>
			<span class="ip">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 class="ip">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>IDE 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>0</span>
            <span class="ip">0</span>
            <span>?</span>
            <span>?</span>
            <span>0</span>
            <span>0</span>
            <span>0</span>
            <span>0</span>
            <span>0</span>
            <span class="ip">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>Spiffs</button>
         <button>Startseite</button>
      </div>
      <div id="note"></div>
      <div>
         <form>
		   <input placeholder=" neuer Hostname" pattern="([A-Za-z0-9-]{1,32})" title="Es dürfen nur Buchstaben
		   (a-z, A-Z), Ziffern (0-9) und Bindestriche (-) enthalten sein. Maximal 32 Zeichen" required>
           <button type="button">Name Senden</button>
         </form>
      </div>
      <div>
         <button>WiFi Reconnect</button>
         <button>ESP Restart</button>
      </div>
   </body>
</html>
spiffs.html

<!DOCTYPE HTML> <!-- For more information visit: https://fipsok.de -->
<html lang="de">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="style.css">
    <title>Esp8266 Datei Manager</title>
    <script>
	  var to = JSON.parse(localStorage.getItem('sortBy'));
      document.addEventListener('DOMContentLoaded', () => {
	    list(to);
        fs.addEventListener('change', () => {
		  for (var bytes = 0, j = 0; j < event.target.files.length; j++) bytes += event.target.files[j].size;
          for (var out = `${bytes} Byte`, i = 0, circa = bytes / 1024; circa > 1; circa /= 1024) {
            out = circa.toFixed(2) + [' KB', ' MB', ' GB'][i++];
          }
          if (bytes > free) {
            si.innerHTML = `<li><b> ${out}</b><strong style="color: red;"> Ungenügend Speicher frei </strong></li>`;
            up.setAttribute('disabled', 'disabled');
          }
          else {
            si.innerHTML = `<li><b>Dateigröße:</b> ${out} </li>`;
            up.removeAttribute('disabled');
          }
        });
		btn.addEventListener('click', () => {
	      if (!confirm(`Wirklich formatieren? Alle Daten gehen verloren.\nDu musst anschließend spiffs.html wieder laden.`)) event.preventDefault();
	    });
	  });
	  function list(arg){
		let myList = document.querySelector('main');
        fetch('list?sort='+arg).then( (response) => {
          return response.json();
        }).then((json) => {
		  myList.innerHTML = '';
          for (var i = 0; i < json.length - 1; i++) {
            let dir = `<li><a href ="${json[i].name}">${json[i].name}</a><small> ${json[i].size}</small><a href ="${json[i].name}"download="${json[i].name}"> Download </a>`;
            if (json[i].name != 'spiffs.html') dir += `or <a href ="?delete=/${json[i].name}">Delete </a>`;
            myList.insertAdjacentHTML('beforeend', dir);
          }
          myList.insertAdjacentHTML('beforeend', `<li><b id="so">${to ? '&#9660;' : '&#9650;'} SPIFFS</b> belegt ${json[i].usedBytes} von ${json[i].totalBytes}`);
          free = json[i].freeBytes;
		  so.addEventListener('click', () => {
			list(to=++to%2);
			localStorage.setItem('sortBy', JSON.stringify(to));
	      });
        });
      }
    </script>
  </head>
  <body>
    <h2>ESP8266 Datei Manager</h2>
    <form action="/upload" method="POST" enctype="multipart/form-data">
	  <input id="fs" type="file" name="upload[]" multiple>
      <input id="up" type="submit" value="Upload" disabled>
    </form>
    <div>
      <span id="si"></span>
      <main></main>
    </div>
    <form action="/format" method="POST"><input id="btn" type="submit" value="Format SPIFFS"></form>	
  </body>
</html>
style.css

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;}input{height:35px;font-size:14px}h1+main{display:flex}aside{display:flex;flex-direction:column;padding:.2em}#left{align-items:flex-end;text-shadow:.5px .5px 1px #757474}.note{background-color:#fecdee;padding:.5em;margin-top:1em;text-align:center;max-width:320px;border-radius:.5em}[type=submit]{height:40px;font-size:16px}[value*=Format]{margin-top:1em;box-shadow:5px 5px 5px rgba(0,0,0,0.7)}button{height:40px;width:130px;background-color:#7bff97;font-size:16px;margin-top:1em;box-shadow:5px 5px 5px rgba(0,0,0,0.7)}form [title]{background-color:skyblue;font-size:16px;width:125px}
Kommentar eintragen

*