ESP8266 ile OpenTherm Üzerinden Warmhaus Ewa Kombi Kontrolü

👤 Yazar: admin 📅 Tarih: 05.11.2026 17:27 👁️ Görüntüleme: 1

ESP8266 NodeMCU ile OpenTherm Üzerinden Warmhaus Ewa Kombi Kontrolü

Bu yazıda, ESP8266 NodeMCU Wi-Fi modülü ve OpenTherm Adaptör kartı kullanılarak geliştirilen akıllı kombi kontrol (Wi-Fi Termostat) sistemini warmhaus ewa kombide test edilmiş uygulamasını yapacağız. ESP8266'nın dahili Wi-Fi yeteneği sayesinde kombinin genel kontrolleri web üzerinden yapılabilir. İstenirse bir tane sıcaklık sensörü eklenerek oda termostatı gibi düzenlenebilir. Sensör eklendiğinde istenen ısı değerine ulaşınca kombinin modunu kıştan yaza alarak enerji tasarrufu sağlanabilir. OpenTherm Adaptör kartı detayı tıklayarak detaylara ulaşabilirsiniz. Sistem ilk açılışta kış modunda ilk verileri okuyup kapalı modda başlamaktadır. Farklı marka ve model kombilerde kullanırken sıkıntı yaşarsanız status mesajındaki moda göre d5 ve d6 gönderimini 0 yapabilirsiniz. warmhaus kombide bu bitleri moda göre göndermez isek ısıtma ve sıcak su kullanımını kapatamayız. 

ESP8266 NodeMCU ve OpenTherm Adaptör Bağlantı Şeması


1. Donanım Pin Eşleşme Tablosu

Görseldeki devre şemasına göre ESP8266 NodeMCU, OpenTherm Adaptör ve Warmhaus ewa 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)
ESP8266 Mantık Seviyesi Notu:
ESP8266 NodeMCU kartı 3.3V mantık seviyelerinde çalışır. OpenTherm adaptör kartı 3.3V uyumlu olduğundan doğrudan ESP8266 besleme ve GPIO pinlerine güvenle bağlanabilir. Bazı warmhaus ewa kombilerde opentherm ve termostat köprüsü olması gereken yerde klemens şeklinde  bulunmamaktadır. Anakarttaki ilgili sokete kablo ekleyerek kendimiz yapmalıyız.

2. C++ ESP8266 Arduino Kodu (Wi-Fi Entegrasyonlu OpenTherm Kontrolü)

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.

program portu 

Aşağıdaki C++ kodu, ESP8266 kullanarak kombi durumunu okuyan ve OpenTherm protokolü üzerinden hedef sıcaklığı ayarlayan temel bir uygulama örneğidir. Kütüphane olarak easyopentherm kullanıldı.

C++ / ESP8266 OpenTherm Kombi kontrol Örnek Kod

easyopentherm_wifi.ino

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <ArduinoOTA.h>
#include "web.h"
#include <EasyOpenTherm.h>

const int inPin = 4;
const int outPin = 5;

const char* ssid = "YENIDIR_WIFI";
const char* password = "WIFI_SIFRENIZ";

const char* webUser = "admin";
const char* webPass = "admin123";

ESP8266WebServer server(80);

#define OT_TIMEOUT_MS 800   
#define OT_NO_SLEEP 0       
#define OT_STATUS_RETRY 1   
OpenTherm ot(inPin, outPin, OT_TIMEOUT_MS);

// Kontrol ve İzleme Değişkenleri
float boilerTemp = 40.0;
float dhwTemp = 45.0;
float dhwMin = 35.0;
float dhwMax = 60.0;

float dhwBoundMin = 35.0;
float dhwBoundMax = 60.0;
bool dhwBoundsSynced = false;
byte boundsSyncTries = 0;

float petekMin = 25.0;
float petekMax = 80.0;
bool petekBoundsSynced = false;
byte petekBoundsTries = 0;
String dhwBoundStr = "--";
float currentWaterTemp = 0.0;
bool tempValid = false;
int tempId = 25;  
String tempInfoStr = "--";
float activeBoilerSet = 0.0;
float activeDhwSet = 0.0;
float modulation = 0.0;
bool modValid = false;       
bool statusValid = false;    
bool faultBit = false;       
bool diagBit = false;        
bool prevFault = false;
String faultDetail = "";     

const byte STALE_FAILS = 2;
byte failFlow = 0, failReturn = 0, failTSet = 0, failDhw = 0, failMod = 0, failPress = 0, failDhwT = 0;
bool pumpOn = false;
bool flameStatus = false;
bool dhwActiveStatus = false;
bool chActiveStatus = false;

// İlk açılışta okumaları yapabilmek için geçici olarak true başlar, senkron bitince kapalıya dönecek
bool centralHeatingEnabled = true;
bool prevCentralHeatingEnabled = true;
bool dhwEnabled = true;
bool initialOffApplied = false; // Senkron sonrası kapalı moda geçildi mi bayrağı

uint8_t id0Extra = 0; 

bool settingMode = true;
bool initialSyncDone = false;
bool tSetSynced = false;
bool dhwSynced = false;
bool pressureSynced = false;   
bool returnTempSynced = false; 
bool dhwTempSynced = false;    
bool flowTempSynced = false;   
byte syncAttempts = 0;

bool tsetOncePending = false;
float tsetOnceVal = 0.0;
bool bPending = false;   
bool dPending = false;   
byte petekTries = 0, petekReadFails = 0, dhwTries = 0, dhwReadFails = 0;
bool petekAck = false, dhwAck = false;
float lastSentBoilerTemp = -99.0;
float lastSentDhwTemp = -99.0;

unsigned long ts = 0;
String lastErrorStr = "OpenTherm başlatılmadı (web sayfasından OpenTherm: Açık'a bas)";
String boilerStatusStr = "Bilinmiyor";
String lastCommandStr = "Henüz komut alınmadı";
String tsetReplyStr = "--";
String statusRawStr = "--";
String idZeroStr = "--";  

float summerTSet = 25.0;
bool skipTSetInSummer = true;
bool pseudoSummer = false;

const int EVT_MAX = 12;
String evtLog[EVT_MAX];
int evtCount = 0;
bool prevFlame = false;
bool prevCh = false;
bool prevDhw = false;
bool prevCommOk = true;
int statusFailStreak = 0;
bool mdnsStarted = false;
bool otPaused = false;           
unsigned long restartAt = 0;     
bool statusRetry = false;  
unsigned long statusTotal = 0;
unsigned long statusFails = 0;
unsigned long failTimeout = 0;
unsigned long failParity = 0;
unsigned long failOther = 0;
unsigned long lastDataMs = 0;  
unsigned long nearTotal = 0, nearFail = 0, farTotal = 0, farFail = 0;
unsigned long retryTotal = 0;
unsigned long retryFails = 0;

String tsetReadStr = "--";
String tsetNumStr = "--";      
String dhwReadNumStr = "--";   
int watchId = 0;
String pressureStr = "--";     
String returnTempStr = "--";   
String dhwTempStr = "--";      
bool chEnableNow = false;  
String watchStr = "--";

