Ethernet thermostat project repository - machine codes: Github
Version 1.0.4 of the Ethernet thermostat project is the latest free version update. Other bugs, bugs and security bugs will not be resolved!
Firmware for the Ethernet thermostat is fully available in English language.
Support the Ethernet thermostat project via PayPal. Support will allow you to add new features in the future and open the source code of the application: PayPal donate
Arduino Ethernet Wiznet W5100 / W5500 DS18B20 OneWire Dallas HTML Webserver WebSocket

Compatible control hardware for Ethernet thermostat:



Compatible Ethernet modules and shields for Ethernet thermostat:



Room thermostat - Arduino + Ethernet


Arduino serves as a versatile embedded platform, ideal for constructing a room thermostat, a project we will explore today. The implementation involves Arduino paired with the Ethernet shield Wiznet W5100, functioning in web server mode. It can handle requests from network clients through the HTTP protocol and respond with HTML code. This thermostat is accessible within the LAN network, equipped with a web interface facilitating the configuration of all thermostat elements. The web server supports multiple independent HTML pages, offering both informative and functional content. It operates on the standard HTTP port - 80. The thermostat autonomously controls the signaling relay to switch on the gas boiler through the output, providing a potential replacement for an existing room thermostat. The decision algorithm utilizes the target temperature with hysteresis, comparing it with the measured temperature from the digital temperature sensor Dallas DS18B20. The DS18B20 sensor boasts a 12-bit resolution during measurements, resulting in a precision of 0.0625 °C. Data transmitted via the OneWire bus can reach the microcontroller within 500 to 1000 ms. The system's decision logic executes every 10 seconds independently of the web server. No keep-alive connection is necessary for logic execution, ensuring the system operates autonomously without requiring constant user attention.

