Unterstützen Sie das Ethernet-Thermostat-Projekt über PayPal. Der Support ermöglicht das Hinzufügen neuer Funktionalitäten (async webserver, manueller Ausgabemodus, Over-The-Air firmware upload) in der Zukunft und das Öffnen des Quellcodes der Anwendung: PayPal donate
Wenn Sie am Quellcode für das ESP32 + PHY-Ethernet-Thermostat interessiert sind, kontaktieren Sie uns bitte per E-Mail auf ENGLISCH: martinius96@gmail.com
Da die Benutzer die Lizenz des Projekts nicht mehr respektieren, ist der Quellcode des Projekts nicht mehr verfügbar.
Sie können jedoch die kompilierte .bin-Firmware mit dynamischer IPv4-Adresse im LAN verwenden. Anweisungen zum Hochladen der Firmware auf den ESP32 finden Sie im WLAN-Thermostat.
Der ESP32-Mikrocontroller ist über die RMII-Schnittstelle mit dem LAN8720 PHY-Ethernet-Modul verbunden. Der ESP32 stellt die MAC-Schicht für das PHY-Ethernet über den WiFi-Controller bereit. Mithilfe des in der Arduino Core implementierten Webservers kann das Thermostat im LAN-Netzwerk über einen Browser gesteuert werden. Der ESP32 ist unter einer zugewiesenen IP-Adresse im DHCP-Bereich verfügbar.
Die Thermostatlogik läuft unabhängig davon, ob die Webseite des Webservers für den Client geöffnet ist. Änderungen an der Logik und den Schwellentemperaturen erfolgen über HTTP-Anfragen von Clients im Netzwerk mithilfe eines HTML-Formulars. Alternativ kann die Anforderung auch direkt über eine HTTP-POST-Anfrage mit den Standardargumenten "fname" und "fname2" gesendet werden.
Je nach Anforderung einer bestimmten Unterseite kann die Ausgabe gesteuert werden, beispielsweise manuell oder automatisch. Es besteht auch die Möglichkeit, die Steuerdaten mit einem auf einen bestimmten Wert gesetzten Argument zu überschreiben. Diese Daten werden im emulierten EEPROM-Speicher im Flash-Speicher gespeichert, wobei die Lebensdauer dieses Sektors bei 10.000 Neuschreibungen liegt.
Es ist möglich, die (angeforderte) Referenztemperatur und Hysterese zu steuern. Der Webserver läuft auf dem Standard-HTTP-Port - 80.
Über den GPIO-Ausgang kann der Thermostat automatisch das Melderelais steuern, um den Boiler ein- oder auszuschalten. Dadurch kann er das vorhandene Raumthermostat ersetzen und allen Clients im Netzwerk zur Verfügung stellen. Der Thermostat kann von jedem Gerät mit einem Browser bedient werden, sei es ein Computer, Smartphone, Tablet, Smart TV oder ähnliches.
Als Entscheidungsalgorithmus wird eine Zieltemperatur mit Hysterese verwendet, die mit der gemessenen Temperatur des digitalen Temperatursensors Dallas DS18B20 verglichen wird. Solltemperatur und Hysterese werden aus dem EEPROM-Speicher ausgelesen und sind auch bei einem Stromausfall dauerhaft gespeichert. Beim Schreiben neuer Daten werden sie überschrieben.
Die Auflösung des DS18B20-Sensors beträgt während der Messung 12 Bit, was einer Temperaturauflösung von 0,0625 °C entspricht, was gleichzeitig der minimale Auflösungsschritt zwischen verschiedenen Messungen ist. Daten über den OneWire-Bus können auf Anfrage in 500 bis 1000 ms beim Mikrocontroller eintreffen, abhängig von der Anzahl der Sensoren am OneWire-Bus, der Länge des Busses usw.
Die Entscheidungslogik des Thermostats wird alle 10 Sekunden unabhängig von der Webanwendung ausgeführt. Eine Keep-Alive-Verbindung ist nicht erforderlich, um die Logik auszuführen. Das System arbeitet autonom und erfordert somit keine laufende Aufmerksamkeit des Benutzers.
In Bezug auf die Hardware verwendet das Projekt:
ESP32
PHY Ethernet Modul LAN8720
Temperatursensor DS18B20 am OneWire-Bus im TO-92-Gehäuse oder in wasserdichter Ausführung im Aluminiumrohr
Elektromagnetisches Relais SRD-5VDC-SL-C / SSR-Relais OMRON G3MB-202P zum Schalten des Kessels (Active-LOW-Signal)
Anschluss von Hardwarekomponenten:
ESP32
Dallas DS18B20
3V3
Vcc
GND
GND
D5
DATA
ESP32
Relais (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-Seiten, die auf ESP32 ausgeführt werden:
/ - Startseite mit dem Formular, der aktuellen Auflistung des Logikausgangs für das Relais, der aktuellen und der Zieltemperatur, der Temperatur, der Hysterese
/action.html - verarbeitet die Werte aus dem Formular, schreibt sie in den emulierten EEPROM-Speicher, leitet den Benutzer zurück zur Stammseite
/get_data.json - verteilt Daten über aktuelle Temperatur, Referenztemperatur und Hysterese an einen Dritten (Computer, Mikrocontroller, anderer Client ...) im JSON-Format - kann mit einem Beispiel-JSON-Client verwendet werden, der Daten an MQTT Broker senden kann, beispielsweise an die Hausautomation
/zap.html - dauerhafte Aktivierung des Ausgangs im Handbetrieb
/vyp.html - dauerhaftes Abschalten des Ausgangs im Handbetrieb
/automat.html - Ändern des Thermostatmodus auf Automatik (verwendet Hysterese und Zieltemperatur)
/manual.html - Ändern des Thermostatmodus auf manuell (permanente Kontrolle des GPIO-Ausgangs für Kesselstatus EIN / AUS)
Das im Projekt verwendete elektromagnetische Relais SRD-5VDC-SL-C ermöglicht das Schalten von bis zu 10 A bei 230 V, was einer maximalen Leistung von 2300 W entspricht. Beim Schalten eines Gleichstromkreises (Last) können bis zu 300 W (10 A bei 30 V DC max) geschaltet werden. Alternativ ist das OMRON G3MB-202P SSR-Relais für den Schaltplan ebenfalls voll kompatibel. Beachte, dass es nur für nicht induktive Lasten und ausschließlich für Wechselspannungslasten geeignet ist, da der Gleichstromkreis nach dem Schalten nicht geöffnet werden kann. Die maximale Schaltleistung beträgt 460 W (230 V, 2 A).
Der Thermostat kann das ganze Jahr über verwendet werden. Im Falle einer unnötigen Steuerung kann der Ausgang physisch getrennt werden, und der Thermostat kann als Ethernet-Thermometer verwendet werden, um Daten aus dem Raum zu erhalten, in dem er sich befindet.Das Webinterface für den Ethernet-Thermostat ermöglicht:
Zeigen Sie in Echtzeit die Temperatur des DS18B20-Sensors auf dem OneWire-Bus, die Betriebszeit des Geräts, den Ausgangsstatus mit dynamischer Änderung, die aktuell eingestellten Konfigurationsdaten für den Thermostat an, d.h. Zieltemperatur und Hysterese aus EEPROM
Ändern Sie die Zieltemperatur (Referenztemperatur) im Bereich von 5 bis 50 °C in Schritten von 0,25 °C
Hysterese ändern im Bereich 0 bis 10 °C in 0,25 °C-Schritten
Kesselregelung ON/OFF:
Beispiel EIN/AUS-Heizungssteuerung - VISUALISIERUNG IST NICHT TEIL DES PROJEKTS
Der Boiler ist aktiv, bis die Zieltemperatur + Hysterese erreicht ist
Die Visualisierung von Wassertemperaturen zeigt die sog Heizungshochlauf und anschließende Abkühlung des Wassers bis zur Wiederaktivierung der Heizung, wenn die gemessene Temperatur unter der eingestellten Solltemperatur liegt - Hysterese
Die Weboberfläche wurde so gestaltet, dass sie sich an verschiedene Bildschirmgrößen anpasst und somit responsiv ist. Sie unterstützt sowohl hochauflösende Widescreens als auch mobile Endgeräte. Die Schnittstelle verwendet importierte Bootstrap-Framework-CSS-Stile von einem externen CDN-Server, der beim Öffnen einer Seite auf dem ESP32 das entsprechende Stylesheet clientseitig lädt.
Um sicherzustellen, dass die eingestellten Werte des Thermostats auch nach einem Stromausfall erhalten bleiben, werden diese im EEPROM-Speicher des ESP gespeichert. Da die Plattform keinen physischen EEPROM-Chip besitzt, wird der EEPROM-Speicher im Flash-Speicher emuliert. Die Referenztemperatur liegt bei Offset 10, die Hysterese bei Offset 100. Jeder dieser Werte belegt maximal 5 Byte im EEPROM-Speicher, zusätzlich zum Abschlusszeichen.
Die Daten werden erst beim Absenden des HTML-Formulars überschrieben. Der Betrieb des Thermostats ist daher maximal schonend für den EEPROM-Speicher, um seine maximale Lebensdauer zu gewährleisten. Der Zustand des Ausgangs existiert ausschließlich im RAM-Speicher, wo er bei Änderungen überschrieben wird. Der Wert wird nicht im emulierten EEPROM-Speicher im Flash-Speicher gespeichert.
Durch die Verwendung des Refresh-Meta-Tags aktualisiert der Webserver alle 30 Sekunden die gesamte Seite. Zusätzlich wird per JavaScript eine ungefähre Refresh-Zeit in die HTML-Seite eingefügt. Bis zu diesem Zeitpunkt müssen Änderungen für das Thermostat protokolliert werden, da andernfalls die Eingabefenster für numerische Eingaben im Formular beim Aktualisieren der Seite zurückgesetzt werden. Aufgrund des Feedbacks von Benutzern von Android-Geräten wurde die Aktualisierungszeit von 10 auf 30 Sekunden verlängert.
Die dynamischen Daten, die sich hauptsächlich ändern, beziehen sich auf den aktuellen Wert der Ausgabe - EIN / AUS, der zusammen mit der Farbmarkierung den Benutzer über den aktuellen Zustand der Ausgabe informiert. Da die Logik des Systems unabhängig vom Webserver ausgeführt wird, kann die Ausgabe bereits vor dem Refresh in einem anderen Zustand sein, als er in der Webanwendung angezeigt wird. Eine Änderung der Ausgabe wird sofort, beispielsweise auf dem UART-Monitor mit einer Baudrate von 115200 Baud, ausgegeben.
Auf der Website des Thermostats findet der Benutzer auch Informationen über die Betriebszeit des Geräts, angegeben in Tagen, Stunden, Minuten und Sekunden, um zu verfolgen, wie lange es bereits in Betrieb ist.
Der Autor des Ethernet-Thermostats übernimmt keine Verantwortung für die Funktionalität des Thermostats, Ausfälle des Kessels oder Stromschläge aufgrund einer unsachgemäßen Installation des Thermostats im Netzwerk.
Hauptseite zur Änderung von Zieltemperatur und Hysterese - Vorschau des eingeschalteten Ausgangs:
Beispieldaten
Zieltemperatur: 22,75 °C
Hysterese: 0,25 °C
Messdaten: 22,49 °C
Ausgabe: Ein
Der Thermostat heizt ab einer gemessenen Temperatur von 22,49 °C und darunter.
Erreicht die Temperatur 23,01 °C und mehr, wird der Ausgang abgeschaltet, das Melderelais abgeschaltet und der Gaskessel heizt nicht mehr.
Die Beheizung und Kühlung des Raumes, in dem die Messungen durchgeführt werden, erfolgt.
Der Thermostat wird erst wieder aktiviert, wenn die Temperatur 22,49 °C oder weniger erreicht.
Voll funktionsfähiger Ethernet-Thermostat mit der Möglichkeit, Datenkontrollen einzustellen:
Aktualisieren Sie die Weboberfläche automatisch alle 30 Sekunden
Hauptseite zur Änderung von Zieltemperatur und Hysterese / Auswahl des manuellen Modus mit EIN/AUS-Steuerungsoption:Bearbeitungsfortschritt der eingegebenen Daten (Nutzerumleitung):JSON-Ausgabe des Webservers im Browser / Client via Websocket:
Ausgabe an UART-Monitor - Systemlogik + eingestellte IP-Adresse:
/*|-----------------------------------------------------------|*/
/*|HTTP webserver - Ethernet thermostat - ESP32 + PHY LAN8720 |*/
/*|Project webpage: |*/
/*|https://martinius96.github.io/termostat-ethernet/en |*/
/*|AUTHOR: Martin Chlebovec |*/
/*|EMAIL: martinius96@gmail.com |*/
/*|DONATE: paypal.me/chlebovec |*/
/*|-----------------------------------------------------------|*/
#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 cas = 0;
String stav = "OFF";
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
#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 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', '6beae97f98b9844b761672af23f38fc60b962338');");
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 = \"Refreshing...\";");
stranka += F("} else {");
stranka += F("document.getElementById(\"countdown\").innerHTML = timeleft + \" seconds to refresh\";");
stranka += F("}");
stranka += F("timeleft -= 1;");
stranka += F("}, 1000);");
stranka += F("</script>");
stranka += F("<title>Ethernet thermostat - ESP32 + PHY Ethernet</title>");
stranka += F("</head>");
stranka += F("<body>");
stranka += F("<center><h3>Enter datas for web server:</h3>");
stranka += F("<form action='/action.html' method='post'>");
stranka += "<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>";
stranka += "<b>Hysteresis:</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='Send'>");
stranka += F("</form><hr>");
if (stav == "ON") {
stranka += F("<b><font color='green'>Output: ON</font></b>");
}
if (stav == "OFF") {
stranka += F("<b><font color='red'>Output: OFF</font></b>");
}
stranka += F("<div id=\"countdown\"></div>");
stranka += F("<b>Current sensor DS18B20 temperature:</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>Author: Martin Chlebovec - martinius96@gmail.com - https://martinius96.github.io/termostat-ethernet/en/</h3>");
stranka += F("<h4>Free version - 1.0.4 build: 22. Aug. 2022</h4>");
stranka += F("</center>");
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("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 hystereza = hysteresis.toFloat();
writeString(100, hystereza);
} else {
Serial.println(F("No number was entered in the hysteresis input field!"));
Serial.println(F("Datas will be not stored in EEPROM!"));
}
}
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>Ethernet thermostat - ESP32 - data processing</title>");
stranka += F("</head>");
stranka += F("<body>");
stranka += F("<center><h3>Server received datas from HTML form:</h3>");
stranka += "<li><b>Target temperature: </b>" + String(read_String(10)) + " °C</li>";
stranka += "<li><b>Hysteresis: </b>" + String(read_String(100)) + " °C</li>";
stranka += F("<b>Redirecting... Please wait</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 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() - cas) >= 10000 || cas == 0) {
cas = millis();
teplota = 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(teplota));
Serial.println(F(" °C"));
sensorsA.requestTemperatures();
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("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);
}
}
server.handleClient();
yield();
}
Die Implementierung enthält Programme für die Zuweisung einer dynamischen IPv4-Adresse an das Ethernet-Shield.
Der Thermostat ist ausschließlich für Innentemperaturen ausgelegt! (über 0°C), und die Logik des Systems ist entsprechend darauf abgestimmt.
Das Thermostat kann genutzt werden, um ein bestehendes Raumthermostat zu ersetzen oder temporär eine Heizung in einem Aquarium/Terrarium zu übernehmen, um eine konstante Temperatur aufrechtzuerhalten.