bool scanning = false;
int scanId = 0;
int scanMode = 0;
int scanMax = 255;
String scanResult = "";

const char* errName(OpenTherm::ERROR_CODES e);

const int OTL_MAX = 20;
String otLog[OTL_MAX];
int otLogHead = 0;
int otLogCount = 0;
bool otLogSuppress = false;  

String otIdName(int id) {
  switch (id) {
    case 0: return " Status";
    case 1: return " TSet";
    case 11: return " TSP";
    case 13: return " FHB";
    case 16: return " TrSet";
    case 17: return " Modülasyon";
    case 18: return " CH Basıncı";
    case 24: return " Tr";
    case 25: return " Tpetek";
    case 26: return " Tmusluk";
    case 28: return " Tdönüş";
    case 56: return " DHWSet";
    case 57: return " MaxTSet";
    default: return "";
  }
}

void otLogAdd(const String& s) {
  if (otLogSuppress) return;
  otLog[otLogHead] = "[" + String(millis() / 1000.0, 1) + "] " + s;
  otLogHead = (otLogHead + 1) % OTL_MAX;
  if (otLogCount < OTL_MAX) otLogCount++;
}

String otLogHtml() {
  String o;
  for (int i = 0; i < otLogCount; i++) {
    int idx = (otLogHead - 1 - i + OTL_MAX * 2) % OTL_MAX;
    if (i) o += "<br>";
    o += otLog[idx];
  }
  return o;
}

bool readF(int id, float& val) {
  float tmp = 0;
  bool ok = ot.read((OpenTherm::READ_DATA_ID)id, tmp) && ot.error() == OpenTherm::ERROR_CODES::OK;
  otLogAdd("OKU  ID" + String(id) + otIdName(id) + (ok ? String(" = ") + String(tmp, 2) : String(" -> ") + errName(ot.error())));
  if (!ok) return false;
  val = tmp;
  return true;
}

const char* errName(OpenTherm::ERROR_CODES e) {
  switch (e) {
    case OpenTherm::ERROR_CODES::OK: return "OK";
    case OpenTherm::ERROR_CODES::UNKNOWN_DATA_ID: return "ID desteklenmiyor";
    case OpenTherm::ERROR_CODES::INVALID_DATA: return "geçersiz veri";
    case OpenTherm::ERROR_CODES::SEND_TIMEOUT: return "gönderim zaman aşımı";
    case OpenTherm::ERROR_CODES::RECEIVE_TIMEOUT: return "yanıt zaman aşımı";
    case OpenTherm::ERROR_CODES::PARITY_ERROR: return "parity hatası";
    default: return "bilinmeyen hata";
  }
}

String hex4(uint16_t v) {
  String h = String(v, HEX);
  h.toUpperCase();
  while (h.length() < 4) h = "0" + h;
  return h;
}

bool readRaw(int id, uint16_t& val) {
  uint16_t tmp = 0;
  bool ok = ot.read((OpenTherm::READ_DATA_ID)id, tmp) && ot.error() == OpenTherm::ERROR_CODES::OK;
  otLogAdd("OKU  ID" + String(id) + otIdName(id) + (ok ? String(" = 0x") + hex4(tmp) : String(" -> ") + errName(ot.error())));
  if (!ok) return false;
  val = tmp;
  return true;
}

bool writeF(int id, float val) {
  bool ok = ot.write((OpenTherm::WRITE_DATA_ID)id, val) && ot.error() == OpenTherm::ERROR_CODES::OK;
  otLogAdd("<b>YAZ</b> ID" + String(id) + otIdName(id) + " = " + String(val, 2) + (ok ? String(" -> OK") : String(" -> ") + errName(ot.error())));
  return ok;
}

bool sendTSet(float t) {
  if (writeF(1, t)) {  
    tsetReplyStr = "Gönderilen " + String(t, 0) + " C -> Başarılı";
    return true;
  }
  tsetReplyStr = "Gönderilen " + String(t, 0) + " C -> Yanıt yok / reddedildi";
  return false;
}

void readDhwBounds() {
  uint16_t raw = 0;
  if (readRaw(48, raw)) {
    int hi = (int8_t)(raw >> 8);
    int lo = (int8_t)(raw & 0xFF);
    if (lo > 0 && hi > lo && hi <= 100) {
      dhwBoundMin = lo;
      dhwBoundMax = hi;
      dhwBoundStr = String(lo) + " - " + String(hi);
      if (!dhwBoundsSynced) {
        dhwMin = lo;
        dhwMax = hi;
        dhwTemp = constrain(dhwTemp, dhwMin, dhwMax);
      }
      dhwBoundsSynced = true;
      return;
    }
  }
  if (!dhwBoundsSynced && boundsSyncTries >= 3) dhwBoundStr = "okunamadı (varsayılan " + String(dhwBoundMin, 0) + " - " + String(dhwBoundMax, 0) + ")";
}

void readPetekBounds() {
  uint16_t raw = 0;
  if (readRaw(49, raw)) {
    int hi = (int8_t)(raw >> 8);
    int lo = (int8_t)(raw & 0xFF);
    if (lo > 0 && hi > lo && hi <= 100) {
      petekMin = lo;
      petekMax = hi;
      petekBoundsSynced = true;
      boilerTemp = constrain(boilerTemp, petekMin, petekMax);
    }
  }
}

bool staleNow(byte &c) {
  if (c < 255) c++;
  return c >= STALE_FAILS;
}

String faultText(uint16_t raw) {
  uint8_t f = raw >> 8;
  uint8_t code = raw & 0xFF;
  String r;
  if (f & 0x01) r += "Servis talebi, ";
  if (f & 0x02) r += "Reset ile kilit açılabilir, ";
  if (f & 0x04) r += "Düşük su basıncı, ";
  if (f & 0x08) r += "Gaz/alev arızası, ";
  if (f & 0x10) r += "Hava basıncı arızası, ";
  if (f & 0x20) r += "Su aşırı sıcaklık, ";
  r += "OEM kod: " + String(code);
  return r;
}

bool checkAuth() {
  if (server.authenticate(webUser, webPass)) return true;
  server.requestAuthentication(BASIC_AUTH, "Kombi Kontrol");
  return false;
}

void handleSummerCfg() {
  if (!checkAuth()) return;
  if (server.hasArg("tset")) summerTSet = constrain(server.arg("tset").toFloat(), 0.0, 80.0);
  if (server.hasArg("notset")) skipTSetInSummer = (server.arg("notset").toInt() != 0);
  if (server.hasArg("pseudo")) pseudoSummer = (server.arg("pseudo").toInt() != 0);
  server.send(200, "text/plain; charset=utf-8",
              "Yaz/Kapalı TSet = " + String(summerTSet, 0) + " C\nCH-enable yaz modunda: " + String(pseudoSummer ? "AÇIK (pseudo)" : "kapalı") + "\nYaz/Kapalı modunda TSet gönderimi: " + String(skipTSetInSummer ? "YOK (Güvenli)" : "VAR") + "\nKullanım: /summer?notset=0|1&tset=25&pseudo=0|1");
}

