Support Ethernet thermostat project via PayPal. Your support will allow me to add new functionalities (async webserver, manual mode, Over-The-Air firmware upload) into firmware that will be distributed to everyone on this webpage: PayPal donate
If you are interested in source code for ESP32 + PHY Ethernet thermostat, email me at: martinius96@gmail.com
Since users stopped respecting the project's license, the project is no longer available with the source code.
You can use compiled .bin firmware with dynamic IPv4 address in LAN. Instructions for uploading firmware to ESP32 can be found under WiFi thermostat.
The ESP32 microcontroller is connected to the LAN8720 PHY Ethernet module via the RMII interface. ESP32 provides the MAC layer for the PHY Ethernet via the WiFi controller.
Web server implemented in Arduino Core and allows controlling the thermostat through a browser in the LAN network, where the ESP32 is available at an assigned IP address within the DHCP range.
The thermostat logic is executed regardless of whether the web server web page is open to the client.
Changing the logic and threshold temperatures is done through HTTP requests from clients in the network via an HTML form, or the request can be sent directly through an HTTP POST request with default arguments fname and fname2.
Based on the request for a specific subpage, it is possible to control the output, for example manually / automatically, or it is possible to overwrite the control data with an argument set to a certain value.
They are stored in the emulated EEPROM memory in the flash memory, while the lifetime of this sector is at the level of 10,000 rewrites.
It is possible to control the reference (requested) temperature and hysteresis.
The web server runs on the standard HTTP port - 80.
Thermostat can automatically control the signaling relay (with inverted logic) for switching the gas boiler ON / OFF via the GPIO output state.
It can thus replace the existing room thermostat and make it available in the LAN network to all clients.
Thermostat can operate on any device with a browser - computer / smartphone / tablet / Smart TV and similar.
As a decision algorithm, a target temperature with hysteresis is used, which is compared with the measured temperature from the Dallas DS18B20 digital temperature sensor.
The target temperature and hysteresis is read from the EEPROM memory, where it is stored permanently even in the event of a power failure and is overwritten when new data is written.
The resolution of the DS18B20 sensor during measurement is 12-bit, which is explained by the temperature resolution of 0.0625 °C, which is the minimum resolution step between different measurements.
Data over the OneWire bus can arrive at the microcontroller upon request in 500 to 1000 ms, depending on the number of sensors on the OneWire bus, the length of the bus, etc...
The decision-making logic of the thermostat is performed every 10 seconds independently of the web application, a keep-alive connection is not required to perform the logic, the system thus works autonomously and does not require the user's attention.
On the hardware side, the project uses:
ESP32
Ethernet module LAN8720
DS18B20 temperature sensor on the OneWire bus in a TO-92 case, or in a waterproof version in an aluminum tube
Electromagnetic relay SRD-5VDC-SL-C / SSR relay OMRON G3MB-202P used for switching the boiler (Active-LOW signal)
Wiring of hardware components:
ESP32
Dallas DS18B20
3V3
Vcc
GND
GND
D5
DATA
ESP32
Relay (OMRON G3MB-202P / SRD-05VDC-SL-C)
5V
Vcc
GND
GND
D4
IN
ESP32
PHY Ethernet LAN8720
3V3
Vcc
GND
GND / RBIAS
D18
MDIO
D19
TXD0
D21
TXEN
D22
TXD1
D23
MDC
D25
RXD0
D26
RXD1
D27
CRS_DV
HTML webpages running on ESP32 webserver:
/ - root page containing the form, the current listing of the logic output for the relay, the current and target temperature, the temperature, hysteresis
/action.html - processes the values from the form, writes them to the emulated EEPROM memory, redirects the user back to the root page
/get_data.json - distributes data about current temperature, reference temperature and hysteresis to a third party (computer, microcontroller, other client...) in JSON format - can be used with an example JSON client that can send data to MQTT Broker, for example to home automation
/zap.html - permanent activation of the output in manual mode
/vyp.html - permanent shutdown of the output in manual mode
/automat.html - changing the thermostat mode to automatic (uses hysteresis and target temperature)
/manual.html - changing the thermostat mode to manual (permanent control of the GPIO output for boiler status ON / OFF)
Electromagnetic relay SRD-5VDC-SL-C, which is used in the project, allows to switch up to 10A at 230V - maximum power 2300W.
In the case of switching a DC circuit (load), it is possible to switch 300W (10A at 30V DC max).
For the wiring diagram, the OMRON G3MB-202P SSR relay is also fully compatible, which is only suitable for non-inductive loads and exclusively for AC voltage loads (the DC circuit cannot open after switching).
Maximum switched power 460W (230V, 2A). The thermostat can be used all year round. In case of unnecessary control, the output can be physically disconnected and the thermostat can be used as an Ethernet thermometer to obtain data from the room where it is located.Web interface for Ethernet thermostat enables:
View in real time the temperature from the DS18B20 sensor on the OneWire bus, device uptime, output status with dynamic change, currently set configuration data for the thermostat, i.e. target temperature and hysteresis from EEPROM
Modify the target (reference) temperature in the range 5 to 50 °C with 0.25 °C steps
Modify hysteresis in the range 0 to 10 °C in 0.25 °C steps
ON/OFF boiler regulation:
Example ON/OFF heating regulation - VISUALIZATION IS NOT PART OF THE PROJECT
The boiler is active until the target temperature + hysteresis is reached
The visualization of water temperatures shows the so-called heating run-up and subsequent cooling of the water until heating is reactivated, when the measured temperature is below the set target temperature - hysteresis
The web interface is designed to adapt to larger and smaller screens. It is responsive, supports high-resolution widescreens, but also mobile devices.
The interface uses imported Bootstrap framework CSS styles from an external CDN server, which loads the client-side device when opening a page running on ESP32.
In order for the set values of the thermostat to be preserved even after a power failure, they are stored in the ESP's EEPROM memory, which is emulated in flash memory, since the platform does not physically have an EEPROM chip (memory).
Reference temperature at offset 10, hysteresis at offset 100. Each value occupies a maximum of 5B in EEPROM memory + terminating character.
The data is overwritten only when the HTML form is sent, the operation of the thermostat is thus maximally gentle on the EEPROM memory for its maximum durability.
The state of the output exists only in the RAM memory, where it is overwritten when changed. The value is not stored in the emulated EEPROM memory in the flash memory.
By means of the Refresh meta tag, the web server refreshes the entire page every 30 seconds, and an approximate refresh time is also written to the HTML page via Javascript.
It is necessary to write down the change for the thermostat by this time, otherwise the input windows for numerical inputs to the form will be reset when the page is refreshed. Based on feedback from Android device users, the Refresh time has been extended from 10 to 30 seconds.
The dynamic data that primarily changes is the current value of the output - ON / OFF , which informs the operator about the actual state of the output together with the color marking.
As the logic of the system is executed independently of the web server, the output may already be in a different state than the one currently displayed in the web application before the refresh. The output change is immediately printed, for example, on the UART monitor (115200 baud/s).
On the website of the thermostat, the user can also find information about the uptime of the device (how long it runs), i.e. time in days, hours, minutes and seconds.
The author of the Ethernet thermostat is not responsible for the functionality of the thermostat, boiler failure, electric shock due to improper installation of the thermostat in the network.
Main page for modification of target temperature and hysteresis - preview of switched on output:
Sample data
Target temperature: 22.75 °C
Hysteresis: 0.25 °C
Measured data: 22.49 °C
Output: ON
The thermostat heats from a measured temperature of 22.49 °C and below.
If the temperature reaches 23.01 °C and higher, the output is switched off, the signaling relay is disconnected and the gas boiler stops heating.
The heating and cooling of the room in which the measurements are performed takes place.
The thermostat is activated again only when the temperature reaches 22.49 °C or lower.
Fully functional Ethernet thermostat with the option of setting data controls:
Refresh the web interface automatically every 30 seconds
Main control webpage of Ethernet PHY thermostat - output ON:Processing progress of entered data (user redirection):JSON output of the webserver in the browser / client via websocket:
Output to UART monitor - system logic + set IP address:
/*|-----------------------------------------------------------|*/
/*|HTTP webserver - Ethernet thermostat - ESP32 + PHY LAN8720 |*/
/*|Project webpage: |*/
/*|https://martinius96.github.io/termostat-ethernet/en |*/
/*|AUTHOR: Martin Chlebovec |*/
/*|EMAIL: martinius96@gmail.com |*/
/*|-----------------------------------------------------------|*/
#include <ETH.h>
#include <WebServer.h>
WebServer server(80);
#include <EEPROM.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#define ONE_WIRE_BUS 5 //D5
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensorsA(&oneWire);
const int rele = 4; //D4
unsigned long time_running = 0;
String state = "OFF";
float temperature;
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
#ifdef ETH_CLK_MODE
#undef ETH_CLK_MODE
#endif
#define ETH_CLK_MODE ETH_CLOCK_GPIO17_OUT
// Pin# of the enable signal for the external crystal oscillator (-1 to disable for internal APLL source)
#define ETH_POWER_PIN -1
// Type of the Ethernet PHY (LAN8720 or TLK110)
#define ETH_TYPE ETH_PHY_LAN8720
// I²C-address of Ethernet PHY (0 or 1 for LAN8720, 31 for TLK110)
#define ETH_ADDR 1
// Pin# of the I²C clock signal for the Ethernet PHY, DONT USE THIS PIN FOR ultrasonic sensor in this sketch
#define ETH_MDC_PIN 23
// Pin# of the I²C IO signal for the Ethernet PHY
#define ETH_MDIO_PIN 18
void WiFiEvent(WiFiEvent_t event)
{
switch (event) {
case ARDUINO_EVENT_ETH_START:
Serial.println("ETH Started");
//set eth hostname here
ETH.setHostname("esp32-ethernet");
break;
case ARDUINO_EVENT_ETH_CONNECTED:
Serial.println("ETH Connected");
break;
case ARDUINO_EVENT_ETH_GOT_IP:
Serial.print("ETH MAC: ");
Serial.print(ETH.macAddress());
Serial.print(", IPv4: ");
Serial.print(ETH.localIP());
if (ETH.fullDuplex()) {
Serial.print(", FULL_DUPLEX");
}
Serial.print(", ");
Serial.print(ETH.linkSpeed());
Serial.println("Mbps");
break;
case ARDUINO_EVENT_ETH_DISCONNECTED:
Serial.println("ETH Disconnected");
break;
case ARDUINO_EVENT_ETH_STOP:
Serial.println("ETH Stopped");
break;
default:
break;
}
}
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 page = F("<!DOCTYPE html>");
page += F("<html>");
page += F("<head>");
page += F("<meta charset='utf-8'>");
page += F("<meta name='author' content='Martin Chlebovec'>");
page += F("<meta http-equiv='Refresh' content='30'; />");
page += F("<meta name='viewport' content='width=device-width, initial-scale=1'>");
page += F("<link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css'>");
page += F("<script type='text/javascript'>");
page += F("var timeleft = 30;");
page += F("var downloadTimer = setInterval(function(){");
page += F("if(timeleft <= 0){");
page += F("clearInterval(downloadTimer);");
page += F("document.getElementById(\"countdown\").innerHTML = \"Refreshing...\";");
page += F("} else {");
page += F("document.getElementById(\"countdown\").innerHTML = timeleft + \" seconds to refresh\";");
page += F("}");
page += F("timeleft -= 1;");
page += F("}, 1000);");
page += F("</script>");
page += F("<title>Ethernet thermostat - ESP32 + PHY Ethernet</title>");
page += F("</head>");
page += F("<body>");
page += F("<center><h3>Enter datas for web server:</h3>");
page += F("<form action='/action.html' method='post'>");
page += "<b>Target temperature:</b><br><input type='text' id='fname' name='fname' min='5' max='50' step='0.25' value=" + String(read_String(10)) + "><br>";
page += "<b>Hysteresis:</b><br><input type='text' id='fname2' name='fname2' min='0' max='10' step='0.25' value=" + String(read_String(100)) + "><br>";
page += F("<input type='submit' class='btn btn-success' value='Send'>");
page += F("</form><hr>");
if (state == "ON") {
page += F("<b><font color='green'>Output: ON</font></b>");
}
if (state == "OFF") {
page += F("<b><font color='red'>Output: OFF</font></b>");
}
page += F("<div id=\"countdown\"></div>");
page += F("<b>Current sensor DS18B20 temperature:</b> ");
page += String(temperature);
page += F(" °C");
page += F("<hr>");
page += F("<b>Uptime: </b>");
page += String(days);
page += F("d");
page += F(" ");
page += String(hours);
page += F("h");
page += F(" ");
page += String(minutes);
page += F("m");
page += F(" ");
page += String(seconds);
page += F("s");
page += F("<h3>Author: Martin Chlebovec - martinius96@gmail.com - https://martinius96.github.io/termostat-ethernet/en/</h3>");
page += F("<h4>Free version - 1.0.4 build: 22. Aug. 2022</h4>");
page += F("</center>");
page += F("</body>");
page += F("</html>");
server.send(200, "text/html", page);
}
void handleBody() {
if (server.hasArg("fname")) {
String target_temp = server.arg("fname");
// float target_temperature = target_temp.toFloat();
if (isFloat(target_temp)) {
float target_temperature = target_temp.toFloat();
writeString(10, target_temperature);
} else {
Serial.println(F("No number was entered in the target temperature input field!"));
Serial.println(F("Datas will be not stored in EEPROM!"));
}
}
if (server.hasArg("fname2")) {
String hysteresis = server.arg("fname2");
if (isFloat(hysteresis)) {
float hysteresis = hysteresis.toFloat();
writeString(100, hysteresis);
} else {
Serial.println(F("No number was entered in the hysteresis input field!"));
Serial.println(F("Datas will be not stored in EEPROM!"));
}
}
String page = F("<!DOCTYPE html>");
page += F("<html>");
page += F("<head>");
page += F("<meta charset='utf-8'>");
page += F("<meta http-equiv='Refresh' content='5; url=/' />");
page += F("<title>Ethernet thermostat - ESP32 - data processing</title>");
page += F("</head>");
page += F("<body>");
page += F("<center><h3>Server received datas from HTML form:</h3>");
page += "<li><b>Target temperature: </b>" + String(read_String(10)) + " °C</li>";
page += "<li><b>Hysteresis: </b>" + String(read_String(100)) + " °C</li>";
page += F("<b>Redirecting... Please wait</b></center>");
page += F("</body>");
page += F("</html>");
server.send(200, "text/html", page);
}
void handleGet() {
String page = "{\n";
page += F("\"Hysteresis\":");
page += String(read_String(100));
page += F(",\n");
page += F("\"Target_Temperature\":");
page += String(read_String(10));
page += F(",\n");
page += F("\"Actual_Temperature\":");
page += String(temperature) + "\n";
page += F("}\n");
server.send(200, "application/json", page);
}
void setup() {
Serial.begin(115200);
WiFi.onEvent(WiFiEvent);
ETH.begin(ETH_ADDR, ETH_POWER_PIN, ETH_MDC_PIN, ETH_MDIO_PIN, ETH_TYPE, ETH_CLK_MODE);
EEPROM.begin(512); //Initialize EEPROM
float a = read_String(10);
float b = read_String(100);
if (isnan(a)) {
writeString(10, 20.25);
}
if (isnan(b)) {
writeString(100, 0.25);
}
sensorsA.begin();
pinMode(rele, OUTPUT);
digitalWrite(rele, HIGH);
sensorsA.requestTemperatures();
delay(750);
Serial.println(F("Webapp created by: Martin Chlebovec"));
Serial.println(F("Build 1.0.4 from 22. Aug. 2022"));
Serial.println(F("IP address of Ethernet thermostat: "));
Serial.print(ETH.localIP());
server.on("/", handleRoot);
server.on("/get_data.json", handleGet);
server.on("/action.html", HTTP_POST, handleBody);
server.begin();
}
void loop() {
if ((millis() - time_running) >= 10000 || time_running == 0) {
time_running = millis();
temperature = sensorsA.getTempCByIndex(0);
Serial.println();
Serial.println(F("----------------------------------------------"));
Serial.println(F("IP address of Ethernet thermostat: "));
Serial.print(ETH.localIP());
Serial.print(F(", to access Ethernet thermostat, visit http://"));
Serial.print(ETH.localIP());
Serial.println(F("/"));
Serial.print(F("Free HEAP: "));
Serial.print(ESP.getFreeHeap());
Serial.println(F(" B"));
Serial.print(F("Current temperature: "));
Serial.print(String(temperature));
Serial.println(F(" °C"));
sensorsA.requestTemperatures();
float target_temperature = read_String(10);
float hysteresis = read_String(100);
float minus_hysteresis_temperature = (-1 * hysteresis);
float difference_temps = target_temperature - temperature; //21 - 20
if (difference_temps > hysteresis) {
Serial.println(F("Output on"));
state = "ON";
digitalWrite(rele, LOW);
} else if (difference_temps < minus_hysteresis_temperature) {
Serial.println(F("Output off"));
state = "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(state);
}
}
server.handleClient();
yield();
}
The implementation includes a program for a dynamic IPv4 address assigned to the Ethernet PHY of the thermostat.
The thermostat is intended only for indoor temperatures! (above 0°C), to which the logic of the system is also adapted!
The thermostat can be used to replace an existing room thermostat, or to temporarily replace a heater in an aquarium / terrarium to maintain a constant temperature.