Bu rehberde, ESP8266 NodeMCU Wi-Fi modülü ve OpenTherm Adaptör kartı kullanılarak geliştirilen akıllı kombi kontrol (Wi-Fi Termostat) sisteminin donanım bağlantıları ve yazılım altyapısı incelenmektedir. ESP8266'nın dahili Wi-Fi yeteneği sayesinde kombi uzaktan akıllı telefon veya web arayüzü üzerinden kontrol edilebilir ve anlık veriler takip edilebilir. OpenTherm Adaptör kartı detayı tıklayarak detaylara ulaşabilirsiniz.

Görseldeki devre şemasına göre ESP8266 NodeMCU, OpenTherm Adaptör ve Kombi arasındaki bağlantı haritası şu şekildedir:
| Bileşen / Hat | Adaptör / Kombi Terminali | ESP8266 NodeMCU Karşılığı | Açıklama / Kablo Rengi |
|---|---|---|---|
| Güç ve Toprak | 3V / VCC | 3V3 | 3.3V Güç Beslemesi (Kırmızı Kablo) |
| GND | G (GND) | Toprak Hattı (Siyah Kablo) | |
| IN (Interrupt) | D1 (GPIO 5) | Veri Giriş / Kesme Hattı (Mavi Kablo) | |
| Sinyal / Kontrol | OUT | D2 (GPIO 4) | Veri Çıkış Hattı (Yeşil Kablo) |
| Kombi Bağlantısı (OT) | OT1 ve OT2 | Kombi Termostat Çıkışı | Çift Damarlı Kablo (Mavi Kablolar) |
Arduino oduna OTA wifi port üzerinden program yüklemede eklidir. İlk yüklemede seri porttan yükleyip sonrasında wifi üzerinden yazılım atılabilir. Programlama portu resimdeki gibi seçilmelidir.
Aşağıdaki C++ kodu, ESP8266 kullanarak kombi durumunu okuyan ve OpenTherm protokolü üzerinden hedef sıcaklığı ayarlayan temel bir uygulama örneğidir:
opentherm_wifi.ino
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <ArduinoOTA.h>
#include <EEPROM.h>
#include "web.h"
#include <OpenTherm.h>
// OpenTherm input and output pins
const int inPin = 4;
const int outPin = 5;
// --- Wi-Fi AYARLARI ---
const char* ssid = "YENIDIR_WIFI";
const char* password = "WIFI_SIFRENIZ";
ESP8266WebServer server(80);
OpenTherm ot(inPin, outPin);
// Kontrol and İzleme Değişkenleri
float boilerTemp = 40.0;
float dhwTemp = 45.0;
float currentWaterTemp = 0.0;
float activeBoilerSet = 0.0;
float activeDhwSet = 0.0;
float modulation = 0.0;
bool flameStatus = false;
bool dhwActiveStatus = false;
bool centralHeatingEnabled = true;
bool dhwEnabled = true;
bool settingMode = true;
unsigned long ts = 0, new_ts = 0;
String lastErrorStr = "Yok (Normal)";
String boilerStatusStr = "Bilinmiyor";
void ICACHE_RAM_ATTR handleInterrupt() {
ot.handleInterrupt();
}
void handleRoot() {
digitalWrite(BUILTIN_LED, 1);
if (server.method() == HTTP_POST) {
for (uint8_t i = 0; i < server.args(); i++) {
if (server.argName(i) == "bt") {
boilerTemp = server.arg(i).toFloat();
boilerTemp = constrain(boilerTemp, 30.0, 80.0);
} else if (server.argName(i) == "dt") {
dhwTemp = server.arg(i).toFloat();
dhwTemp = constrain(dhwTemp, 30.0, 65.0);
} else if (server.argName(i) == "mode") {
String m = server.arg(i);
if (m == "winter") centralHeatingEnabled = true;
else if (m == "summer") centralHeatingEnabled = false;
} else if (server.argName(i) == "opmode") {
String om = server.arg(i);
if (om == "monitor") settingMode = false;
else if (om == "setting") settingMode = true;
EEPROM.write(0, settingMode ? 1 : 0);
EEPROM.commit();
}
}
}
String page = FPSTR(HTTP_HTML);
page.replace("{0}", String(boilerTemp, 1));
page.replace("{1}", String(dhwTemp, 1));
server.send(200, "text/html", page);
digitalWrite(BUILTIN_LED, 0);
}
void handleData() {
String seasonMode = centralHeatingEnabled ? "Kış Modu (Petekler Açık)" : "Yaz Modu (Petekler Kapalı)";
String chStatusStr = centralHeatingEnabled ? "Aktif (Açık)" : "Kapalı";
String flameStr = flameStatus ? "Alev Var (Yanıyor)" : "Alev Yok (Beklemede)";
String dhwStr = dhwActiveStatus ? "Musluk Açık (Isıtılıyor)" : "Kapalı (Beklemede)";
String opModeStr = settingMode ? "Ayar Modu (Tam Kontrol)" : "Monitör Modu (Salt Okunur)";
String json = "{";
json += "\"t\":" + String(currentWaterTemp, 1) + ",";
json += "\"f\":\"" + flameStr + "\",";
json += "\"dhwa\":\"" + dhwStr + "\",";
json += "\"m\":" + String(modulation, 1) + ",";
json += "\"bs\":" + String(activeBoilerSet, 1) + ",";
json += "\"ds\":" + String(activeDhwSet, 1) + ",";
json += "\"sm\":\"" + seasonMode + "\",";
json += "\"chs\":\"" + chStatusStr + "\",";
json += "\"om\":\"" + opModeStr + "\",";
json += "\"om_val\":" + String(settingMode ? 1 : 0) + ",";
json += "\"st\":\"" + boilerStatusStr + "\",";
json += "\"err\":\"" + lastErrorStr + "\"";
json += "}";
server.send(200, "application/json", json);
}
void setup(void) {
pinMode(BUILTIN_LED, OUTPUT);
digitalWrite(BUILTIN_LED, 0);
Serial.begin(115200);
EEPROM.begin(512);
byte savedMode = EEPROM.read(0);
if (savedMode == 0 || savedMode == 1) {
settingMode = (savedMode == 1);
}
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.println("");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
if (MDNS.begin("boiler-control")) {
Serial.println("MDNS responder started");
}
server.on("/", handleRoot);
server.on("/data", handleData);
server.begin();
Serial.println("HTTP server started");
ArduinoOTA.setHostname("esp8266-boiler-control");
ArduinoOTA.setPassword("admin123");
ArduinoOTA.onStart([]() {
String type = (ArduinoOTA.getCommand() == U_FLASH) ? "sketch" : "filesystem";
Serial.println("OTA Güncellemesi başlıyor: " + type);
});
ArduinoOTA.onEnd([]() {
Serial.println("\nOTA Güncellemesi tamamlandı!");
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
Serial.printf("İlerleme: %u%%\r", (progress / (total / 100)));
});
ArduinoOTA.onError([](ota_error_t error) {
Serial.printf("Hata[%u]: ", error);
if (error == OTA_AUTH_ERROR) lastErrorStr = "OTA: Yetkilendirme Hatası";
else if (error == OTA_BEGIN_ERROR) lastErrorStr = "OTA: Başlangıç Hatası";
else if (error == OTA_CONNECT_ERROR) lastErrorStr = "OTA: Bağlantı Hatası";
else if (error == OTA_RECEIVE_ERROR) lastErrorStr = "OTA: Veri Alım Hatası";
else if (error == OTA_END_ERROR) lastErrorStr = "OTA: Bitiş Hatası";
});
ArduinoOTA.begin();
Serial.println("OTA Hazır");
ts = millis();
ot.begin(handleInterrupt);
}
void loop(void) {
ArduinoOTA.handle();
server.handleClient();
new_ts = millis();
if (new_ts - ts > 2500) {
bool enableCooling = false;
unsigned long responseStatusMsg = ot.setBoilerStatus(centralHeatingEnabled, dhwEnabled, enableCooling);
OpenThermResponseStatus responseStatus = ot.getLastResponseStatus();
if (responseStatus == OpenThermResponseStatus::SUCCESS) {
boilerStatusStr = "Bağlantı Başarılı (OK)";
// Gelen ham yanıt mesajını Seri Monitör'e yazdıralım (Musluk açıkken/kapalıyken takip için)
Serial.print("OpenTherm Yanıt Mesajı (HEX): 0x");
Serial.println(responseStatusMsg, HEX);
if (settingMode) {
ot.setBoilerTemperature(boilerTemp);
ot.setDHWSetpoint(dhwTemp);
activeBoilerSet = boilerTemp;
activeDhwSet = dhwTemp;
} else {
activeBoilerSet = boilerTemp;
activeDhwSet = dhwTemp;
}
float tempRead = ot.getBoilerTemperature();
if (tempRead > 0 && tempRead < 100) {
currentWaterTemp = tempRead;
}
float modRead = ot.getModulation();
if (modRead >= 0 && modRead <= 100) {
modulation = modRead;
}
flameStatus = ot.isFlameOn(responseStatusMsg);
// Standart OpenTherm Slave Status'te Bit 2 DHW (Musluk Suyu) aktifliğini gösterir
dhwActiveStatus = (responseStatusMsg & (1 << 2)) != 0;
lastErrorStr = "Yok (Normal)";
} else {
boilerStatusStr = "İletişim Hatası!";
if (responseStatus == OpenThermResponseStatus::NONE) lastErrorStr = "OpenTherm: Yanıt Alınamadı (NONE)";
else if (responseStatus == OpenThermResponseStatus::INVALID) lastErrorStr = "OpenTherm: Geçersiz Veri (0x" + String(responseStatusMsg, HEX) + ")";
else if (responseStatus == OpenThermResponseStatus::TIMEOUT) lastErrorStr = "OpenTherm: Zaman Aşımı (TIMEOUT)";
}
ts = new_ts;
}
}
web.h#ifndef WEB_H
#define WEB_H
#include <Arduino.h>
const char HTTP_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kombi Uzaktan Kontrol</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; background-color: #f4f4f9; margin: 0; padding: 20px; }
.card { background: white; max-width: 420px; margin: 0 auto; padding: 20px; border-radius: 12px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
h2 { color: #333; }
.box { background: #eef2f7; padding: 12px; border-radius: 8px; margin: 10px 0; text-align: left; font-size: 15px; }
.info { font-size: 15px; color: #555; margin: 8px 0; }
.error-box { background: #ffe6e6; border: 1px solid #ff4d4d; color: #cc0000; padding: 10px; border-radius: 6px; margin-top: 15px; font-size: 14px; text-align: left; }
input[type=number] { width: 70px; padding: 6px; font-size: 16px; text-align: center; }
input[type=submit] { padding: 8px 14px; font-size: 14px; background-color: #007BFF; color: white; border: none; border-radius: 5px; cursor: pointer; }
input[type=submit]:hover { background-color: #0056b3; }
.btn-mode { padding: 8px 10px; font-size: 13px; border: none; border-radius: 5px; cursor: pointer; color: white; text-decoration: none; display: inline-block; }
.btn-winter { background-color: #d9534f; }
.btn-summer { background-color: #f0ad4e; }
.btn-monitor { background-color: #6c757d; }
.btn-setting { background-color: #28a745; }
.form-row { display: flex; justify-content: space-between; align-items: center; margin: 8px 0; }
hr { border: 0; border-top: 1px solid #eee; margin: 15px 0; }
</style>
<script>
function updateData() {
fetch('/data')
.then(response => response.json())
.then(data => {
document.getElementById('op-mode').innerText = data.om;
document.getElementById('season-mode').innerText = data.sm;
document.getElementById('ch-status').innerText = data.chs;
document.getElementById('dhw-status').innerText = data.dhwa;
document.getElementById('active-bs').innerText = data.bs;
document.getElementById('active-ds').innerText = data.ds;
document.getElementById('curr-temp').innerText = data.t;
document.getElementById('flame-status').innerText = data.f;
document.getElementById('mod-rate').innerText = data.m;
document.getElementById('boiler-status').innerText = data.st;
document.getElementById('last-err').innerText = data.err;
var settingControls = document.getElementById('setting-controls');
if (data.om_val == 0) {
settingControls.style.display = 'none';
} else {
settingControls.style.display = 'block';
}
})
.catch(error => console.log('Veri alınamadı:', error));
}
setInterval(updateData, 500);
</script>
</head>
<body>
<div class="card">
<h2>Kombi Kontrol Paneli</h2>
<!-- Canlı Veriler ve Mod Kutusu -->
<div class="box">
<div><b>Kontrol Modu:</b> <span id="op-mode" style="color: #007BFF; font-weight: bold;">Yükleniyor...</span></div>
<div style="margin-top: 6px;"><b>Sezon:</b> <span id="season-mode" style="color: #333;">--</span></div>
<div style="margin-top: 6px;"><b>Isıtma Suyu (Petekler):</b> <span id="ch-status" style="font-weight: bold; color: #28a745;">--</span></div>
<div style="margin-top: 6px;"><b>Musluk Suyu (DHW):</b> <span id="dhw-status" style="font-weight: bold; color: #0275d8;">--</span></div>
<div style="margin-top: 6px;"><b>Aktif Petek Seti:</b> <span id="active-bs">--</span> °C</div>
<div style="margin-top: 6px;"><b>Aktif Musluk Seti:</b> <span id="active-ds">--</span> °C</div>
<div style="margin-top: 6px;"><b>Anlık Petek Suyu:</b> <span id="curr-temp">--</span> °C</div>
<div style="margin-top: 6px;"><b>Alev Durumu:</b> <span id="flame-status" style="color: #d9534f;">--</span></div>
<div style="margin-top: 6px;"><b>Modülasyon Oranı:</b> %<span id="mod-rate">--</span></div>
</div>
<hr>
<!-- Kontrol Modu Seçimi -->
<div class="form-row">
<span><b>Çalışma Modu:</b></span>
<span>
<form action="/" method="POST" style="display:inline;">
<input type="hidden" name="opmode" value="monitor">
<input type="submit" value="Monitör" class="btn-mode btn-monitor">
</form>
<form action="/" method="POST" style="display:inline; margin-left: 3px;">
<input type="hidden" name="opmode" value="setting">
<input type="submit" value="Ayar" class="btn-mode btn-setting">
</form>
</span>
</div>
<hr>
<!-- Yaz / Kış Modu Değiştirme Butonları -->
<div class="form-row">
<span><b>Sezon Seçimi:</b></span>
<span>
<form action="/" method="POST" style="display:inline;">
<input type="hidden" name="mode" value="winter">
<input type="submit" value="Kış" class="btn-mode btn-winter">
</form>
<form action="/" method="POST" style="display:inline; margin-left: 3px;">
<input type="hidden" name="mode" value="summer">
<input type="submit" value="Yaz" class="btn-mode btn-summer">
</form>
</span>
</div>
<!-- Sadece Ayar Modunda Görünen Kontroller -->
<div id="setting-controls">
<hr>
<form action="/" method="POST">
<div class="form-row">
<span><b>Petek Hedef:</b></span>
<span>
<input type="number" name="bt" value="{0}" step="1" min="30" max="80"> °C
<input type="submit" value="Gönder">
</span>
</div>
</form>
<hr>
<form action="/" method="POST">
<div class="form-row">
<span><b>Musluk Hedef:</b></span>
<span>
<input type="number" name="dt" value="{1}" step="1" min="30" max="65"> °C
<input type="submit" value="Gönder">
</span>
</div>
</form>
</div>
<hr>
<div class="info" style="text-align: left;"><b>Kombi Durumu:</b> <span id="boiler-status">Bağlanıyor...</span></div>
<div class="error-box">
<b>Son Hata / Uyarı:</b><br>
<span id="last-err">Yok</span>
</div>
</div>
</body>
</html>
)rawliteral";
#endif
ip numarasından web kontrol ve görüntülemeye girince aşağıdaki gibi bir sayfa ile karşlaşacağız.

Henüz yorum yapılmamış. İlk yorumu siz yapın!