void handleTsetOnce() {
  if (!checkAuth()) return;
  String msg;
  if (!server.hasArg("t")) {
    msg = "Kullanım: /tset?t=45  (TSet'i kombiye bir kez gönderir, 0-80 °C)";
  } else if (!settingMode) {
    msg = "Reddedildi: Monitör modunda komut verilemez (Ayar moduna geç)";
  } else {
    tsetOnceVal = constrain(server.arg("t").toFloat(), 0.0, 80.0);
    tsetOncePending = true;
    lastCommandStr = "Tek seferlik TSet: " + String(tsetOnceVal, 0) + " C";
    msg = "TSet " + String(tsetOnceVal, 0) + " C kuyruğa alındı, bir sonraki turda (en geç ~1 sn) bir kez gönderilir.\nSonucu ana sayfadaki OpenTherm Trafiği kutusunda gör.";
  }
  server.send(200, "text/plain; charset=utf-8", msg);
}

void handleTempId() {
  if (!checkAuth()) return;
  if (server.hasArg("id")) {
    tempId = constrain(server.arg("id").toInt(), 1, 255);
    tempValid = false;
    tempInfoStr = "--";
  }
  server.send(200, "text/plain; charset=utf-8",
              "Anlık petek suyu ID: " + String(tempId) + "\nKullanım: /tempid?id=25  (varsayılan 25; denenebilir: 28 dönüş suyu, 26 musluk suyu)");
}

void handleWatch() {
  if (!checkAuth()) return;
  if (server.hasArg("id")) {
    watchId = constrain(server.arg("id").toInt(), 0, 255);
    watchStr = "--";
  }
  server.send(200, "text/plain; charset=utf-8",
              (watchId ? "İzlenen ID: " + String(watchId) : String("İzleme kapalı")) + "\nKullanım: /watch?id=204  (kapatmak için /watch?id=0)");
}

void logEvent(const char* what) {
  String mode = centralHeatingEnabled ? (dhwEnabled ? "KIŞ" : "SADECE PETEK") : (dhwEnabled ? "YAZ" : "KAPALI");
  String e = "[" + String(millis() / 1000) + " sn] " + what + " | mod=" + mode + (settingMode ? "/ayar" : "/monitor") + " | CH=" + String(chActiveStatus ? "aktif" : "pasif") + " | DHW=" + String(dhwActiveStatus ? "aktif" : "pasif") + " | kombi TSet=" + tsetReadStr + " | petek suyu=" + String(currentWaterTemp, 1) + " | modülasyon=" + String(modulation, 0) + " | " + tsetReplyStr;
  for (int i = EVT_MAX - 1; i > 0; i--) evtLog[i] = evtLog[i - 1];
  evtLog[0] = e;
  if (evtCount < EVT_MAX) evtCount++;
}

void handleLog() {
  if (!checkAuth()) return;
  String out = "Olay kaydı (en yeni üstte):\n\n";
  if (evtCount == 0) out += "Henüz kayıt yok.\n";
  for (int i = 0; i < evtCount; i++) out += evtLog[i] + "\n\n";
  server.send(200, "text/plain; charset=utf-8", out);
}

void handleRoot() {
  if (!checkAuth()) return;
  digitalWrite(LED_BUILTIN, LOW);

  if (server.method() == HTTP_POST) {
    for (uint8_t i = 0; i < server.args(); i++) {
      String name = server.argName(i);
      String val = server.arg(i);

      if (!settingMode) {
        lastCommandStr = "Reddedildi: Monitör modunda komut verilemez";
        continue;
      }

      if (name == "bt" && val.length() > 0) {
        boilerTemp = constrain(val.toFloat(), petekBoundsSynced ? petekMin : 25.0f, petekBoundsSynced ? petekMax : 80.0f);
        bPending = true;
        petekTries = 0; petekReadFails = 0; petekAck = false;
        lastCommandStr = "Petek Seti: " + String(boilerTemp, 0) + " C";
      } else if (name == "dt" && val.length() > 0) {
        dhwTemp = constrain(val.toFloat(), dhwMin, dhwMax);
        dPending = true;
        dhwTries = 0; dhwReadFails = 0; dhwAck = false;
        lastCommandStr = "Musluk Seti: " + String(dhwTemp, 0) + " C";
      } else if (name == "reset") {
        restartAt = millis() + 800;  
        lastCommandStr = "ESP yeniden başlatılıyor...";
        logEvent("ESP RESET (kullanıcı)");
      } else if (name == "mode") {
        if (val == "winter") {
          centralHeatingEnabled = true;   
          dhwEnabled = true;
          id0Extra &= ~(0x20 | 0x40);     
          lastCommandStr = "Mod: Kış Seçildi";
        } else if (val == "summer") {
          centralHeatingEnabled = false;  
          dhwEnabled = true;
          id0Extra |= 0x20;               
          id0Extra &= ~0x40;              
          lastCommandStr = "Mod: Yaz Seçildi";
        } else if (val == "off") {
          centralHeatingEnabled = false;  
          dhwEnabled = false;
          id0Extra |= (0x20 | 0x40);      
          lastCommandStr = "Mod: Kapalı Seçildi";
        }
      }
    }

    digitalWrite(LED_BUILTIN, HIGH);
    server.sendHeader("Location", "/", true);
    server.send(303, "text/plain", "");
    return;
  }

  String page = FPSTR(HTTP_HTML);
  page.replace("{0}", String(boilerTemp, 0));
  page.replace("{1}", String(dhwTemp, 0));
  page.replace("{2}", String(dhwMin, 0));
  page.replace("{3}", String(dhwMax, 0));

  server.send(200, "text/html; charset=utf-8", page);
  digitalWrite(LED_BUILTIN, HIGH);
}

void startScan(int mode, int first, int last) {
  scanResult = "";
  scanMode = mode;
  scanId = first;
  scanMax = last;
  scanning = true;
  server.sendHeader("Location", "/scanresult", true);
  server.send(303, "text/plain", "");
}

void handleScanStart() { if (!checkAuth()) return; startScan(0, 1, 255); }
void handleTspStart() { if (!checkAuth()) return; startScan(1, 0, 63); }
void handleFhbStart() { if (!checkAuth()) return; startScan(2, 0, 15); }

void handleScanResult() {
  if (!checkAuth()) return;
  String out = FPSTR(HTTP_SCAN_HTML);
  out.replace("{refresh}", scanning ? "<meta http-equiv='refresh' content='3'>" : "");
  out.replace("{status}", scanning ? ("Taranıyor... " + String(scanId) + " / " + String(scanMax)) : "Tarama bitti (veya başlatılmadı). Yeniden: <a href='/scan'>/scan</a> | <a href='/tsp'>/tsp</a> | <a href='/fhb'>/fhb</a>");
  out.replace("{result}", scanResult);
  server.send(200, "text/html; charset=utf-8", out);
}

