Repozitár projektu WiFi termostat so strojovými kódmi pre cieľovú platformu ESP8266 a ESP32: Firmvér
Firmvér WiFi_Termostat a WiFi_Termostat_mDNS je dostupný okrem slovenského, aj v anglickom jazyku. Experimentálny firmvér s manuálnym ovládaním výstupu je dostupný iba v slovenskom jazyku.
Podporte projekt WiFi termostat cez PayPal. Podpora umožní pridať nové funkcionality v budúcnosti a otvorenie zdrojového kódu aplikácie: PayPal donate
ESP8266 ESP32 WiFi DS18B20 OneWire Dallas HTML Webserver WebSocket JSON mDNS UART

WiFi termostat - ESP8266 / ESP32


Naša programová implementácia je špeciálne navrhnutá pre mikrokontrólery od renomovaného výrobcu Espressif Systems, ktoré disponujú WiFi konektivitou. Podporujeme mikrokontroléry ESP8266 a ESP32 s WiFi (2,4 GHz) technológiou. Náš WiFi termostat je ľahko prístupný prostredníctvom LAN siete, v ktorej sa nachádza. Je vybavený intuitívnym webovým rozhraním, ktoré umožňuje konfiguráciu všetkých parametrov termostatu a poskytuje vizuálny prehľad o aktuálnych stavoch. Termostat efektívne riadi kotol na základe nameranej teploty, cieľovej hodnoty a definovanej hysterézy, pričom je nezávislý od webovej aplikácie. Webová aplikácia slúži výhradne na konfiguráciu a stanovenie rozhodovacích prahov termostatu. Okrem jednoduchej dostupnosti termostatu na konkrétnej IP adrese je možné pridať mDNS záznam, ktorý vytvára lokálnu doménu (hostname.local). Táto doména je prístupná iba v rámci LAN siete, čo zvyšuje užívateľskú pohodlnosť v rámci multicastovej služby. Konfigurácia termostatu pre domácu WiFi LAN sieť je jednoduchá vďaka WiFiManagerovi, ktorý bezpečne ukladá údaje o WiFi sieti (SSID a heslo) priamo do flash pamäte mikrokontroléra. Tieto údaje sú uložené permanentne, umožňujúc termostatu automatické pripojenie po získaní IP adresy od vášho routra v domácej WiFi sieti. Náš HTTP webserver, bežiaci na mikrokontroléroch ESP8266 / ESP32, umožňuje simultánny beh viacerých nezávislých HTML stránok. Tieto stránky môžu slúžiť nielen informatívnym účelom, ale aj ako funkčné rozhranie, čím zvyšujú využiteľnosť nášho termostatu.