In terms of hardware, the project uses:
  • Arduino Uno
  • Ethernet shield Wiznet W5100 / Ethernet module Wiznet W5200-W5500
  • DS18B20 temperature sensor on the OneWire bus in a TO-92 housing, or in a waterproof version in an aluminum tube
  • Electromagnetic relay SRD-5VDC-SL-C / SSR relay OMRON G3MB-202P used for boiler switching (Active-LOW signal)
  • HTML pages running on Arduino:

  • / - root page containing the form, current listing of logic output for the relay, temperature
  • /action.html - processes values from the form, writes them to the EEPROM memory, redirects the user back to the root page
  • /get_data.json - distributes data on current temperature, reference temperature and hysteresis to a third party (computer, microcontroller, other client ...) in JSON format

  • The SRD-5VDC-SL-C electromagnetic relay, employed in the project, enables the switching of up to 10A at 230V, corresponding to a maximum power of 2300W. When dealing with a DC circuit, it can handle a switching capacity of 300W (10A at 30V DC max). The OMRON G3MB-202P SSR relay is also fully compatible with the wiring diagram, suitable exclusively for non-inductive loads and AC loads (the DC circuit cannot open after switching on). It supports a maximum switching power of 460W (230V, 2A). The thermostat is designed for year-round use. If control is unnecessary, the output can be physically disconnected, allowing the thermostat to function as an Ethernet thermometer for obtaining data from the room where it is located.

    Ethernet thermostat - Arduino + Wiznet W5100/W5500 - webserver The web interface for the Ethernet thermostat allows you to:
  • View in real time the temperature from the DS18B20 sensor, the device uptime, the output value with dynamic change, the currently set configuration data for the thermostat, i. target temperature and hysteresis
  • Modify the target (reference) temperature from 5 to 50 °C with 0.25 °C step
  • Modify hysteresis from 0 to 10 °C with 0.25 °C step
  • Boiler ON / OFF control:
  • Example of ON / OFF of heating control - VISUALIZATION IS NOT PART OF THE PROJECT
  • The boiler is active as long as the target temperature + hysteresis is reached
  • The visualization of water temperatures shows the so-called heating run and subsequent cooling of the water until the heating activity is repeated, when the measured temperature is below the set target temperature - hysteresis
  • ZAP/VYP regulácia kotla s hysterézou

    The web interface is crafted to be adaptable to screens of varying sizes, ensuring responsiveness across larger and smaller displays. It seamlessly supports widescreen high-definition monitors as well as mobile devices. To achieve this, the interface incorporates CSS styles imported from the Bootstrap framework, sourced from an external CDN server. This approach optimizes client-side loading when accessing a page hosted on Arduino. Given the memory constraints of the Arduino Uno, which can only accommodate pages of a few kB in size, importing CSS styles from an external server helps reduce the performance and memory demands on Arduino. In terms of software implementation (for Arduino Uno), approximately 70% of the flash memory (32kB - 4kB Bootloader) and 44% of the RAM memory (2kB) are utilized. Critical components of the web page, such as the HTML document header and footer, Bootstrap CSS linking, meta tags, HTTP response header, Content Type, and forms, are stored directly in Arduino's flash memory. This strategic placement significantly diminishes the RAM usage for user-generated content. The resulting web server is more stable, capable of handling multiple connections from various devices within the network simultaneously. The overall power consumption of the thermostat is within the range of 200 mA at a 5V supply - maintaining efficiency below 1W.


    To retain configured values in the event of a power failure, they are stored in the EEPROM memory of the Arduino. The reference temperature is written to offset 10, while the hysteresis is stored at offset 100. Each of these values takes up a maximum of 5B in the EEPROM memory, including the terminator. The EEPROM transcription is limited to 100,000 transcripts. Data is overwritten exclusively when the HTML form is submitted. The thermostat's operation is designed to be extremely gentle on the EEPROM memory, aiming to maximize its service life. Upon the device's initial startup, if there is no stored information at the mentioned EEPROM offsets, an automatic writing process will take place with default values - reference: 20.25 °C, hysteresis: 0.25 °C. This fail-safe solution ensures that the thermostat is immediately functional and ready for operation.

    The web server employs the Refresh meta tag to refresh the entire page every 30 seconds, and an approximate countdown time is dynamically displayed on the HTML page through Javascript. It is crucial to finalize any thermostat changes within this timeframe; otherwise, the input fields for numerical entries in the form will reset upon page refresh. Responding to feedback from Android device users, the Refresh interval has been extended from 10 to 30 seconds. Due to the absence of asynchronous web server support in the built-in Ethernet library (commonly found in microcontrollers like Espressif ESP8266/ESP32), a full page rewrite becomes necessary. The dynamically changing data primarily includes the current state of the output - On / Off. This information conveys the operational status to the operator, complete with color-coded indications. Since the system logic operates independently of the web server, the output may have changed before the scheduled refresh in the web application. Changes in the output state are instantly reflected in the UART monitor (115200 baud/s). On the thermostat's web page, users will also find details about the device's uptime - indicating how long it has been running in terms of days, hours, minutes, and seconds.

    The author of the Ethernet thermostat disclaims responsibility for the functionality of the thermostat, any issues with the boiler, and the risk of electric shock resulting from improper installation of the thermostat in the network.
    Main page for modification of target temperature and hysteresis - example of switched on output:
    Sample data
  • Target temperature: 22.75 °C
  • Hysteresis: 0.25 °C
  • Measured temperature: 22.49 °C
  • Output: Turned on

  • The thermostat initiates heating when the measured temperature falls to 22.49 °C or below. Once the temperature surpasses 23.01 °C, the output is deactivated, leading to the opening of the signaling relay and subsequently stopping the gas boiler from heating. This cycle controls the heating and cooling of the room where the measurements are conducted. The thermostat remains inactive until the temperature drops to 22.49 °C or lower, at which point it resumes the heating process.

    Ethernet thermostat - Main dashboard - Arduino Process of processing the entered data (user redirection) with VALID and NOT VALID datas: Ethernet thermostat - processing HTML form with valid datas Ethernet thermostat - processing HTML form without valid datas JSON web server output in browser / client via websocket:
    Ethernet thermostat - JSON output
    Output to UART monitor - system logic + Ethernet adapter settings:
    Ethernet thermostat - UART

    Available libraries for microcontrollers (Arduino)



    Source code - Ethernet thermostat - automatic mode

    /*|----------------------------------------------------------|*/
    /*|HTTP webserver - Ethernet thermostat, Wiznet W5100 / W5500|*/
    /*|Arduino Uno / Nano / Mega compatible (R3)                 |*/
    /*|AUTHOR: Martin Chlebovec                                  |*/
    /*|EMAIL: martinius96@gmail.com                              |*/
    /*|DONATE: paypal.me/chlebovec                               |*/
    /*|----------------------------------------------------------|*/
    
    #include <avr\wdt.h>
    #include <SPI.h>
    #include <Ethernet.h> //FOR Wiznet W5100
    //#include <Ethernet2.h> //FOR Wiznet W5500
    #include <EEPROM.h>
    #include <OneWire.h>
    #include <DallasTemperature.h>
    
    #define ONE_WIRE_BUS 5
    OneWire oneWire(ONE_WIRE_BUS);
    DallasTemperature sensorsA(&oneWire);
    const int rele = 6;
    byte mac[] = { 0xDE, 0xAD, 0xBB, 0xEF, 0xFE, 0xAE }; //physical mac address
    byte ip[] = { 192, 168, 4, 1 };
    EthernetServer server(80); //server port
    const char terminator1[2] = " ";
    const char terminator3[2] = "?";
    const char terminator4[2] = "&";
    const char terminator5[2] = "=";
    unsigned long cas = 0;
    
    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 teplota;
    String stav;
    
    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) == '.') {
          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, String data)
    {
      int _size = data.length();
      int i;
      for (i = 0; i < _size; i++)
      {
        EEPROM.write(add + i, data[i]);
      }
      EEPROM.write(add + _size, '\0'); //Add termination null character for String Data
    }
    
    
    String read_String(char add)
    {
      int i;
      char data[100]; //Max 100 Bytes
      int len = 0;
      unsigned char k;
      k = EEPROM.read(add);
      while (k != '\0' && len < 500) //Read until null character
      {
        k = EEPROM.read(add + len);
        data[len] = k;
        len++;
      }
      data[len] = '\0';
      return String(data);
    }
    
    void setup() {
      String a = read_String(10);
      String b = read_String(100);
      if (a == "" || a == NULL) {
        writeString(100, "20.25");
      }
      if (b == "" || b == NULL) {
        writeString(10, "0.25");
      }
      sensorsA.begin();
      pinMode(rele, OUTPUT);
      digitalWrite(rele, HIGH);
      Serial.begin(115200);
      sensorsA.requestTemperatures();
      delay(2000);
      // Ethernet.begin(mac); //FOR DHCP
      Ethernet.begin(mac, ip); //FOR STATIC IPv4
      server.begin();
      Serial.println(F("Webapp created by: Martin Chlebovec"));
      Serial.println(F("Build 1.0.4 from 18. Sep 2021"));
      Serial.println(F("Ethernet thermostat IP address:"));
      Serial.println(Ethernet.localIP());
      wdt_enable(WDTO_8S);
    }
    
    void loop() {
      wdt_reset();
      if ((millis() - cas) >= 10000 || cas == 0) {
        cas = millis();
        Serial.println(F("Ethernet thermostat IP address:"));
        Serial.println(Ethernet.localIP());
        teplota = sensorsA.getTempCByIndex(0);
        String referencia = read_String(10);
        String hystereza = read_String(100);
        float referencia_teplota = referencia.toFloat();
        float hystereza_teplota = hystereza.toFloat();
        float minus_hystereza_teplota = (-1 * hystereza_teplota);
        float rozdiel = referencia_teplota - teplota;
        if (rozdiel > hystereza_teplota) {
          Serial.println(F("Output on"));
          stav = "ON";
          digitalWrite(rele, LOW);
        } else if (rozdiel < minus_hystereza_teplota) {
          Serial.println(F("Output off"));
          stav = "OFF";
          digitalWrite(rele, HIGH);
        } else {
          Serial.println(F("Difference between target and actual temp is not above or below the hysteresis. The output state does not change."));
          Serial.print(F("Actual state of output: "));
          Serial.println(stav);
        }
        sensorsA.requestTemperatures();
      }
      EthernetClient client = server.available();
      if (client) {
        while (client.connected()) {
          if (client.available()) {
            String line = client.readStringUntil('\n');
            char str[line.length() + 1];
            line.toCharArray(str, line.length());
            char *method;
            char *request;
            method = strtok(str, terminator1);
            request = strtok(NULL, terminator1);
            if (String(request) == "/") {
              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;
              //HLAVNA ROOT HTTP STRANKA
              client.println(F("HTTP/1.1 200 OK"));
              client.println(F("Content-Type: text/html"));
              client.println();
              client.println(F("<!DOCTYPE html>"));
              client.println(F("<html>"));
              client.println(F("<head>"));
              client.println(F("<script type='text/javascript'>"));
              client.println(F("window.smartlook||(function(d) {"));
              client.println(F("var o=smartlook=function(){ o.api.push(arguments)},h=d.getElementsByTagName('head')[0];"));
              client.println(F("var c=d.createElement('script');o.api=new Array();c.async=true;c.type='text/javascript';"));
              client.println(F("c.charset='utf-8';c.src='https://rec.smartlook.com/recorder.js';h.appendChild(c);"));
              client.println(F("})(document);"));
              client.println(F("smartlook('init', '6beae97f98b9844b761672af23f38fc60b962338');"));
              client.println(F("</script>"));
              client.println(F("<meta charset='utf-8'>"));
              client.println(F("<meta name='author' content='Martin Chlebovec'>"));
              client.println(F("<meta http-equiv='Refresh' content='30'; />"));
              client.println(F("<meta name='viewport' content='width=device-width, initial-scale=1'>"));
              client.println(F("<link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css'>"));
              client.println(F("<script type='text/javascript'>"));
              client.println(F("var timeleft = 30;"));
              client.println(F("var downloadTimer = setInterval(function(){"));
              client.println(F("if(timeleft <= 0){"));
              client.println(F("clearInterval(downloadTimer);"));
              client.println(F("document.getElementById(\"countdown\").innerHTML = \"Refresh...\";"));
              client.println(F("} else {"));
              client.println(F("document.getElementById(\"countdown\").innerHTML = timeleft + \" seconds to refresh\";"));
              client.println(F("}"));
              client.println(F("timeleft -= 1;"));
              client.println(F("}, 1000);"));
              client.println(F("</script>"));
              client.println(F("<title>HTTP webserver - Arduino + Ethernet</title>"));
              client.println(F("</head>"));
              client.println(F("<body>"));
              client.println(F("<center><h3>Enter data for webserver (will be stored in EEPROM):</h3>"));
              client.println(F("<form action='/action.html' method='get'>"));
              client.println("<b>Target temperature:</b><br><input type='text' id='fname' name='fname' value=" + read_String(10) + "><br>");
              client.println("<b>Hysteresis:</b><br><input type='text' id='fname2' name='fname2' value=" + read_String(100) + "><br>");
              client.println(F("<input type='submit' class='btn btn-success' value='Send'>"));
              client.println(F("</form><hr>"));
              if (stav == "ON") {
                client.println(F("<b><font color='green'>Output: ON</font></b>"));
              }
              if (stav == "OFF") {
                client.println(F("<b><font color='red'>Output: OFF</font></b>"));
              }
              client.println(F("<div id=\"countdown\"></div>"));
              client.print(F("<b>Actual temperature from DS18B20:</b> "));
              client.print(teplota);
              client.println(F(" °C"));
              client.print(F("<hr>"));
              client.print(F("<b>Uptime: </b>"));
              client.print(days);
              client.print(F("d"));
              client.print(F(" "));
              client.print(hours);
              client.print(F("h"));
              client.print(F(" "));
              client.print(minutes);
              client.print(F("m"));
              client.print(F(" "));
              client.print(seconds);
              client.print(F("s"));
              client.println(F("<h3>Author: Martin Chlebovec - martinius96@gmail.com - https://martinius96.github.io/termostat-ethernet/en/</h3>"));
              client.println(F("<h4>Free version - 1.0.4 build: 18. Sep. 2021</h4>"));
              client.println(F("</center>"));
              client.println(F("</body>"));
              client.println(F("</html>"));
              delay(1);
              client.stop();
              client.flush();
            } else if (String(request) == "/get_data.json") {
              //PODSTRANKA PRE VYCITANIE DAT (INYM MIKROKONTROLEROM)
              client.println(F("HTTP/1.1 200 OK"));
              client.println(F("Content-Type: application/json"));
              client.println();
              client.println(F("{"));
              client.print(F("\"Hysteresis\":"));
              client.print(read_String(100));
              client.println(F(","));
              client.print(F("\"Target_Temperature\":"));
              client.print(read_String(10));
              client.println(F(","));
              client.print(F("\"Actual_Temperature\":"));
              client.println(String(teplota));
              client.println(F("}"));
              delay(1);
              client.stop();
              client.flush();
            } else if (String(request) == "/favicon.ico") { //fix chybajuceho faviconu
              client.stop();
            } else {
              String myString = String(request);
              if (myString.startsWith("/action.html")) {
                char* parameter;
                char* value;
                char* hodnota1;
                char* hodnota2;
                parameter = strtok(request, terminator3);
                Serial.println(parameter);
                value = strtok(NULL, terminator3);
                hodnota1 = strtok(value, terminator4);
                hodnota2 = strtok(NULL, terminator4);
                char* H_1;
                char* H_2;
                strtok(hodnota1, terminator5);
                H_1 = strtok(NULL, terminator5);
                strtok(hodnota2, terminator5);
                H_2 = strtok(NULL, terminator5);
                String first_param = String(H_1);
                String second_param = String(H_2);
                if (isFloat(first_param)) {
                  writeString(10, String(H_1));
                } else {
                  Serial.println(F("User input for key fname (Target temperature) is not a number!"));
                }
                if (isFloat(second_param)) {
                  writeString(100, String(H_2));
                } else {
                  Serial.println(F("User input for key fname2 (Hysteresis) is not a number!"));
                }
                client.println(F("HTTP/1.1 200 OK"));
                client.println(F("Content-Type: text/html"));
                client.println();
                client.println(F("<!DOCTYPE html>"));
                client.println(F("<html>"));
                client.println(F("<head>"));
                client.println(F("<meta charset='utf-8'>"));
                client.println(F("<meta http-equiv='Refresh' content='5; url=/' />"));
                client.println(F("<title>HTTP webserver - Arduino + Ethernet</title>"));
                client.println(F("</head>"));
                client.println(F("<body>"));
                client.println(F("<center><h3>Server received datas from HTML form:</h3>"));
                if (!isFloat(second_param) || !isFloat(second_param)) {
                  client.println(F("<h3><font color='red'>The data entered is not a number!!! Please try again after redirecting.</font></h3>"));
                } else {
                  client.println("<li><b>Target temperature: </b>" + String(H_1) + "</li>");
                  client.println("<li><b>Hysteresis: </b>" + String(H_2) + "</li>");
                }
                client.println(F("<b>Redirecting ... Please wait</b></center>"));
                client.println(F("</body>"));
                client.println(F("</html>"));
                delay(1);
                client.stop();
                client.flush();
              } else {
                client.println(F("HTTP/1.1 404 Not Found"));
                client.println();
                delay(1);
                client.stop();
                client.flush();
              }
            }
          }
        }
      }
    }
    
    

    The implementation incorporates programs for both static and dynamic assignment of an IPv4 address to the Ethernet shield. It is important to note that the thermostat is specifically designed for indoor temperatures! (above 0 °C), and the system logic is tailored accordingly. The thermostat is versatile and can function as a replacement for an existing room thermostat. Additionally, it can be temporarily employed to regulate the heater in an aquarium or terrarium, ensuring a consistent temperature.