int scanTry = 0;

bool scanOne(int idx) {
  bool ok;
  String line;
  if (scanMode == 0) {
    float val = 0;
    ok = readF(idx, val);
    if (ok) line = "ID " + String(idx) + " = " + String(val, 2);
  } else {
    uint8_t lsb = 0;
    OpenTherm::READ_WRITE_DATA_ID rid = (scanMode == 1) ? OpenTherm::READ_WRITE_DATA_ID::TSP_COMMAND
                                                         : OpenTherm::READ_WRITE_DATA_ID::FAULT_BUFFER_DATA;
    ok = ot.readWrite(rid, (uint8_t)idx, lsb) && ot.error() == OpenTherm::ERROR_CODES::OK;
    if (ok) line = String(scanMode == 1 ? "TSP[" : "FHB[") + String(idx) + "] = " + String(lsb);
  }

  if (ok) {
    scanResult += line + "\n";
    scanTry = 0;
    return true;
  }

  OpenTherm::ERROR_CODES e = ot.error();
  bool transient = (e == OpenTherm::ERROR_CODES::RECEIVE_TIMEOUT || e == OpenTherm::ERROR_CODES::PARITY_ERROR ||
                    e == OpenTherm::ERROR_CODES::SEND_TIMEOUT);
  if (transient && scanTry < 2) {
    scanTry++;
    return false;  
  }
  scanTry = 0;

  if (scanMode != 0 || e != OpenTherm::ERROR_CODES::UNKNOWN_DATA_ID) {
    String name = (scanMode == 0) ? "ID " + String(idx) : String(scanMode == 1 ? "TSP[" : "FHB[") + String(idx) + "]";
    scanResult += name + " -> " + errName(e) + "\n";
  }
  return true;
}

void scanStep() {
  if (!scanning) return;
  otLogSuppress = true;
  bool scanDone = scanOne(scanId);
  otLogSuppress = false;
  if (scanDone) scanId++;
  if (scanId > scanMax) {
    scanning = false;
    scanResult += "\n--- TARAMA TAMAMLANDI ---\n";
  }
}

void handleData() {
  if (!checkAuth()) return;
  lastDataMs = millis();
  String seasonMode;
  if (centralHeatingEnabled && dhwEnabled) {
    seasonMode = "Kış Modu (Petekler ve Musluk Açık)";
  } else if (!centralHeatingEnabled && dhwEnabled) {
    seasonMode = "Yaz Modu (Sadece Musluk Açık)";
  } else if (centralHeatingEnabled && !dhwEnabled) {
    seasonMode = "Sadece Petek (CH=1, DHW=0)";
  } else {
    seasonMode = "Kapalı (Her İkisi de Kapalı)";
  }

  String flameStr = !statusValid ? String("--") : (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 chStr = chActiveStatus ? "Petek Isıtma Aktif" : "Petek Isıtma Pasif";

  String faultStr;
  if (!statusValid) faultStr = "--";
  else if (faultBit) faultStr = String("ARIZA VAR! ") + (faultDetail.length() ? faultDetail : String("(ayrıntı okunmuyor)")) + (diagBit ? " | Teşhis bayrağı aktif" : "");
  else faultStr = diagBit ? "Yok (teşhis bayrağı aktif)" : "Yok";

  String json = "{";
  json += "\"t\":" + (tempValid ? String(currentWaterTemp, 1) : String("null")) + ",";
  json += "\"f\":\"" + flameStr + "\",";
  json += "\"cha\":\"" + chStr + "\",";
  json += "\"dhwa\":\"" + dhwStr + "\",";
  json += "\"m\":" + (modValid ? String(modulation, 1) : String("null")) + ",";
  json += "\"chon\":" + String((statusValid && chActiveStatus) ? 1 : 0) + ",";
  json += "\"dhon\":" + String((statusValid && dhwActiveStatus) ? 1 : 0) + ",";
  json += "\"flt\":\"" + faultStr + "\",";
  json += "\"fon\":" + String((statusValid && faultBit) ? 1 : 0) + ",";
  json += "\"pbs\":" + String(petekBoundsSynced ? 1 : 0) + ",\"pmn\":" + String(petekMin, 0) + ",\"pmx\":" + String(petekMax, 0) + ",\"dbs\":" + String(dhwBoundsSynced ? 1 : 0) + ",";
  json += "\"dmn\":" + String(dhwMin, 0) + ",";
  json += "\"bs\":" + String(activeBoilerSet, 1) + ",";
  json += "\"ds\":" + String(activeDhwSet, 1) + ",";
  json += "\"sm\":\"" + seasonMode + "\",";
  json += "\"om\":\"" + opModeStr + "\",";
  json += "\"om_val\":" + String(settingMode ? 1 : 0) + ",";
  json += "\"otp\":" + String(otPaused ? 1 : 0) + ",";
  json += "\"st\":\"" + boilerStatusStr + "\",";
  json += "\"tsb\":\"" + tsetReadStr + "\",";
  json += "\"tsn\":\"" + tsetNumStr + "\",";
  json += "\"dsn\":\"" + dhwReadNumStr + "\",";
  json += "\"wch\":\"" + watchStr + "\",";
  json += "\"prs\":\"" + pressureStr + "\",";
  json += "\"rt\":\"" + returnTempStr + "\",";
  json += "\"dht\":\"" + dhwTempStr + "\",";
  json += "\"raw\":\"" + statusRawStr + "\",";
  json += "\"tsr\":\"" + tsetReplyStr + "\",";
  json += "\"otl\":\"" + otLogHtml() + "\",";
  json += "\"bt\":" + String(boilerTemp, 0) + ",";
  json += "\"bts\":" + String(tSetSynced ? 1 : 0) + ",";
  json += "\"dmx\":\"" + String(dhwMax, 0) + "\",";
  json += "\"dbn\":\"" + dhwBoundStr + "\",";
  json += "\"err\":\"" + lastErrorStr + "\"";
  json += "}";

  server.send(200, "application/json; charset=utf-8", json);
}

void setup(void) {
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);
  Serial.begin(115200);

  WiFi.mode(WIFI_STA);
#if OT_NO_SLEEP
  WiFi.setSleepMode(WIFI_NONE_SLEEP);
#endif
  WiFi.setAutoReconnect(true);
  WiFi.begin(ssid, password);

  unsigned long wifiStart = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - wifiStart < 10000) {
    delay(250);
    Serial.print(".");
  }
  Serial.println();
  if (WiFi.status() == WL_CONNECTED) Serial.println(WiFi.localIP());

  server.on("/", handleRoot);
  server.on("/data", handleData);
  server.on("/scan", handleScanStart);
  server.on("/summer", handleSummerCfg);
  server.on("/log", handleLog);
  server.on("/watch", handleWatch);
  server.on("/tempid", handleTempId);
  server.on("/tset", handleTsetOnce);
  server.on("/tsp", handleTspStart);
  server.on("/fhb", handleFhbStart);
  server.on("/scanresult", handleScanResult);
  server.begin();

  ArduinoOTA.setHostname("esp8266-boiler-control");
  ArduinoOTA.setPassword("admin123");
  ArduinoOTA.begin();

  logEvent((String("AÇILIŞ (reset nedeni: ") + ESP.getResetReason() + ") - OpenTherm başlatıldı").c_str());
  ts = millis();
}

