├── LICENSE ├── README.md └── esp32-wifi-thermostat ├── RingBuffer.h └── esp32-wifi-thermostat.ino /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 diyless 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP32 Wi-Fi OpenTherm Thermostat 2 | 3 | ESP32 Wi-Fi OpenTherm Thermostat sketch allows you to build simple OpenTherm Wi-Fi thermostat using ESP32 module and [ESP32/ESP8266 Thermostat Shield](https://diyless.com/product/esp8266-thermostat-shield). 4 | 5 | Detailed instructions you can find in [ESP32 Wi-Fi OpenTherm Thermostat](https://diyless.com/blog/esp32-wifi-thermostat) post of [diyless.com](https://diyless.com/Blog) blog. 6 | 7 | ## ESP32 Wi-Fi OpenTherm Thermostat Web Server 8 | ![ESP32 Wi-Fi OpenTherm Thermostat Web Server](https://diyless.com/blog/esp32-wifi-thermostat/esp32-wifi-thermostat-web-server.webp) 9 | 10 | ## License 11 | Copyright (c) 2020 [DIYLESS](http://diyless.com/). Licensed under the [MIT license](/LICENSE?raw=true). 12 | 13 | -------------------------------------------------------------------------------- /esp32-wifi-thermostat/RingBuffer.h: -------------------------------------------------------------------------------- 1 | #ifndef RINGBUFFER_h 2 | #define RINGBUFFER_h 3 | 4 | #include 5 | #include 6 | 7 | struct ChartItem { 8 | uint8_t status; 9 | uint8_t ch_temperature; 10 | uint8_t room_temperature; 11 | uint8_t modulation; 12 | bool marked; 13 | }; 14 | 15 | 16 | template 17 | class RingBuffer { 18 | private: 19 | const uint16_t size; 20 | T buf[Size]; 21 | T* write_p; 22 | T* read_p; 23 | T* end_p; 24 | uint16_t count; 25 | public: 26 | RingBuffer() : size(Size), write_p(buf), read_p(buf), end_p(buf + Size), count(0) {} 27 | 28 | T* push() { 29 | T* result = write_p++; 30 | if (write_p == end_p) write_p = buf; 31 | if (count < size) { 32 | count++; 33 | } 34 | else { 35 | read_p++; 36 | if (read_p == end_p) read_p = buf; 37 | } 38 | return result; 39 | } 40 | 41 | T* peek(uint16_t index = 0) { 42 | if (index >= count) { 43 | return NULL; 44 | } 45 | T* result = read_p + index; 46 | if (result >= end_p) { 47 | result = buf + (result - end_p); 48 | } 49 | return result; 50 | } 51 | 52 | T* pop() { 53 | if (count <= 0) { 54 | return NULL; 55 | } 56 | T* result = read_p++; 57 | count--; 58 | if (read_p == end_p) read_p = buf; 59 | return result; 60 | } 61 | 62 | uint16_t getCount() const { 63 | return count; 64 | } 65 | 66 | uint16_t getSize() const { 67 | return size; 68 | } 69 | 70 | bool isEmpty() const { 71 | return count <= 0; 72 | } 73 | 74 | bool isFull() const { 75 | return count >= size; 76 | } 77 | }; 78 | 79 | #endif 80 | -------------------------------------------------------------------------------- /esp32-wifi-thermostat/esp32-wifi-thermostat.ino: -------------------------------------------------------------------------------- 1 | /* 2 | Name: esp32_wifi_thermostat.ino 3 | Author: DIYLESS 4 | */ 5 | #ifdef ESP32 6 | #include 7 | #include 8 | #include 9 | #include 10 | #elif defined(ESP8266) 11 | #include 12 | #include 13 | #include 14 | #include 15 | #endif 16 | 17 | #include 18 | #include 19 | #include 20 | #include "RingBuffer.h" 21 | 22 | const char* ssid = "WIFI-SSID"; 23 | const char* password = "WIFI-PASSWORD"; 24 | 25 | //Master OpenTherm Shield pins configuration 26 | const int OT_IN_PIN = 21; //4 for ESP8266 (D2), 21 for ESP32 27 | const int OT_OUT_PIN = 22; //5 for ESP8266 (D1), 22 for ESP32 28 | 29 | //Temperature sensor pin 30 | const int ROOM_TEMP_SENSOR_PIN = 18; //14 for ESP8266 (D5), 18 for ESP32 31 | 32 | OpenTherm ot(OT_IN_PIN, OT_OUT_PIN); 33 | #ifdef ESP32 34 | WebServer server(80); 35 | #elif defined(ESP8266) 36 | ESP8266WebServer server(80); 37 | #endif 38 | OneWire oneWire(ROOM_TEMP_SENSOR_PIN); 39 | DallasTemperature sensors(&oneWire); 40 | 41 | const char HTTP_HEAD_BEGIN[] PROGMEM = "{v}"; 42 | const char HTTP_STYLE[] PROGMEM = ""; 58 | 59 | const char HTTP_SCRIPT_VARS[] PROGMEM = "var drag=false,clickY,moveY=0,lastMoveY=0,room_setpoint=0;"; 60 | const char HTTP_SCRIPT_START_DRAG[] PROGMEM = "function startDrag(t){t.preventDefault(),drag=true,clickY=\"touchstart\"===t.type?t.changedTouches[0].clientY:t.clientY;}"; 61 | const char HTTP_SCRIPT_DO_DRAG[] PROGMEM = "function doDrag(e){if(e.preventDefault(),drag){var t=\"touchmove\"===e.type?e.changedTouches[0].clientY:e.clientY;moveY=Math.round(lastMoveY+(t-clickY));var o=parseInt(document.getElementById(\"roomL\").getAttribute(\"y1\"));moveY>0?moveY>286-o&&(moveY=286-o):moveY<186-o&&(moveY=186-o),document.getElementById(\"roomSp\").setAttribute(\"transform\",\"translate(0, \"+moveY+\")\"),room_setpoint=(286-o-moveY)/4+5,document.getElementById(\"roomTxt\").textContent=room_setpoint+\" \\u2103\";}}"; 62 | const char HTTP_SCRIPT_STOP_DRAG[] PROGMEM = "function stopDrag(e){if(e.preventDefault(),drag){drag=!1,lastMoveY=moveY;document.getElementById('info').innerHTML = document.getElementById('info').innerHTML;attachEvents(0);var t = new XMLHttpRequest;t.open(\"POST\", \"/room_setpoint\", !0), t.setRequestHeader(\"Content-type\", \"application/x-www-form-urlencoded\"), t.send(\"value=\" + room_setpoint)}}"; 63 | const char HTTP_SCRIPT_SEND_STATUS[] PROGMEM = "function sendStatus(){for(var e=document.getElementsByTagName(\"input\"),t=0,n=0;n=0&&a<=7&&(t|=1<
"; 68 | const char HTTP_END[] PROGMEM = "
"; 69 | const char HTTP_INFO[] PROGMEM = "\ 70 | \ 71 | \ 72 | \ 73 | \ 74 | \ 75 | \ 76 | \ 77 | \ 78 |
Central Heating
Domestic Hot Water
Cooling
Fault
Diagnostic
CH temperature
 
{t} ℃
Relative Modulation Level
 
{m} %
Room Temperature
 
{r} ℃
"; 79 | 80 | 81 | OpenThermMessageID requests[] = { 82 | OpenThermMessageID::Status, 83 | OpenThermMessageID::TSet, 84 | OpenThermMessageID::Tboiler, 85 | OpenThermMessageID::RelModLevel, 86 | }; 87 | const byte requests_count = sizeof(requests) / sizeof(uint8_t); 88 | 89 | #define MASTER_STATUS_CH_ENABLED 0x1 90 | #define MASTER_STATUS_DHW_ENABLED 0x2 91 | #define MASTER_STATUS_COOLING_ENABLED 0x4 92 | 93 | uint8_t CHEnabled = 0, DHWEnabled = 0, CoolingEnabled = 0; 94 | 95 | uint8_t boiler_status = 0; 96 | float ch_temperature = 0; 97 | float ch_setpoint = 0; 98 | float room_temperature = 0; 99 | float room_setpoint = 5; 100 | float modulation_level = 0; 101 | unsigned long marked_min = 0; 102 | 103 | float room_temperature_last = 0, //prior temperature 104 | ierr = 0, //integral error 105 | dt = 0, //time between measurements 106 | op = 0; //PID controller output 107 | unsigned long ts = 0, new_ts = 0; //timestamp 108 | 109 | RingBuffer chart_items; 110 | byte req_idx = 0; 111 | ChartItem* curr_item = NULL; 112 | 113 | void ICACHE_RAM_ATTR handleInterrupt() { 114 | ot.handleInterrupt(); 115 | } 116 | 117 | float getTemperature() { 118 | float t = sensors.getTempCByIndex(0); 119 | if (t < 0) 120 | { 121 | t = 0; 122 | } 123 | return t; 124 | } 125 | 126 | float pid(float sp, float pv, float pv_last, float& ierr, float dt) { 127 | float KP = 30; 128 | float KI = 0.02; 129 | // upper and lower bounds on heater level 130 | float ophi = 80; 131 | float oplo = 10; 132 | // calculate the error 133 | float error = sp - pv; 134 | // calculate the integral error 135 | ierr = ierr + KI * error * dt; 136 | // calculate the measurement derivative 137 | //float dpv = (pv - pv_last) / dt; 138 | // calculate the PID output 139 | float P = KP * error; //proportional contribution 140 | float I = ierr; //integral contribution 141 | float op = P + I; 142 | // implement anti-reset windup 143 | if ((op < oplo) || (op > ophi)) { 144 | I = I - KI * error * dt; 145 | // clip output 146 | op = max(oplo, min(ophi, op)); 147 | } 148 | ierr = I; 149 | Serial.println("sp=" + String(sp) + " pv=" + String(pv) + " dt=" + String(dt) + " op=" + String(op) + " P=" + String(P) + " I=" + String(I)); 150 | return op; 151 | } 152 | 153 | void handleNotFound() { 154 | digitalWrite(BUILTIN_LED, 1); 155 | String message = "File Not Found\n\n"; 156 | message += "URI: "; 157 | message += server.uri(); 158 | message += "\nMethod: "; 159 | message += (server.method() == HTTP_GET) ? "GET" : "POST"; 160 | message += "\nArguments: "; 161 | message += server.args(); 162 | message += "\n"; 163 | for (uint8_t i = 0; i < server.args(); i++) { 164 | message += " " + server.argName(i) + ": " + server.arg(i) + "\n"; 165 | } 166 | server.send(404, "text/plain", message); 167 | digitalWrite(BUILTIN_LED, 0); 168 | } 169 | 170 | String getInfo() { 171 | unsigned long timestamp = millis(); 172 | char uptime[32]; 173 | unsigned long secs = timestamp / 1000, mins = secs / 60; 174 | unsigned int hours = mins / 60, days = hours / 24; 175 | timestamp -= secs * 1000; 176 | secs -= mins * 60; 177 | mins -= hours * 60; 178 | hours -= days * 24; 179 | sprintf(uptime, "%dd %02d:%02d:%02d", (byte)days, (byte)hours, (byte)mins, (byte)secs); 180 | 181 | String page = FPSTR(HTTP_INFO); 182 | page.replace("{0}", bitRead(boiler_status, 0) ? "checked=\"checked\"" : ""); 183 | page.replace("{s}", CHEnabled ? "checked=\"checked\"" : ""); 184 | page.replace("{d}", DHWEnabled ? "checked=\"checked\"" : ""); 185 | page.replace("{c}", CoolingEnabled ? "checked=\"checked\"" : ""); 186 | page.replace("{6}", bitRead(boiler_status, 6) ? "checked=\"checked\"" : ""); 187 | 188 | page.replace("{t}", String(ch_temperature)); 189 | page.replace("{m}", String(modulation_level)); 190 | page.replace("{r}", String(room_temperature)); 191 | page.replace("{rp}", String(room_temperature <= 25 ? room_temperature * 4 : 25)); 192 | page.replace("{rsp}", String(room_setpoint)); 193 | page.replace("{u}", uptime); 194 | return page; 195 | } 196 | 197 | void handleInfo() { 198 | Serial.println(">>> Info request"); 199 | String tStr = server.arg("value"); 200 | if (!tStr.isEmpty()) 201 | { 202 | float t = tStr.toFloat(); 203 | Serial.println(">>> Room Setpoint request: " + String(t)); 204 | updateRoomSetpointRequest(t); 205 | } 206 | server.send(200, "text/html", getInfo() + getChart()); 207 | } 208 | 209 | void handleRoot() { 210 | String page = FPSTR(HTTP_HEAD_BEGIN); 211 | page.replace("{v}", "Wi-Fi Thermostat"); 212 | page += FPSTR(HTTP_STYLE); 213 | page += ""; 222 | page += FPSTR(HTTP_HEAD_END); 223 | page += F("

Wi-Fi Thermostat

"); 224 | page += getInfo() + getChart(); 225 | page += F("
"); 226 | page += FPSTR(HTTP_END); 227 | server.send(200, "text/html", page); 228 | } 229 | 230 | void handleMessage() { 231 | byte type = server.arg("type").toInt() ? 1 : 0; 232 | byte id = server.arg("id").toInt(); 233 | unsigned int data = server.arg("data").toInt(); 234 | unsigned long msg = ot.buildRequest((OpenThermMessageType)type, (OpenThermMessageID)id, data); 235 | unsigned long response = ot.sendRequest(msg); 236 | Serial.println("Msg: type=" + String(type) + ", id=" + String(id) + ", data=" + String(data) + " Req: " + String(msg) + " Resp: " + String(response)); 237 | server.send(200, "text/html", String(response)); 238 | } 239 | 240 | void updateStatus(byte status) { 241 | CHEnabled = (status & MASTER_STATUS_CH_ENABLED) ? 1 : 0; 242 | DHWEnabled = (status & MASTER_STATUS_DHW_ENABLED) ? 1 : 0; 243 | CoolingEnabled = (status & MASTER_STATUS_COOLING_ENABLED) ? 1 : 0; 244 | } 245 | 246 | void handleStatus() { 247 | byte status = server.arg("value").toInt(); 248 | Serial.println(">>> Status request: " + String(status, BIN)); 249 | updateStatus(status); 250 | server.send(200, "text/html", String("ok")); 251 | } 252 | 253 | void updateCHTempRequest(float t) { 254 | if (t < 0) t = 0; 255 | if (t > 100) t = 100; 256 | ch_setpoint = t; 257 | } 258 | 259 | void handleCHTemp() { 260 | byte t = server.arg("value").toInt(); 261 | Serial.println(">>> CH Temp request: " + String(t)); 262 | updateCHTempRequest(t); 263 | server.send(200, "text/html", String("ok")); 264 | } 265 | 266 | void addChTemperature(String& out, char* buf) 267 | { 268 | int i = 0; 269 | byte prev = 0; 270 | out += F(""); 271 | out += F("ch_temperature != prev) { 275 | sprintf(buf, ",%d,%d,%d,%d", i, 172 - prev, i, 172 - item->ch_temperature); 276 | prev = item->ch_temperature; 277 | out += buf; 278 | } 279 | } 280 | sprintf(buf, ",%d,%d,%d,%d,0,%d", i, 172 - prev, i, 172, 172); 281 | out += buf; 282 | out += "\" />"; 283 | } 284 | 285 | void addModulation(String& out, char* buf) 286 | { 287 | int i = 0; 288 | byte prev = 0; 289 | //out += ""; 290 | out += F("modulation != prev) { 294 | sprintf(buf, ",%d,%d,%d,%d", i, 172 - prev, i, 172 - item->modulation); 295 | prev = item->modulation; 296 | out += buf; 297 | } 298 | } 299 | sprintf(buf, ",%d,%d,%d,%d,0,%d", i, 172 - prev, i, 172, 172); 300 | out += buf; 301 | out += "\" />"; 302 | } 303 | 304 | void updateRoomSetpointRequest(float t) { 305 | if (t < 0) t = 0; 306 | if (t > 30) t = 30; 307 | room_setpoint = t; 308 | } 309 | 310 | void handleRoomSetpoint() { 311 | float t = server.arg("value").toFloat(); 312 | Serial.println(">>> Room Setpoint request: " + String(t)); 313 | updateRoomSetpointRequest(t); 314 | server.send(200, "text/html", String("ok")); 315 | } 316 | 317 | uint16_t getTChartValue(uint8_t t) 318 | { 319 | float v = t / 10.0 - 5; 320 | v *= 4; 321 | return uint16_t(v); 322 | } 323 | 324 | void addRoomTemperature(String& out, char* buf) 325 | { 326 | int i = 0; 327 | byte prev = 0; 328 | out += ""; 329 | out += F("room_temperature != prev) { 333 | if (!prev) prev = item->room_temperature; 334 | sprintf(buf, ",%d,%d,%d,%d", i, 286 - getTChartValue(prev), i, 286 - getTChartValue(item->room_temperature)); 335 | prev = item->room_temperature; 336 | out += buf; 337 | } 338 | } 339 | sprintf(buf, ",%d,%d,%d,%d,0,%d", i, 286 - getTChartValue(prev), i, 286, 286); 340 | out += buf; 341 | out += "\" />"; 342 | } 343 | 344 | void addRoomSetpoint(String& out, char* buf) 345 | { 346 | out += ""; 347 | float sp = uint16_t((room_setpoint - 5) * 4); 348 | uint16_t spPos = 286 - sp; 349 | out += ""; 350 | out += "" + String(room_setpoint) + " ℃"; 351 | out += ""; 352 | } 353 | 354 | String getChart() { 355 | String out = ""; 356 | char buf[30]; 357 | out += F(""); 358 | out += F(""); 359 | out += F(""); 360 | 361 | byte bits[] = { 3,1,2 }; 362 | String colors[] = { "#d17905", "#f05b4f", "#0544d3" }; 363 | String titles[] = { "Flame", "Central Heating", "Domestic Hot Water" }; 364 | int i; 365 | byte prev; 366 | for (byte b = 0; b < 3; b++) { 367 | int y = 10 + b * 24; 368 | prev = 0; 369 | out += ""; 370 | out += "status, bits[b]); 374 | if (val != prev) { 375 | sprintf(buf, ",%d,%d,%d,%d", i, y - (prev ? 10 : 0), i, y - (val ? 10 : 0)); 376 | prev = val; 377 | out += buf; 378 | } 379 | } 380 | sprintf(buf, ",%d,%d,%d,%d,0,%d", i, y - (prev ? 10 : 0), i, y, y); 381 | out += buf; 382 | 383 | out += "\" />"; 384 | out += "" + titles[b] + ""; 385 | 386 | } 387 | 388 | 389 | out += ""; 390 | addChTemperature(out, buf); 391 | addModulation(out, buf); 392 | //CH SP 393 | /* 394 | out += ""; 395 | byte sp = (requests[set_ch_temp_req_idx] & 0xFFFF) / 256.0; 396 | byte spPos = 172 - sp; 397 | out += ""; 398 | out += "" + String(sp) + " ℃"; 399 | out += ""; 400 | */ 401 | out += ""; 402 | out += F(""); 403 | out += F("CH Temp"); 404 | out += F("/Modulation"); 405 | out += ""; 406 | 407 | 408 | out += ""; 409 | addRoomTemperature(out, buf); 410 | addRoomSetpoint(out, buf); 411 | out += ""; 412 | out += F("Room Thermperature"); 413 | 414 | 415 | out += ""; 416 | 417 | //Time grid 418 | out += ""; 419 | for (i = 0; i < chart_items.getCount(); i++) { 420 | ChartItem* item = chart_items.peek(i); 421 | if (item->marked) { 422 | out += ""; 423 | } 424 | } 425 | out += ""; 426 | 427 | out += ""; 428 | return out; 429 | } 430 | 431 | void handleChart() { 432 | Serial.println(">>> Chart request"); 433 | server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); 434 | server.sendHeader("Pragma", "no-cache"); 435 | server.sendHeader("Expires", "-1"); 436 | server.send(200, "image/svg+xml", getChart()); 437 | } 438 | 439 | 440 | void setup(void) { 441 | pinMode(BUILTIN_LED, OUTPUT); 442 | digitalWrite(BUILTIN_LED, 0); 443 | Serial.begin(115200); 444 | WiFi.mode(WIFI_STA); 445 | WiFi.begin(ssid, password); 446 | Serial.println(""); 447 | 448 | // Wait for connection 449 | while (WiFi.status() != WL_CONNECTED) { 450 | delay(500); 451 | Serial.print("."); 452 | } 453 | Serial.println(""); 454 | Serial.print("Connected to "); 455 | Serial.println(ssid); 456 | Serial.print("IP address: "); 457 | Serial.println(WiFi.localIP()); 458 | 459 | if (MDNS.begin("thermostat")) { 460 | Serial.println("MDNS responder started"); 461 | } 462 | 463 | server.on("/", handleRoot); 464 | server.on("/info", handleInfo); 465 | server.on("/chart.svg", handleChart); 466 | server.on("/msg", handleMessage); 467 | server.on("/status", handleStatus); 468 | server.on("/ch_temp", handleCHTemp); 469 | server.on("/room_setpoint", handleRoomSetpoint); 470 | server.on("/test", []() { 471 | server.send(200, "text/plain", "this works as well"); 472 | }); 473 | 474 | server.onNotFound(handleNotFound); 475 | 476 | server.begin(); 477 | Serial.println("HTTP server started"); 478 | 479 | ot.begin(handleInterrupt, processResponse); 480 | marked_min = millis() / 60000; 481 | 482 | //Init DS18B20 sensor 483 | sensors.begin(); 484 | sensors.requestTemperatures(); 485 | sensors.setWaitForConversion(false); //switch to async mode 486 | room_temperature, room_temperature_last = getTemperature(); 487 | ts = millis(); 488 | } 489 | 490 | 491 | void processResponse(unsigned long response, OpenThermResponseStatus status) { 492 | if (!ot.isValidResponse(response)) { 493 | Serial.println("Invalid response: " + String(response, HEX) + ", status=" + OpenTherm::statusToString(ot.getLastResponseStatus())); 494 | return; 495 | } 496 | if (curr_item == NULL) { 497 | Serial.println("Failed to process response: " + String(response, HEX)); 498 | return; 499 | } 500 | float t; 501 | byte id = (response >> 16 & 0xFF); 502 | switch ((OpenThermMessageID)id) 503 | { 504 | case OpenThermMessageID::Status: 505 | boiler_status = response & 0xFF; 506 | curr_item->status = boiler_status; 507 | Serial.println("Boiler status: " + String(boiler_status, BIN)); 508 | break; 509 | case OpenThermMessageID::TSet: 510 | t = (response & 0xFFFF) / 256.0; 511 | updateCHTempRequest(t); 512 | Serial.println("Set CH temp: " + String(t)); 513 | break; 514 | case OpenThermMessageID::Tboiler: 515 | ch_temperature = (response & 0xFFFF) / 256.0; 516 | curr_item->ch_temperature = ch_temperature; 517 | Serial.println("CH temp: " + String(ch_temperature)); 518 | break; 519 | case OpenThermMessageID::RelModLevel: 520 | modulation_level = (response & 0xFFFF) / 256.0; 521 | curr_item->modulation = modulation_level; 522 | Serial.println("Modulation level: " + String(modulation_level)); 523 | break; 524 | default: 525 | Serial.println("Response: " + String(response, HEX) + ", id=" + String(id)); 526 | } 527 | } 528 | 529 | void clearItem(ChartItem* item) 530 | { 531 | curr_item->status = 0; 532 | curr_item->ch_temperature = 0; 533 | curr_item->room_temperature = 0; 534 | curr_item->modulation = 0; 535 | } 536 | 537 | unsigned int buildRequest(byte req_idx) 538 | { 539 | uint16_t status; 540 | OpenThermMessageID id = requests[req_idx]; 541 | switch (id) 542 | { 543 | case OpenThermMessageID::Status: 544 | status = 0; 545 | if (CHEnabled) status |= MASTER_STATUS_CH_ENABLED; 546 | if (DHWEnabled) status |= MASTER_STATUS_DHW_ENABLED; 547 | if (CoolingEnabled) status |= MASTER_STATUS_COOLING_ENABLED; 548 | status <<= 8; 549 | return ot.buildRequest(OpenThermMessageType::READ, OpenThermMessageID::Status, status); 550 | case OpenThermMessageID::TSet: 551 | return ot.buildRequest(OpenThermMessageType::WRITE, OpenThermMessageID::TSet, ((uint16_t)ch_setpoint) << 8); 552 | case OpenThermMessageID::Tboiler: 553 | return ot.buildRequest(OpenThermMessageType::READ, OpenThermMessageID::Tboiler, 0); 554 | case OpenThermMessageID::RelModLevel: 555 | return ot.buildRequest(OpenThermMessageType::READ, OpenThermMessageID::RelModLevel, 0); 556 | } 557 | return 0; 558 | } 559 | 560 | uint8_t normalizeTemperatureForChart(float t) 561 | { 562 | if (t < 5) t = 5; 563 | if (t > 30) t = 30; 564 | return uint8_t(t * 10); 565 | } 566 | 567 | 568 | void handleOpenTherm() { 569 | if (ot.isReady()) { 570 | if (curr_item == NULL || (req_idx == 0)) { 571 | curr_item = chart_items.push(); 572 | clearItem(curr_item); 573 | curr_item->room_temperature = normalizeTemperatureForChart(room_temperature); 574 | unsigned long min = millis() / 60000; 575 | if (min > marked_min) { 576 | marked_min = min; 577 | curr_item->marked = true; 578 | } 579 | else { 580 | curr_item->marked = false; 581 | } 582 | } 583 | if (curr_item == NULL) return; 584 | 585 | unsigned int request = buildRequest(req_idx); 586 | ot.sendRequestAync(request); 587 | //Serial.println("Request: " + String(request, HEX)); 588 | req_idx++; 589 | if (req_idx >= requests_count) { 590 | req_idx = 0; 591 | } 592 | } 593 | ot.process(); 594 | } 595 | 596 | void loop(void) { 597 | handleOpenTherm(); 598 | server.handleClient(); 599 | 600 | new_ts = millis(); 601 | if (new_ts - ts > 1000) { 602 | room_temperature = getTemperature(); 603 | dt = (new_ts - ts) / 1000.0; 604 | ts = new_ts; 605 | ch_setpoint = pid(room_setpoint, room_temperature, room_temperature_last, ierr, dt); 606 | room_temperature_last = room_temperature; 607 | sensors.requestTemperatures(); //async temperature request 608 | } 609 | } 610 | --------------------------------------------------------------------------------