Po hardvérovej stránke projekt využíva:
  • ESP8266 / ESP32
  • Teplotný senzor DS18B20 na OneWire zbernici
  • Relé SRD-5VDC-SL-C / OMRON G3MB-202P slúžiace na spínanie kotla (Active-LOW signál)

  • HTML stránky bežiace na platforme ESP8266 / ESP32:
  • / - root stránka obsahujúca formulár, aktuálny výpis logického výstupu pre relé, aktuálnu a cieľovú teplotu teplotu, hysterézu
  • /action.html - spracúvava hodnoty z formulára, zapisuje ich do emulovanej EEPROM pamäte, presmeruje používateľa späť na root stránku
  • /get_data.json - distribuuje dáta o aktuálnej teplote, referenčnej teplote a hysteréza tretej strane (počítač, mikrokontróler, iný klient...) v JSON formáte - možno využiť s príkladom JSON klient, ktorý dáta vie odoslať na MQTT Broker, napríklad do domácej automatizácie

  • Rozšírené o HTML stránky v prípadne experimentálnej verzie s manuálnym režimom:
  • /zap.html - permanentné zapnutie výstupu v manuálnom režime
  • /vyp.html - permanentné vypnutie výstupu v manuálnom režime
  • /automat.html - zmena režimu na automatický (používa hysterézu a cieľovú teplotu)
  • /manual.html - zmena režimu na manuálny (permanentné ovládanie výstupu ZAP / VYP natvrdo)

  • Senzor DS18B20 s rozlíšením 12 bitov poskytuje presné merania s minimálnym krokom teploty 0.0625 °C. Dáta získané cez OneWire zbernicu môžu byť prenesené do mikrokontroléra za 500 až 1000 ms, pričom doba odpovede závisí od počtu pripojených senzorov a dĺžky zbernice. V našom projekte využívame elektromagnetické relé SRD-5VDC-SL-C, ktoré dokáže spínať až 10A pri 230V, čo zodpovedá výkonu 2300W. Pre jednosmerný obvod je možné spínať 300W (10A pri 30V DC). V prípade potreby je možné použiť aj SSR relé OMRON G3MB-202P, ktoré je ideálne pre neindukčné záťaže a určené výhradne pre obvody so striedavým napätím. Jeho maximálny spínaný výkon je 460W (230V, 2A). Termostat, vybavený týmito komponentmi, je vhodný na celoročné používanie. V prípade nevyžadovaného riadenia je možné fyzicky odpojiť výstup a využívať termostat ako WiFi teplomer pre monitorovanie teploty v danej miestnosti.


    V prípade, že používate firmware s možnosťou manuálneho ovládania GPIO výstupu mikrokontroléru ESP, je možné termostat fyzicky vypnúť bez nutnosti odpojenia zo svorkovnice relé. Logika termostatu sa vykonáva každých 10 sekúnd nezávisle na webserveri a pripojených klientoch, čo eliminuje potrebu udržiavať keep-alive spojenie. Po vykonaní každej logiky termostat vypíše informáciu o aktuálnej IP adrese a prípadne aj mDNS zázname (v prípade, že je firmware konfigurovaný s mDNS podporou). Týmto spôsobom poskytuje používateľovi údaje o dostupnosti termostatu so svojím webovým rozhraním v rámci LAN siete. Termostat navyše informuje o aktuálnom stave výstupu, vrátane oznamu o akýchkoľvek zmenách. Dynamická voľná pamäť (HEAP) termostatu sa pohybuje v rozmedzí 40 až 44 kB. Výstupná 3,3V operačná logika GPIO mikrokontrolérov ESP8266 a ESP32 postačuje pre digitálny signál zmeny. Je však dôležité mať na pamäti, že relé musí byť napájané 5V z VUSB alebo VIN pre správne fungovanie.

    Webové rozhranie pre WiFi termostat umožňuje:
  • Prehliadať v reálnom čase teplotu zo senzora DS18B20, uptime zariadenia, hodnotu výstupu s dynamickou zmenou, aktuálne nastavené konfiguračné údaje pre termostat t.j. cieľovú teplotu a hysterézu
  • Modifikovať cieľovú (referenčnú) teplotu v rozsahu 5 až 50 °C s 0,25 °C krokom
  • Modifikovať hysterézu v rozsahu 0 až 10 °C s 0,25 °C krokom
  • Programová implementácia termostatu s automatickým i manuálnym režimom ovládania výstupu je experimentálna!
    ZAP/VYP regulácia kotla - automatický režim:
  • Príklad ZAP/VYP regulácie vykurovania - VIZUALIZÁCIA NIE JE SÚČASŤOU PROJEKTU
  • Kotol je aktívny po dobu dostiahnutia cieľovej teploty + hysterézy
  • Na vizualizácii teplôt vody je patrný tzv. dobeh vykurovania a následné chladnutie vody až do opätovnej aktivity vykurovania, kedy je nameraná teplota pod nastavenú cieľovú teplotu - hystérzu
  • ZAP/VYP regulácia kotla s hysterézou

    V základnej verzii nášho WiFi termostatu (bez mDNS záznamu) sme integrovali manuálny režim ovládania s možnosťou jednoduchého prepínania medzi manuálnym a automatickým režimom. Webové rozhranie je flexibilné a prispôsobuje sa rôznym obrazovkám, či už ide o veľké monitory alebo malé mobilné zariadenia. Je plne responzívne, podporuje širokouhlé obrazovky s vysokým rozlíšením a zároveň je optimalizované pre používanie na mobilných zariadeniach. Rozhranie využíva importované CSS štýly z Bootstrap frameworku, ktoré sú načítané z externého CDN servera pri otvorení stránky bežiacej na mikrokontroléri ESP. Týmto spôsobom minimalizujeme výkonové a pamäťové zaťaženie mikrokontroléra, zabezpečujúc rýchlu a efektívnu funkcionalitu webového rozhrania.

    WiFi termostat - webové rozhranie vizualizované v systéme Android - Chrome

    Pre uchovanie nastavených hodnôt termostatu aj po výpadku napájania sme zvolili ukladanie do EEPROM pamäte ESP, ktorá je emulovaná vo flash pamäti. Táto voľba je nevyhnutná, keďže platforma neobsahuje fyzický EEPROM čip (pamäť). V EEPROM pamäti sú uložené referenčná teplota na offsete 10 a hodnota hysterézy na offsete 100. Každá z týchto hodnôt využíva maximálne 5 bajtov v EEPROM pamäti, vrátane ukončovacieho znaku. Dáta sa prepisujú iba pri odoslaní HTML formulára, čo minimalizuje záťaž na EEPROM pamäť a zabezpečuje maximálnu trvanlivosť. Prevádzka termostatu je šetrná k EEPROM pamäti. Stav výstupu existuje výhradne v RAM pamäti, kde sa prepisuje pri každej zmene. Hodnota sa neukladá do emulovanej EEPROM pamäte vo flash pamäti, čo zabezpečuje efektívne a spoľahlivé fungovanie termostatu.


    Pri prvom spustení zariadenia bez existujúcich údajov na EEPROM offsetoch automaticky dochádza k zápisu predvolených hodnôt, a to s referenčnou teplotou 20.25 °C a hysterézou 0.00 °C. Toto fail-safe riešenie umožňuje bezproblémový chod termostatu aj na mikrokontroléroch, ktoré nemajú žiadne predchádzajúce údaje v EEPROM pamäti. Pre zápis do EEPROM pamäte používame funkciu EEPROM.put(), ktorá podporuje akýkoľvek dátový typ, a následné potvrdenie zápisu pomocou EEPROM.commit() na cieľový offset. Implementácia využíva dátový typ float() pre 32-bitové číslo, ktoré je uložené v EEPROM a korešponduje s referenčnou teplotou a hysterézou. Webserver vykonáva obnovu celej HTML stránky každých 30 sekúnd prostredníctvom meta tagu Refresh. Zároveň vypisuje pomocou Javascriptu orientačný čas do ďalšieho obnovenia do HTML stránky. Pre zachovanie zmeny pre termostat je dôležité stihnúť ju zapísať do EEPROM pred obnovením stránky, inak sa input okná pre číselné vstupy do formulára resetujú. Vzhľadom na to, že built-in knižnica Ethernet neumožňuje využitie asynchrónneho webservera (čo je možné pri mikrokontroléroch Espressif ESP8266 / ESP32), je nutné prepisovať celú stránku, pretože táto implementácia je 1:1 s pôvodným Ethernet termostatom.


    Dynamický údaj, ktorý sa predovšetkým mení je aktuálna hodnota výstupu - Zapnutý / Vypnutý, ktorý informuje prevádzkovateľa o skutočnom stave výstupu spoločne aj s farebným označením. Vzhľadom na to, že logika systému operuje nezávisle na webserveri, môže dochádzať k odlišnému stavu výstupu pred refreshom oproti tomu, čo je aktuálne zobrazené v webaplikácii. Akákoľvek zmena výstupu je okamžite zaznamenaná, napríklad na UART monitore. Na webovej stránke termostatu nájde používateľ aj informácie o uptime zariadenia, teda o tom, ako dlho zariadenie beží, vyjadrené v dňoch, hodinách, minútach a sekundách. Termostat je špeciálne navrhnutý iba pre interiérové teploty nad 0°C, a táto charakteristika sa odráža aj v logike systému. Termostat ponúka možnosť nahradiť existujúci izbový termostat, prípadne dočasne zastúpiť ohrievač v akváriu/teráriu na udržiavanie konštantnej teploty. Je vynikajúcim riešením pre kontrolu a udržiavanie teploty vo vnútri prostredí.

    Autor WiFi termostatu nezodpovedá za funkčnosť termostatu, prípadné poruchy kotla ani za úrazy spôsobené elektrickým prúdom v prípade neodbornej montáže termostatu do siete. Je nevyhnutné dodržiavať bezpečnostné pokyny a zabezpečiť profesionálnu inštaláciu pre optimálne a bezpečné využívanie termostatu.
    Hlavná stránka pre modifikáciu cieľovej teploty a hysterézy - ukážka zapnutého:
    Ukážkové dáta
  • Cieľová teplota: 22.75 °C
  • Hysteréza: 0.25 °C
  • Namerané dáta: 22.49 °C
  • Výstup: Zapnutý

  • Termostat spúšťa vykurovanie pri teplote 22.49 °C a nižšej. Po dosiahnutí teploty 23.01 °C sa výstup vypne, signalizačné relé sa rozpojí a plynový kotol zastaví vykurovanie. Následne prebieha fáza dobehu vykurovania, ktorá prispieva k chladnutiu miestnosti, kde sa merania vykonávajú. Termostat sa opäť aktivuje až pri dosiahnutí teploty 22.49 °C alebo nižšej, spúšťajúc ďalší cyklus vykurovania podľa nastavených parametrov.

    Hlavná stránka pre modifikáciu cieľovej teploty a hysterézy: WiFi termostat - Hlavný prehľad s modifikáciou cieľovej teploty a hysterézy - Vypnutý Priebeh spracovania zadaných údajov (presmerovanie používateľa): WiFi termostat - spracovanie údajov z HTML formulára JSON výstup webservera v prehliadači / klientovi cez websocket:
    WiFi termostat - JSON output
    Výstup do UART monitoru - logika systému + nastavená IP adresa, mDNS záznam:
    WiFi termostat - UART - ESP8266 - mDNS záznam WiFi termostat - UART - ESP32 - výstup - ovládanie kotla

    Dostupné knižnice pre mikrokontroléry (ESP)



    WiFi termostat - zdrojový kód - ESP8266

    /*|----------------------------------------------------------|*/
    /*|HTTP webserver - WiFi thermostat - ESP8266 + DS18B20      |*/
    /*|AUTHOR: Martin Chlebovec                                  |*/
    /*|EMAIL: martinius96@gmail.com                              |*/
    /*|DONATE: paypal.me/chlebovec                               |*/
    /*|----------------------------------------------------------|*/
    
    #include <ESP8266WiFi.h>
    #include <ESP8266WebServer.h>
    #include <WiFiManager.h>
    ESP8266WebServer server(80);
    #include <DNSServer.h>
    #include <EEPROM.h>
    #include <OneWire.h>
    #include <DallasTemperature.h>
    
    #define ONE_WIRE_BUS 5 //D1
    OneWire oneWire(ONE_WIRE_BUS);
    DallasTemperature sensorsA(&oneWire);
    const int rele = 4; //D2
    unsigned long cas = 0;
    String stav = "VYP";
    float teplota;
    long day = 86400000; // 86400000 milliseconds in a day
    long hour = 3600000; // 3600000 milliseconds in an hour
    long minute = 60000; // 60000 milliseconds in a minute
    long second =  1000; // 1000 milliseconds in a second
    float rezim;
    boolean isFloat(String tString) {
      String tBuf;
      boolean decPt = false;
    
      if (tString.charAt(0) == '+' || tString.charAt(0) == '-') tBuf = &tString[1];
      else tBuf = tString;
    
      for (int x = 0; x < tBuf.length(); x++)
      {
        if (tBuf.charAt(x) == '.' || tBuf.charAt(x) == ',') {
          if (decPt) return false;
          else decPt = true;
        }
        else if (tBuf.charAt(x) < '0' || tBuf.charAt(x) > '9') return false;
      }
      return true;
    }
    
    void writeString(char add, float data)
    {
      EEPROM.put(add, (data * 1000));
      EEPROM.commit();
    }
    
    
    float read_String(char add)
    {
      float payload = 0;
      float data = EEPROM.get(add, payload);
      return (data / 1000);
    }
    
    void handleRoot() {
      int days = millis() / day ;                                //number of days
      unsigned int hours = (millis() % day) / hour;                       //the remainder from days division (in milliseconds) divided by hours, this gives the full hours
      unsigned int minutes = ((millis() % day) % hour) / minute ;         //and so on...
      unsigned int seconds = (((millis() % day) % hour) % minute) / second;
      String stranka = F("<!DOCTYPE html>");
      stranka += F("<html>");
      stranka += F("<head>");
      stranka += F("<meta charset='utf-8'>");
      stranka += F("<meta name='author' content='Martin Chlebovec'>");
      stranka += F("<meta http-equiv='Refresh' content='30'; />");
      stranka += F("<meta name='viewport' content='width=device-width, initial-scale=1'>");
      stranka += F("<link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css'>");
      stranka += F("<script type='text/javascript'> window.smartlook||(function(d) {"); 
      stranka += F("var o=smartlook=function(){ o.api.push(arguments)},h=d.getElementsByTagName('head')[0];");
      stranka += F("var c=d.createElement('script');o.api=new Array();c.async=true;c.type='text/javascript';");
      stranka += F("c.charset='utf-8';c.src='https://rec.smartlook.com/recorder.js';h.appendChild(c); })(document);"); 
      stranka += F("smartlook('init', '63ff3bf4db2f85029b19856425a4f2086533c9e6');"); 
      stranka += F("</script>");  
      stranka += F("<script type='text/javascript'>");
      stranka += F("var timeleft = 30;");
      stranka += F("var downloadTimer = setInterval(function(){");
      stranka += F("if(timeleft <= 0){");
      stranka += F("clearInterval(downloadTimer);");
      stranka += F("document.getElementById(\"countdown\").innerHTML = \"Reštart...\";");
      stranka += F("} else {");
      stranka += F("document.getElementById(\"countdown\").innerHTML = timeleft + \" sekúnd do reštartu\";");
      stranka += F("}");
      stranka += F("timeleft -= 1;");
      stranka += F("}, 1000);");
      stranka += F("</script>");
      stranka += F("<title>WiFi termostat - ESP8266</title>");
      stranka += F("</head>");
      stranka += F("<body>");
      stranka += F("<center><h3>WiFi termostat - ESP8266:</h3>");
      if (rezim == 0.00) {
        stranka += F("<form action='/action.html' method='post'>");
        stranka += "<b>Referenčná teplota:</b><br><input type='text' id='fname' name='fname' min='5' max='50' step='0.25' value=" + String(read_String(10)) + "><br>";
        stranka += "<b>Hysteréza:</b><br><input type='text' id='fname2' name='fname2' min='0' max='10' step='0.25' value=" + String(read_String(100)) + "><br>";
        stranka += F("<input type='submit' class='btn btn-success' value='Zapísať'>");
        stranka += F("</form>");
        stranka += F("<a href='manual.html' class='btn btn-primary' role='button'>Manuálny režim</a><hr>");
      } else if (rezim == 1.00) {
        if (stav == "ZAP") {
          stranka += F("<a href='vyp.html' class='btn btn-danger' role='button'>Vypnúť</a><br>");
        }
        if (stav == "VYP") {
          stranka += F("<a href='zap.html' class='btn btn-success' role='button'>Zapnúť</a><br>");
        }
        stranka += F("<a href='automat.html' class='btn btn-primary' role='button'>Automatický režim</a><hr>");
      }
      if (stav == "ZAP") {
        stranka += F("<b><font color='green'>Výstup: Zapnutý</font></b>");
      }
      if (stav == "VYP") {
        stranka += F("<b><font color='red'>Výstup: Vypnutý</font></b>");
      }
      stranka += F("<div id=\"countdown\"></div>");
      stranka += F("<b>Aktuálna teplota senzora DS18B20:</b> ");
      stranka += String(teplota);
      stranka += F(" °C");
      stranka += F("<hr>");
      stranka += F("<b>Uptime: </b>");
      stranka += String(days);
      stranka += F("d");
      stranka += F(" ");
      stranka += String(hours);
      stranka += F("h");
      stranka += F(" ");
      stranka += String(minutes);
      stranka += F("m");
      stranka += F(" ");
      stranka += String(seconds);
      stranka += F("s");
      stranka += F("<h3>Autor: Martin Chlebovec - martinius96@gmail.com - https://martinius96.github.io/WiFi-termostat/</h3>");
      stranka += F("<h4>Verzia free - 1.0.3 build: 22. Jan. 2021</h4>");
      stranka += F("</center>");
      stranka += F("<div class='alert alert-info'>");
      stranka += F("Finálny build projektu WiFi termostat. Ďakujem za vyskúšanie webaplikácie. Rozšírené verzie projektu sú platené.<br><strong>Obsah platených verzií:</strong><li>Async Webserver - AJAX update</li><li>Manuálny režim</li><li>OTA aktualizácie</li><li>Ovládanie hlasom cez Amazon Alexa</li><li>Ovládanie cez UDP callbacky</li><li>Možnosť publikácie dát na MQTT Broker (Loxone, IoT Industries Slovakia, Blynk...),</li><li>Dostupné senzory Bosch, Sensirion, DHT</li><li>Watchdog Timer</li><li>Zdrojový kód (.ino) pre aplikáciu.</li><li>Auto-test periférii, fail-safe riešenie</li><li>JSON output rozšírený o systémové dáta (WiFi sieť, RSSI, uptime, napájacie napätie...)</li>");
      stranka += F("</div>");
      stranka += F("</body>");
      stranka += F("</html>");
      server.send(200, "text/html", stranka);
    }
    
    void handleBody() {
      if (server.hasArg("fname")) {
        String target_temp = server.arg("fname");
        // float cielova_teplota = target_temp.toFloat();
    
        if (isFloat(target_temp)) {
          float cielova_teplota = target_temp.toFloat();
          writeString(10, cielova_teplota);
        } else {
          Serial.println(F("Do input pola cielovej teploty nebolo vlozene cislo!"));
          Serial.println(F("Zapis zakazany!"));
        }
      }
      if (server.hasArg("fname2")) {
        String hysteresis = server.arg("fname2");
        if (isFloat(hysteresis)) {
          float hystereza = hysteresis.toFloat();
          writeString(100, hystereza);
        } else {
          Serial.println(F("Do input pola hysterezy nebolo vlozene cislo!"));
          Serial.println(F("Zapis zakazany!"));
        }
      }
      String stranka = F("<!DOCTYPE html>");
      stranka += F("<html>");
      stranka += F("<head>");
      stranka += F("<meta charset='utf-8'>");
      stranka += F("<meta http-equiv='Refresh' content='5; url=/' />");
      stranka += F("<title>WiFi termostat - ESP8266 - spracovanie riadiach dát</title>");
      stranka += F("</head>");
      stranka += F("<body>");
      stranka += F("<center><h3>Server prijal data z formulára:</h3>");
      stranka += "<li><b>Referenčná teplota: </b>" + String(read_String(10)) + " °C</li>";
      stranka += "<li><b>Hysteréza: </b>" + String(read_String(100)) + " °C</li>";
      stranka += F("<b>Presmerovanie... Prosím čakajte</b></center>");
      stranka += F("</body>");
      stranka += F("</html>");
      server.send(200, "text/html", stranka);
    }
    
    void handleGet() {
      String stranka = "{\n";
      stranka += F("\"Hysteresis\":");
      stranka += String(read_String(100));
      stranka += F(",\n");
      stranka += F("\"Target_Temperature\":");
      stranka += String(read_String(10));
      stranka += F(",\n");
      stranka += F("\"Actual_Temperature\":");
      stranka += String(teplota) + "\n";
      stranka += F("}\n");
      server.send(200, "application/json", stranka);
    }
    
    void handleZAP() {
      stav = "ZAP";
      digitalWrite(rele, LOW);
      String stranka = F("<!DOCTYPE html>");
      stranka += F("<html>");
      stranka += F("<head>");
      stranka += F("<meta charset='utf-8'>");
      stranka += F("<meta http-equiv='Refresh' content='0; url=/' />");
      stranka += F("</head>");
      stranka += F("</html>");
      server.send(200, "text/html", stranka);
    }
    
    void handleAuto() {
      writeString(150, 0.00);
      rezim = read_String(150);
      String stranka = F("<!DOCTYPE html>");
      stranka += F("<html>");
      stranka += F("<head>");
      stranka += F("<meta charset='utf-8'>");
      stranka += F("<meta http-equiv='Refresh' content='0; url=/' />");
      stranka += F("</head>");
      stranka += F("</html>");
      server.send(200, "text/html", stranka);
    }
    void handleManual() {
      writeString(150, 1.00);
      rezim = read_String(150);
      String stranka = F("<!DOCTYPE html>");
      stranka += F("<html>");
      stranka += F("<head>");
      stranka += F("<meta charset='utf-8'>");
      stranka += F("<meta http-equiv='Refresh' content='0; url=/' />");
      stranka += F("</head>");
      stranka += F("</html>");
      server.send(200, "text/html", stranka);
    }
    void handleVYP() {
      stav = "VYP";
      digitalWrite(rele, HIGH);
      String stranka = F("<!DOCTYPE html>");
      stranka += F("<html>");
      stranka += F("<head>");
      stranka += F("<meta charset='utf-8'>");
      stranka += F("<meta http-equiv='Refresh' content='0; url=/' />");
      stranka += F("</head>");
      stranka += F("</html>");
      server.send(200, "text/html", stranka);
    }
    void setup() {
      Serial.begin(115200);
      WiFiManager wifiManager;
      wifiManager.autoConnect("WiFi_TERMOSTAT_AP");
      EEPROM.begin(512);  //Initialize EEPROM
      float a = read_String(10);
      float b = read_String(100);
      float c = read_String(150);
      if (isnan(a)) {
        writeString(10, 20.25);
      }
      if (isnan(b)) {
        writeString(100, 0.25);
      }
      if (isnan(c)) {
        writeString(150, 0.00);
      }
      sensorsA.begin();
      pinMode(rele, OUTPUT);
      digitalWrite(rele, HIGH);
      sensorsA.requestTemperatures();
      delay(750);
      Serial.println(F("WiFi termostat - Author: Martin Chlebovec"));
      Serial.println("");
      Serial.println(F("WiFi connected."));
      Serial.println(F("IP address: "));
      Serial.println(WiFi.localIP());
      server.on("/", handleRoot);
      server.on("/get_data.json", handleGet);
      server.on("/automat.html", handleAuto);
      server.on("/manual.html", handleManual);
      server.on("/zap.html", handleZAP);
      server.on("/vyp.html", handleVYP);
      server.on("/action.html", HTTP_POST, handleBody);
      server.begin();
    }
    
    void loop() {
      if ((millis() - cas) >= 10000 || cas == 0) {
        cas = millis();
        teplota = sensorsA.getTempCByIndex(0);
        Serial.println();
        Serial.println(F("----------------------------------------------"));
        Serial.print(F("IP addresa ESP8266 termostat: "));
        Serial.print(WiFi.localIP());
        Serial.print(F(", pre pristup k termostatu navstivte http://"));
        Serial.print(WiFi.localIP());
        Serial.println(F("/"));
        Serial.print(F("Free HEAP: "));
        Serial.print(ESP.getFreeHeap());
        Serial.println(F(" B"));
        Serial.print(F("Aktuálna teplota: "));
        Serial.print(String(teplota));
        Serial.println(F(" °C"));
        sensorsA.requestTemperatures();
        rezim = read_String(150);
        if (rezim == 0.00) {
          float cielova_teplota = read_String(10);
          float  hystereza = read_String(100);
          float minus_hystereza_teplota = (-1 * hystereza);
          float rozdiel = cielova_teplota - teplota; //21 - 20
          if (rozdiel > hystereza) {
            Serial.println(F("Kotol zapnuty"));
            stav = "ZAP";
            digitalWrite(rele, LOW);
          } else if (rozdiel < minus_hystereza_teplota) {
            Serial.println(F("Kotol vypnuty"));
            stav = "VYP";
            digitalWrite(rele, HIGH);
          } else {
            Serial.println(F("Rozdiel cielovej a aktuálnej teploty nie je nad, ani pod hysterezou. Stav vystupu sa nemeni."));
            Serial.print(F("Aktualny stav vystupu pre kotol: "));
            Serial.println(stav);
          }
        } else {
          Serial.print(F("Manualny rezim, stav vystupu: "));
          Serial.println(stav);
        }
      }
      server.handleClient();
      yield();
    }
    

    WiFi termostat - zdrojový kód - ESP32

    /*|----------------------------------------------------------|*/
    /*|HTTP webserver - WiFi thermostat - ESP32 + DS18B20        |*/
    /*|AUTHOR: Martin Chlebovec                                  |*/
    /*|EMAIL: martinius96@gmail.com                              |*/
    /*|DONATE: paypal.me/chlebovec                               |*/
    /*|----------------------------------------------------------|*/
    
    #include <WiFi.h>
    #include <WebServer.h>
    #include <WiFiManager.h>
    WebServer server(80);
    
    #include <EEPROM.h>
    #include <OneWire.h>
    #include <DallasTemperature.h>
    
    #define ONE_WIRE_BUS 23 //D1
    OneWire oneWire(ONE_WIRE_BUS);
    DallasTemperature sensorsA(&oneWire);
    const int rele = 22; //D2
    unsigned long cas = 0;
    String stav = "VYP";
    float teplota;
    long day = 86400000; // 86400000 milliseconds in a day
    long hour = 3600000; // 3600000 milliseconds in an hour
    long minute = 60000; // 60000 milliseconds in a minute
    long second =  1000; // 1000 milliseconds in a second
    float rezim;
    boolean isFloat(String tString) {
      String tBuf;
      boolean decPt = false;
    
      if (tString.charAt(0) == '+' || tString.charAt(0) == '-') tBuf = &tString[1];
      else tBuf = tString;
    
      for (int x = 0; x < tBuf.length(); x++)
      {
        if (tBuf.charAt(x) == '.' || tBuf.charAt(x) == ',') {
          if (decPt) return false;
          else decPt = true;
        }
        else if (tBuf.charAt(x) < '0' || tBuf.charAt(x) > '9') return false;
      }
      return true;
    }
    
    void writeString(char add, float data)
    {
      EEPROM.put(add, (data * 1000));
      EEPROM.commit();
    }
    
    
    float read_String(char add)
    {
      float payload = 0;
      float data = EEPROM.get(add, payload);
      return (data / 1000);
    }
    
    void handleRoot() {
      int days = millis() / day ;                                //number of days
      unsigned int hours = (millis() % day) / hour;                       //the remainder from days division (in milliseconds) divided by hours, this gives the full hours
      unsigned int minutes = ((millis() % day) % hour) / minute ;         //and so on...
      unsigned int seconds = (((millis() % day) % hour) % minute) / second;
      String stranka = F("<!DOCTYPE html>");
      stranka += F("<html>");
      stranka += F("<head>");
      stranka += F("<meta charset='utf-8'>");
      stranka += F("<meta name='author' content='Martin Chlebovec'>");
      stranka += F("<meta http-equiv='Refresh' content='30'; />");
      stranka += F("<meta name='viewport' content='width=device-width, initial-scale=1'>");
      stranka += F("<link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css'>");
      stranka += F("<script type='text/javascript'> window.smartlook||(function(d) {"); 
      stranka += F("var o=smartlook=function(){ o.api.push(arguments)},h=d.getElementsByTagName('head')[0];");
      stranka += F("var c=d.createElement('script');o.api=new Array();c.async=true;c.type='text/javascript';");
      stranka += F("c.charset='utf-8';c.src='https://rec.smartlook.com/recorder.js';h.appendChild(c); })(document);"); 
      stranka += F("smartlook('init', '63ff3bf4db2f85029b19856425a4f2086533c9e6');"); 
      stranka += F("</script>");  
      stranka += F("<script type='text/javascript'>");
      stranka += F("var timeleft = 30;");
      stranka += F("var downloadTimer = setInterval(function(){");
      stranka += F("if(timeleft <= 0){");
      stranka += F("clearInterval(downloadTimer);");
      stranka += F("document.getElementById(\"countdown\").innerHTML = \"Reštart...\";");
      stranka += F("} else {");
      stranka += F("document.getElementById(\"countdown\").innerHTML = timeleft + \" sekúnd do reštartu\";");
      stranka += F("}");
      stranka += F("timeleft -= 1;");
      stranka += F("}, 1000);");
      stranka += F("</script>");
      stranka += F("<title>WiFi termostat - ESP32</title>");
      stranka += F("</head>");
      stranka += F("<body>");
      stranka += F("<center><h3>WiFi termostat - ESP32:</h3>");
      if (rezim == 0.00) {
        stranka += F("<form action='/action.html' method='post'>");
        stranka += "<b>Referenčná teplota:</b><br><input type='text' id='fname' name='fname' min='5' max='50' step='0.25' value=" + String(read_String(10)) + "><br>";
        stranka += "<b>Hysteréza:</b><br><input type='text' id='fname2' name='fname2' min='0' max='10' step='0.25' value=" + String(read_String(100)) + "><br>";
        stranka += F("<input type='submit' class='btn btn-success' value='Zapísať'>");
        stranka += F("</form>");
        stranka += F("<a href='manual.html' class='btn btn-primary' role='button'>Manuálny režim</a><hr>");
      } else if (rezim == 1.00) {
        if (stav == "ZAP") {
          stranka += F("<a href='vyp.html' class='btn btn-danger' role='button'>Vypnúť</a><br>");
        }
        if (stav == "VYP") {
          stranka += F("<a href='zap.html' class='btn btn-success' role='button'>Zapnúť</a><br>");
        }
        stranka += F("<a href='automat.html' class='btn btn-primary' role='button'>Automatický režim</a><hr>");
      }
      if (stav == "ZAP") {
        stranka += F("<b><font color='green'>Výstup: Zapnutý</font></b>");
      }
      if (stav == "VYP") {
        stranka += F("<b><font color='red'>Výstup: Vypnutý</font></b>");
      }
      stranka += F("<div id=\"countdown\"></div>");
      stranka += F("<b>Aktuálna teplota senzora DS18B20:</b> ");
      stranka += String(teplota);
      stranka += F(" °C");
      stranka += F("<hr>");
      stranka += F("<b>Uptime: </b>");
      stranka += String(days);
      stranka += F("d");
      stranka += F(" ");
      stranka += String(hours);
      stranka += F("h");
      stranka += F(" ");
      stranka += String(minutes);
      stranka += F("m");
      stranka += F(" ");
      stranka += String(seconds);
      stranka += F("s");
      stranka += F("<h3>Autor: Martin Chlebovec - martinius96@gmail.com - https://martinius96.github.io/WiFi-termostat/</h3>");
      stranka += F("<h4>Verzia free - 1.0.3 build: 22. Jan. 2021</h4>");
      stranka += F("</center>");
      stranka += F("<div class='alert alert-info'>");
      stranka += F("Finálny build projektu WiFi termostat. Ďakujem za vyskúšanie webaplikácie. Rozšírené verzie projektu sú platené.<br><strong>Obsah platených verzií:</strong><li>Async Webserver - AJAX update</li><li>Manuálny režim</li><li>OTA aktualizácie</li><li>Ovládanie hlasom cez Amazon Alexa</li><li>Ovládanie cez UDP callbacky</li><li>Možnosť publikácie dát na MQTT Broker (Loxone, IoT Industries Slovakia, Blynk...),</li><li>Dostupné senzory Bosch, Sensirion, DHT</li><li>Watchdog Timer</li><li>Zdrojový kód (.ino) pre aplikáciu.</li><li>Auto-test periférii, fail-safe riešenie</li><li>JSON output rozšírený o systémové dáta (WiFi sieť, RSSI, uptime, napájacie napätie...)</li>");
      stranka += F("</div>");
      stranka += F("</body>");
      stranka += F("</html>");
      server.send(200, "text/html", stranka);
    }
    
    void handleBody() {
      if (server.hasArg("fname")) {
        String target_temp = server.arg("fname");
        // float cielova_teplota = target_temp.toFloat();
    
        if (isFloat(target_temp)) {
          float cielova_teplota = target_temp.toFloat();
          writeString(10, cielova_teplota);
        } else {
          Serial.println(F("Do input pola cielovej teploty nebolo vlozene cislo!"));
          Serial.println(F("Zapis zakazany!"));
        }
      }
      if (server.hasArg("fname2")) {
        String hysteresis = server.arg("fname2");
        if (isFloat(hysteresis)) {
          float hystereza = hysteresis.toFloat();
          writeString(100, hystereza);
        } else {
          Serial.println(F("Do input pola hysterezy nebolo vlozene cislo!"));
          Serial.println(F("Zapis zakazany!"));
        }
      }
      String stranka = F("<!DOCTYPE html>");
      stranka += F("<html>");
      stranka += F("<head>");
      stranka += F("<meta charset='utf-8'>");
      stranka += F("<meta http-equiv='Refresh' content='5; url=/' />");
      stranka += F("<title>WiFi termostat - ESP32 - spracovanie riadiach dát</title>");
      stranka += F("</head>");
      stranka += F("<body>");
      stranka += F("<center><h3>Server prijal data z formulára:</h3>");
      stranka += "<li><b>Referenčná teplota: </b>" + String(read_String(10)) + " °C</li>";
      stranka += "<li><b>Hysteréza: </b>" + String(read_String(100)) + " °C</li>";
      stranka += F("<b>Presmerovanie... Prosím čakajte</b></center>");
      stranka += F("</body>");
      stranka += F("</html>");
      server.send(200, "text/html", stranka);
    }
    
    void handleGet() {
      String stranka = "{\n";
      stranka += F("\"Hysteresis\":");
      stranka += String(read_String(100));
      stranka += F(",\n");
      stranka += F("\"Target_Temperature\":");
      stranka += String(read_String(10));
      stranka += F(",\n");
      stranka += F("\"Actual_Temperature\":");
      stranka += String(teplota) + "\n";
      stranka += F("}\n");
      server.send(200, "application/json", stranka);
    }
    
    void handleZAP() {
      stav = "ZAP";
      digitalWrite(rele, LOW);
      String stranka = F("<!DOCTYPE html>");
      stranka += F("<html>");
      stranka += F("<head>");
      stranka += F("<meta charset='utf-8'>");
      stranka += F("<meta http-equiv='Refresh' content='0; url=/' />");
      stranka += F("</head>");
      stranka += F("</html>");
      server.send(200, "text/html", stranka);
    }
    
    void handleAuto() {
      writeString(150, 0.00);
      rezim = read_String(150);
      String stranka = F("<!DOCTYPE html>");
      stranka += F("<html>");
      stranka += F("<head>");
      stranka += F("<meta charset='utf-8'>");
      stranka += F("<meta http-equiv='Refresh' content='0; url=/' />");
      stranka += F("</head>");
      stranka += F("</html>");
      server.send(200, "text/html", stranka);
    }
    void handleManual() {
      writeString(150, 1.00);
      rezim = read_String(150);
      String stranka = F("<!DOCTYPE html>");
      stranka += F("<html>");
      stranka += F("<head>");
      stranka += F("<meta charset='utf-8'>");
      stranka += F("<meta http-equiv='Refresh' content='0; url=/' />");
      stranka += F("</head>");
      stranka += F("</html>");
      server.send(200, "text/html", stranka);
    }
    void handleVYP() {
      stav = "VYP";
      digitalWrite(rele, HIGH);
      String stranka = F("<!DOCTYPE html>");
      stranka += F("<html>");
      stranka += F("<head>");
      stranka += F("<meta charset='utf-8'>");
      stranka += F("<meta http-equiv='Refresh' content='0; url=/' />");
      stranka += F("</head>");
      stranka += F("</html>");
      server.send(200, "text/html", stranka);
    }
    void setup() {
      Serial.begin(115200);
      WiFiManager wifiManager;
      wifiManager.autoConnect("WiFi_TERMOSTAT_AP");
      EEPROM.begin(512);  //Initialize EEPROM
      float a = read_String(10);
      float b = read_String(100);
      float c = read_String(150);
      if (isnan(a)) {
        writeString(10, 20.25);
      }
      if (isnan(b)) {
        writeString(100, 0.25);
      }
      if (isnan(c)) {
        writeString(150, 0.00);
      }
      sensorsA.begin();
      pinMode(rele, OUTPUT);
      digitalWrite(rele, HIGH);
      sensorsA.requestTemperatures();
      delay(750);
      Serial.println(F("WiFi termostat - Author: Martin Chlebovec"));
      Serial.println("");
      Serial.println(F("WiFi connected."));
      Serial.println(F("IP address: "));
      Serial.println(WiFi.localIP());
      server.on("/", handleRoot);
      server.on("/get_data.json", handleGet);
      server.on("/automat.html", handleAuto);
      server.on("/manual.html", handleManual);
      server.on("/zap.html", handleZAP);
      server.on("/vyp.html", handleVYP);
      server.on("/action.html", HTTP_POST, handleBody);
      server.begin();
    }
    
    void loop() {
      if ((millis() - cas) >= 10000 || cas == 0) {
        cas = millis();
        teplota = sensorsA.getTempCByIndex(0);
        Serial.println();
        Serial.println(F("----------------------------------------------"));
        Serial.print(F("IP addresa ESP32 termostat: "));
        Serial.print(WiFi.localIP());
        Serial.print(F(", pre pristup k termostatu navstivte http://"));
        Serial.print(WiFi.localIP());
        Serial.println(F("/"));
        Serial.print(F("Free HEAP: "));
        Serial.print(ESP.getFreeHeap());
        Serial.println(F(" B"));
        Serial.print(F("Aktuálna teplota: "));
        Serial.print(String(teplota));
        Serial.println(F(" °C"));
        sensorsA.requestTemperatures();
        rezim = read_String(150);
        if (rezim == 0.00) {
          float cielova_teplota = read_String(10);
          float  hystereza = read_String(100);
          float minus_hystereza_teplota = (-1 * hystereza);
          float rozdiel = cielova_teplota - teplota; //21 - 20
          if (rozdiel > hystereza) {
            Serial.println(F("Kotol zapnuty"));
            stav = "ZAP";
            digitalWrite(rele, LOW);
          } else if (rozdiel < minus_hystereza_teplota) {
            Serial.println(F("Kotol vypnuty"));
            stav = "VYP";
            digitalWrite(rele, HIGH);
          } else {
            Serial.println(F("Rozdiel cielovej a aktuálnej teploty nie je nad, ani pod hysterezou. Stav vystupu sa nemeni."));
            Serial.print(F("Aktualny stav vystupu pre kotol: "));
            Serial.println(stav);
          }
        } else {
          Serial.print(F("Manualny rezim, stav vystupu: "));
          Serial.println(stav);
        }
      }
      server.handleClient();
      yield();
    }