String bin8(uint8_t v) {
  String r = "0b";
  for (int i = 7; i >= 0; i--) r += ((v >> i) & 1) ? '1' : '0';
  return r;
}

String idZeroBits(uint8_t f) {
  return "MSB=" + bin8(f) + " (0x" + String(f, HEX) + ") | b0 CH=" + String(f & 0x01 ? 1 : 0) +
         ", b1 DHW=" + String(f & 0x02 ? 1 : 0) + ", b2 Soğutma=" + String(f & 0x04 ? 1 : 0) +
         ", b3 OTC=" + String(f & 0x08 ? 1 : 0) + ", b4 CH2=" + String(f & 0x10 ? 1 : 0) +
         ", b5 Yaz/Kış=" + String(f & 0x20 ? 1 : 0) + ", b6 DHW blokaj=" + String(f & 0x40 ? 1 : 0) +
         ", b7=" + String(f & 0x80 ? 1 : 0) + " | LSB=0b00000000";
}

void doStatus() {
  bool chFlag = (centralHeatingEnabled || pseudoSummer) && initialSyncDone;
  bool dhwFlag = dhwEnabled;
  chEnableNow = chFlag;

  uint8_t primaryFlags = 0;
  if (chFlag) primaryFlags |= uint8_t(OpenTherm::STATUS_FLAGS::PRIMARY_CH_ENABLE);
  if (dhwFlag) primaryFlags |= uint8_t(OpenTherm::STATUS_FLAGS::PRIMARY_DHW_ENABLE);

  primaryFlags |= (id0Extra & 0xFC);
  idZeroStr = idZeroBits(primaryFlags);
  uint8_t statusFlags = 0;
  bool statusSuccess = ot.status(primaryFlags, statusFlags);
  statusTotal++;
  if (!statusSuccess) statusFails++;
  otLogAdd("<b>STATUS</b> ID0 gönderilen " + bin8(primaryFlags) + ": CH=" + String(chFlag ? 1 : 0) + " DHW=" + String(dhwFlag ? 1 : 0) +
           (statusSuccess ? String(" -> alev=") + String((statusFlags & 0x08) ? 1 : 0) + " CH=" + String((statusFlags & 0x02) ? 1 : 0) + " DHW=" + String((statusFlags & 0x04) ? 1 : 0)
                          : String(" -> ") + errName(ot.error())));
  bool nearWeb = (millis() - lastDataMs) < 400;  
  if (nearWeb) { nearTotal++; if (!statusSuccess) nearFail++; }
  else         { farTotal++;  if (!statusSuccess) farFail++; }

  if (statusSuccess) {
    boilerStatusStr = "Bağlantı Başarılı (OK)";
    flameStatus = (statusFlags & 0x08) != 0;
    dhwActiveStatus = (statusFlags & 0x04) != 0;
    chActiveStatus = (statusFlags & 0x02) != 0;
    statusValid = true;
    faultBit = (statusFlags & 0x01) != 0;
    diagBit = (statusFlags & 0x40) != 0;
    pumpOn = chActiveStatus;  

    statusFailStreak = 0;
    statusRetry = false;
    lastErrorStr = "Yok (Normal)";
    if (!prevCommOk) { logEvent("İLETİŞİM DÜZELDİ"); prevCommOk = true; }
  } else {
    statusFailStreak++;
    #if OT_STATUS_RETRY
    statusRetry = (statusFailStreak == 1);  
#endif
    OpenTherm::ERROR_CODES fe = ot.error();
    if (fe == OpenTherm::ERROR_CODES::RECEIVE_TIMEOUT || fe == OpenTherm::ERROR_CODES::SEND_TIMEOUT) failTimeout++;
    else if (fe == OpenTherm::ERROR_CODES::PARITY_ERROR) failParity++;
    else failOther++;
    String reason = String("OpenTherm status: ") + errName(ot.error());
    if (statusFailStreak >= 2) {
      boilerStatusStr = "İletişim Hatası!";
      statusValid = false;
      lastErrorStr = reason;
      if (prevCommOk) { logEvent((String("İLETİŞİM KOPTU (") + errName(ot.error()) + ")").c_str()); prevCommOk = false; }
    } else {
      lastErrorStr = "Tek seferlik: " + reason;
    }
  }
  statusRawStr = "CH-en: " + String(chFlag) + ", DHW-en: " + String(dhwFlag) + " | " + tempInfoStr +
                 " | status hata: " + String(statusFails) + "/" + String(statusTotal) +
                 " (zaman aşımı " + String(failTimeout) + ", parity " + String(failParity) + ", diğer " + String(failOther) + ")" +
                 " | tekrar deneme: " + String(retryFails) + "/" + String(retryTotal) + " başarısız" +
                 " | web yakını: " + String(nearFail) + "/" + String(nearTotal) + ", uzak: " + String(farFail) + "/" + String(farTotal);
}

const byte J_STATUS = 1, J_FLOW = 2, J_RETURN = 3, J_DHWT = 4, J_MOD = 5, J_SYNC = 6, J_B48 = 7, J_B49 = 8;
const byte J_W1 = 9, J_V1 = 10, J_W56 = 11, J_V56 = 12, J_P18 = 13, J_R1 = 14, J_R56 = 15, J_FAULT = 16;
const byte J_WATCH = 17, J_SCAN = 18, J_TSETONCE = 19;
const unsigned long CYCLE_MS = 1000;        
const unsigned long SLOW_READ_MS = 60000;   
const byte MAX_SET_TRIES = 30;              

byte jobs[20];
byte jobCount = 0;
byte jobIdx = 0;
unsigned long cycleStart = 0;
unsigned long lastSlowRead = 0;
byte slowPending = 0;   

void addJob(byte j) {
  if (jobCount < sizeof(jobs)) jobs[jobCount++] = j;
}

void trackEvents() {
  if (flameStatus != prevFlame) {
    logEvent(flameStatus ? "ALEV YANDI" : "ALEV SÖNDÜ");
    prevFlame = flameStatus;
  }
  if (chActiveStatus != prevCh) {
    logEvent(chActiveStatus ? "PETEK (CH) AKTİF OLDU" : "PETEK (CH) PASİF OLDU");
    prevCh = chActiveStatus;
  }
  if (faultBit != prevFault) {
    logEvent(faultBit ? "ARIZA BİTİ SET OLDU" : "ARIZA BİTİ TEMİZLENDİ");
    prevFault = faultBit;
  }
  if (dhwActiveStatus != prevDhw) {
    logEvent(dhwActiveStatus ? "MUSLUK (DHW) AKTİF OLDU" : "MUSLUK (DHW) PASİF OLDU");
    prevDhw = dhwActiveStatus;
  }
}

void buildCycle() {
  jobCount = 0;

  if (centralHeatingEnabled != prevCentralHeatingEnabled) {
    logEvent(centralHeatingEnabled ? "MOD DEĞİŞTİ: Kış Moduna Geçildi" : "MOD DEĞİŞTİ: Yaz/Kapalı Moduna Geçildi - CH kapatıldı");
    prevCentralHeatingEnabled = centralHeatingEnabled;
  }
  trackEvents();
  activeBoilerSet = centralHeatingEnabled ? boilerTemp : 0.0;
  activeDhwSet = dhwTemp;
  if (statusValid && !flameStatus) { modulation = 0; modValid = true; }  
  if (!faultBit) faultDetail = "";

  addJob(J_STATUS);

  if (scanning) {   
    addJob(J_SCAN);
    return;
  }

  if (!initialSyncDone) addJob(J_SYNC);
  else if (!dhwBoundsSynced && boundsSyncTries < 3) addJob(J_B48);
  else if (!petekBoundsSynced && petekBoundsTries < 3) addJob(J_B49);

  bool winter = centralHeatingEnabled;                      
  bool summer = !centralHeatingEnabled && dhwEnabled;       
  if (winter) {
    addJob(J_FLOW);
    addJob(J_RETURN);
    if (statusValid && dhwActiveStatus) addJob(J_DHWT);     
    if (statusValid && flameStatus) addJob(J_MOD);          
  } else if (summer) {
    if (statusValid && dhwActiveStatus) addJob(J_DHWT);
    if (statusValid && flameStatus) addJob(J_MOD);
  }

  if (faultBit) addJob(J_FAULT);                            

  if (settingMode && initialSyncDone) {
    if (tsetOncePending) addJob(J_TSETONCE);
    if (bPending && centralHeatingEnabled) { addJob(J_W1); addJob(J_V1); }   
    if (dPending && dhwEnabled) { addJob(J_W56); addJob(J_V56); }            
  }

  if (millis() - lastSlowRead >= SLOW_READ_MS || lastSlowRead == 0) {
    lastSlowRead = millis();
    slowPending = 0x07 | (watchId > 0 ? 0x08 : 0);
  }
  if (slowPending & 0x01) { addJob(J_P18); slowPending &= ~0x01; }
  else if (slowPending & 0x02) { 
    // YENİ: Petek Seti okuması (J_R1) sadece Kış Modunda (centralHeatingEnabled) kuyruğa eklenir
    if (centralHeatingEnabled) addJob(J_R1); 
    slowPending &= ~0x02; 
  }
  else if (slowPending & 0x04) { addJob(J_R56); slowPending &= ~0x04; }
  else if (slowPending & 0x08) { addJob(J_WATCH); slowPending &= ~0x08; }
}

bool setDone(bool readOk, float rv, float want, bool ackOk, byte &tries, byte &readFails, const char* name) {
  if (tries < 255) tries++;
  if (readOk) {
    readFails = 0;
    if (fabs(rv - want) < 0.5) return true;                  
  } else if (readFails < 255) {
    readFails++;
  }
  if (!readOk && readFails >= 3 && ackOk) return true;       
  if (tries >= MAX_SET_TRIES) {
    lastErrorStr = String(name) + " set kombiden doğrulanamadı (" + (readOk ? "okunan " + String(rv, 1) : String("okunamadı")) + ")";
    return true;
  }
  return false;
}

void runJob(byte j) {
  float rv = 0;
  switch (j) {
    case J_STATUS:
      doStatus();
      break;

    case J_SYNC: {
      syncAttempts++;
      float v = 0;
      
      // 1. Petek Set (ID 1)
      if (!tSetSynced && readF(1, v)) {
        if (v >= 25.0 && v <= 80.0) { boilerTemp = v; tSetSynced = true; lastSentBoilerTemp = v; tsetReadStr = String(v, 1) + " °C"; tsetNumStr = String(v, 1); }
      }
      
      // 2. Musluk Set (ID 56)
      v = 0;
      if (!dhwSynced && readF(56, v)) {
        if (v >= dhwMin && v <= dhwMax) { dhwTemp = v; dhwSynced = true; lastSentDhwTemp = v; }
      }

      // 3. CH Su Basıncı (ID 18)
      v = 0;
      if (!pressureSynced && readF(18, v)) {
        if (v >= 0.3 && v <= 3.5) {
          pressureStr = String(v, 2);
          pressureSynced = true;
        }
      }

      // 4. Petek Dönüş Suyu (ID 28)
      v = 0;
      if (!returnTempSynced && readF(28, v)) {
        if (v > 0 && v < 120) {
          returnTempStr = String(v, 1);
          returnTempSynced = true;
        }
      }

      // 5. Musluk Suyu Sıcaklığı (ID 26)
      v = 0;
      if (!dhwTempSynced && readF(26, v)) {
        if (v > 0 && v < 120) {
          dhwTempStr = String(v, 1);
          dhwTempSynced = true;
        }
      }

      // 6. Anlık Petek Suyu (ID tempId)
      v = 0;
      if (!flowTempSynced && readF(tempId, v)) {
        if (v > 0 && v < 120) {
          currentWaterTemp = v;
          tempValid = true;
          tempInfoStr = "ID" + String(tempId) + " OK";
          flowTempSynced = true;
        }
      }

      if ((tSetSynced && dhwSynced && pressureSynced && returnTempSynced && dhwTempSynced && flowTempSynced) || syncAttempts >= 6) {
        initialSyncDone = true;
        
        if (!initialOffApplied) {
          centralHeatingEnabled = false;
          dhwEnabled = false;
          id0Extra |= (0x20 | 0x40); 
          initialOffApplied = true;
          logEvent("AÇILIŞ OKUMALARI TAMAMLANDI -> Otomatik Kapalı Mod'a geçildi");
        }
      }
      break;
    }
    case J_B48:
      boundsSyncTries++;
      readDhwBounds();
      break;
    case J_B49:
      petekBoundsTries++;
      readPetekBounds();
      break;

    case J_FLOW: {   
      String idTxt = "ID" + String(tempId);
      if (readF(tempId, rv)) {
        if (rv > 0 && rv < 120) {
          currentWaterTemp = rv;
          tempValid = true;
          failFlow = 0;
          tempInfoStr = idTxt + " OK";
        } else {
          tempInfoStr = idTxt + " değer verdi: " + String(rv, 1) + " (kombi bu ID'yi desteklemiyor olabilir)";
          if (staleNow(failFlow)) tempValid = false;
        }
      } else {
        tempInfoStr = idTxt + ": " + errName(ot.error());
        if (staleNow(failFlow)) tempValid = false;
      }
      break;
    }
    case J_RETURN:
      if (readF(28, rv)) { returnTempStr = String(rv, 1); failReturn = 0; }
      else if (staleNow(failReturn)) returnTempStr = "--";
      break;
    case J_DHWT:
      if (readF(26, rv)) { dhwTempStr = String(rv, 1); failDhwT = 0; }
      else if (staleNow(failDhwT)) dhwTempStr = "--";
      break;
    case J_MOD:
      if (readF(17, rv) && rv >= 0 && rv <= 100) { modulation = rv; modValid = true; failMod = 0; }
      else if (staleNow(failMod)) modValid = false;
      break;

    case J_W1:   
      petekAck = sendTSet(boilerTemp);
      lastSentBoilerTemp = boilerTemp;
      break;
    case J_V1: {  
      bool ok = readF(1, rv) && rv >= 25.0 && rv <= 80.0;
      if (ok) { tsetNumStr = String(rv, 1); tsetReadStr = String(rv, 1) + " °C (kombiden okundu)"; failTSet = 0; }
      if (setDone(ok, rv, boilerTemp, petekAck, petekTries, petekReadFails, "Petek")) {
        bPending = false;
        if (ok && fabs(rv - boilerTemp) < 0.5) logEvent("PETEK SET KOMBİDEN DOĞRULANDI");
      }
      break;
    }
    case J_W56:   
      dhwAck = writeF(56, dhwTemp);
      lastSentDhwTemp = dhwTemp;
      break;
    case J_V56: {
      bool ok = readF(56, rv) && rv > 0;
      if (ok) { dhwReadNumStr = String(rv, 1); failDhw = 0; }
      if (setDone(ok, rv, dhwTemp, dhwAck, dhwTries, dhwReadFails, "Musluk")) {
        dPending = false;
        if (ok && fabs(rv - dhwTemp) < 0.5) logEvent("MUSLUK SET KOMBİDEN DOĞRULANDI");
      }
      break;
    }

    case J_P18:   
      if (readF(18, rv)) { pressureStr = String(rv, 2); failPress = 0; }
      else if (staleNow(failPress)) pressureStr = "--";
      break;
    case J_R1:    
      // YENİ: Sadece Kış Modundaysa ID 1 okuması gerçekleştirilir
      if (centralHeatingEnabled) {
        if (readF(1, rv) && rv >= 25.0 && rv <= 80.0) {
          tsetNumStr = String(rv, 1);
          tsetReadStr = String(rv, 1) + " °C (kombiden okundu)";
          failTSet = 0;
        } else if (staleNow(failTSet)) {
          tsetNumStr = "--";
          tsetReadStr = "--";
        }
      }
      break;
    case J_R56:   
      if (readF(56, rv) && rv > 0) {
        dhwReadNumStr = String(rv, 1);
        failDhw = 0;
        if (!dPending && rv >= dhwMin && rv <= dhwMax) dhwTemp = rv;
      } else if (staleNow(failDhw)) {
        dhwReadNumStr = "--";
      }
      break;

    case J_FAULT: {
      uint16_t raw = 0;
      faultDetail = readRaw(5, raw) ? faultText(raw) : String("ID 5 okunamadı (kombi desteklemiyor olabilir)");
      break;
    }
    case J_WATCH: {
      uint16_t raw = 0;
      if (readRaw(watchId, raw)) {
        watchStr = "ID" + String(watchId) + otIdName(watchId) + ": MSB=" + String(raw >> 8) + " LSB=" + String(raw & 0xFF) +
                   " | f8.8=" + String((int16_t)raw / 256.0, 2) + " | ham=0x" + hex4(raw);
      } else {
        watchStr = "ID" + String(watchId) + otIdName(watchId) + ": Yanıt yok / desteklenmiyor";
      }
      break;
    }
    case J_SCAN:
      scanStep();
      break;
    case J_TSETONCE:
      tsetOncePending = false;
      sendTSet(tsetOnceVal);
      break;
  }
}

void loop(void) {
  ArduinoOTA.handle();
  if (!mdnsStarted && WiFi.status() == WL_CONNECTED) {
    if (MDNS.begin("boiler-control")) {
      MDNS.addService("http", "tcp", 80);
      mdnsStarted = true;
    }
  }
  if (mdnsStarted) MDNS.update();
  server.handleClient();

  if (restartAt && (long)(millis() - restartAt) >= 0) ESP.restart();

  if (otPaused) {
    pinMode(outPin, OUTPUT);
    digitalWrite(outPin, LOW); 
    boilerStatusStr = "OpenTherm iletişimi durduruldu (kullanıcı)";
    return;
  }

  if (statusRetry && millis() - ts >= 120) {
    ts = millis();
    statusRetry = false;
    doStatus();
    retryTotal++;
    if (statusFailStreak > 0) retryFails++;
    return;
  }

  if (millis() - ts < 160) return;
  if (jobIdx >= jobCount) {
    if (millis() - cycleStart < CYCLE_MS) return;  
    buildCycle();
    cycleStart = millis();
    jobIdx = 0;
  }
  ts = millis();
  runJob(jobs[jobIdx++]);
}

web.h dosyasını eksize eklemek için sağdaki üç noktadan yeni sekme ekle seçerek adını web.h yaparak oluşturabilirsiniz.


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; }
    h3 { margin: 0 0 8px 0; color: #333; text-align: left; font-size: 16px; }
    .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: #e2f0d9; border: 1px solid #548235; color: #385723; 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: 12px; border: none; border-radius: 5px; cursor: pointer; color: white; text-decoration: none; display: inline-block; }
    input[type=submit].btn-mode { background-color: #6c757d; }
    input[type=submit].btn-mode.active { 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; }
    summary::-webkit-details-marker { color: #007BFF; }
  </style>
  <script>
    function setActive(id, on) {
      var el = document.getElementById(id);
      if (el) el.classList.toggle('active', on);
    }
    var btFilled = false;
    function updateData() {
      fetch('/data')
        .then(response => response.json())
        .then(data => {
          document.getElementById('curr-temp').innerText = (data.t === null ? '--' : data.t);
          document.getElementById('petek-read').innerText = data.tsn;
          document.getElementById('musluk-read').innerText = data.dsn;
          document.getElementById('flame-status').innerText = data.f;
          document.getElementById('mod-rate').innerText = (data.m === null ? '--' : data.m);
          document.getElementById('boiler-status').innerText = data.st;
          document.getElementById('tset-reply').innerText = data.tsr;
          document.getElementById('raw-status').innerText = data.raw;
          document.getElementById('watch-val').innerText = data.wch;
          document.getElementById('ot-log').innerHTML = data.otl;
          document.getElementById('pressure-val').innerText = data.prs;
          document.getElementById('return-val').innerText = data.rt;
          document.getElementById('dhw-temp-val').innerText = data.dht;
          document.getElementById('last-err').innerText = data.err;

          var fv = document.getElementById('fault-val');
          fv.innerText = data.flt;
          fv.style.color = data.fon == 1 ? '#d9534f' : (data.flt === '--' ? '#888' : '#28a745');

          document.getElementById('petek-title').style.color = data.chon == 1 ? '#28a745' : '#888';
          document.getElementById('dhw-title').style.color = data.dhon == 1 ? '#28a745' : '#888';

          var pIn = document.querySelector('input[name=bt]');
          if (data.pbs == 1) {
            document.getElementById('petek-range').innerText = data.pmn + ' - ' + data.pmx;
            if (pIn) { pIn.min = data.pmn; pIn.max = data.pmx; }
          }
          var dIn = document.querySelector('input[name=dt]');
          if (data.dbs == 1) {
            document.getElementById('dt-range').innerText = data.dmn + ' - ' + data.dmx;
            if (dIn) { dIn.min = data.dmn; dIn.max = data.dmx; }
          }

          if (!btFilled && data.bts == 1) {
            var bt = document.querySelector('input[name=bt]');
            if (bt && document.activeElement !== bt) bt.value = data.bt;
            btFilled = true;
          }

          var sm = data.sm || '';
          setActive('btn-winter-b', sm.indexOf('Kış') === 0);
          setActive('btn-summer-b', sm.indexOf('Yaz') === 0);
          setActive('btn-off-b', sm.indexOf('Kapalı') === 0);
        })
        .catch(error => console.log('Veri alınamadı:', error))
        .finally(() => setTimeout(updateData, 1000));
    }
    window.addEventListener('load', updateData);
  </script>
</head>
<body>
  <div class="card">
    <h2>Kombi Kontrol Paneli</h2>

    <div class="box">
      <div class="form-row" style="margin: 0;">
        <span><b>Kombi Modu:</b></span>
        <span>
          <form action="/" method="POST" style="display:inline;">
            <input type="hidden" name="mode" value="winter">
            <input type="submit" value="Kış" id="btn-winter-b" class="btn-mode">
          </form>
          <form action="/" method="POST" style="display:inline; margin-left: 2px;">
            <input type="hidden" name="mode" value="summer">
            <input type="submit" value="Yaz" id="btn-summer-b" class="btn-mode">
          </form>
          <form action="/" method="POST" style="display:inline; margin-left: 2px;">
            <input type="hidden" name="mode" value="off">
            <input type="submit" value="Kapalı" id="btn-off-b" class="btn-mode">
          </form>
        </span>
      </div>
      <div style="margin-top: 8px;"><b>Alev Durumu:</b> <span id="flame-status" style="color: #d9534f;">--</span></div>
      <div style="margin-top: 6px;"><b>Brülör Modülasyonu:</b> %<span id="mod-rate">--</span></div>
    </div>

    <div class="box">
      <b>Arıza Durumu:</b> <span id="fault-val" style="font-weight: bold; color: #888;">--</span>
    </div>

    <div class="box" id="petek-box">
      <h3 id="petek-title" style="color: #888;">Petek (CH)</h3>
      <div style="margin-top: 6px;"><b>Anlık Petek Suyu:</b> <span id="curr-temp">--</span> °C</div>
      <div style="margin-top: 6px;"><b>Petek Dönüş Suyu:</b> <span id="return-val">--</span> °C</div>
      <div style="margin-top: 6px;"><b>Petek Devresi Basıncı:</b> <span id="pressure-val">--</span> bar</div>
      <hr>
      <form action="/" method="POST">
        <div class="form-row">
          <span><b>Petek Set:<span id="petek-read" style="font-weight: bold; color: #d9534f;">--</span> °C</b><br></span>
          <small><span> (<span id="petek-range">--</span> °C)</small>
            <input type="number" name="bt" value="{0}" step="1" min="25" max="80" title="Petek set değeri"> °C
            <input type="submit" value="Gönder">
          </span>
        </div>
      </form>
    </div>

    <div class="box" id="musluk-box">
      <h3 id="dhw-title" style="color: #888;">Musluk (DHW)</h3>
      <div style="margin-top: 6px;"><b>Musluk Suyu Sıcaklığı:</b> <span id="dhw-temp-val">--</span> °C</div>
      <hr>
      <form action="/" method="POST">
        <div class="form-row">
          <span><b>Musluk Set:<span id="musluk-read" style="font-weight: bold; color: #0275d8;">--</span> °C</b> <small>(<span id="dt-range">--</span> °C)</small><br></span>
          <span>
            <input type="number" name="dt" value="{1}" step="1" min="{2}" max="{3}" title="Musluk set değeri"> °C
            <input type="submit" value="Gönder">
          </span>
        </div>
      </form>
    </div>

    <div class="form-row">
      <span><b>ESP:</b></span>
      <span>
        <form action="/" method="POST" style="display:inline;" onsubmit="return confirm('ESP yeniden başlatılsın mı? Birkaç saniye kombiyle iletişim kesilir.');">
          <input type="hidden" name="reset" value="1">
          <input type="submit" value="Reset" class="btn-mode">
        </form>
      </span>
    </div>

    <hr>

    <details style="text-align: left; background: #eef2f7; padding: 10px 12px; border-radius: 8px; margin-top: 10px;">
      <summary style="font-weight: bold; cursor: pointer; color: #333; user-select: none;">
        Detaylı Kombi Durumu ve Trafik Logları
      </summary>

      <div style="margin-top: 12px;">
        <div class="info"><b>Kombi Durumu:</b> <span id="boiler-status">Bağlanıyor...</span></div>

        <div class="info"><b>OpenTherm Trafiği</b> (en yeni üstte):</div>
        <div id="ot-log" style="font-family: monospace; font-size: 11px; background: #1e1e1e; color: #d4d4d4; padding: 8px; border-radius: 6px; max-height: 260px; overflow: auto; white-space: nowrap;">--</div>

        <div class="info"><b>İzlenen ID (/watch?id=N):</b> <span id="watch-val">--</span></div>

        <div class="info" style="word-break: break-all;"><b>Ham Durum Mesajı:</b> <span id="raw-status">--</span></div>

        <div class="error-box" style="background-color: #fff3cd; border-color: #ffc107; color: #856404; margin-top: 8px;">
          <b>Son Petek Set (TSet) Yanıtı:</b><br>
          <span id="tset-reply">--</span>
        </div>

        <div class="error-box" style="background-color: #ffe6e6; border-color: #ff4d4d; color: #cc0000; margin-top: 8px;">
          <b>Son Hata / Uyarı:</b><br>
          <span id="last-err">Yok</span>
        </div>
      </div>
    </details>
  </div>
</body>
</html>
)rawliteral";

const char HTTP_SCAN_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <meta charset='UTF-8'>
  {refresh}
  <meta name='viewport' content='width=device-width, initial-scale=1'>
  <title>OpenTherm Tarama</title>
</head>
<body style='font-family:monospace'>
  <p>{status}</p>
  <pre>{result}</pre>
</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.

web sayfası ekran görüntüsü


Projede sorun yaşarsanız yorum bölümünden sormaya çekinmeyin.

💬 Yorumlar

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

Bir Yorum Bırakın

Güvenlik Kontrolü:

Yenidir.com
↑ Sayfa Başı
📞 İletişim ✉ E-Posta Gönder 🟥 YouTube
© 2026 Yenidir.com
Yenidir CMS V0.1