ESP8266: термостат для водонагревателя с удалённым управлением
Однажды, у меня произошла поломка водонагревателя — лопнул тэн. Через пару дней он был заменён, но проработал недолго: уже через сутки где-то напряжение пробило на воду. На кранах появилось 50 вольт, а на самом баке — 200. Первая мысль — залило конденсатом термостат при наполнении бака холодной водой. Отчасти так оно и было, поэтому было решено избавиться от механического термостата, а вместо него поставить самодельный электронный. Это дало бы возможность вынести его подальше от водяного бака, что обеспечило бы его защиту от воды.
P.S. Чуть позже выяснилось, что пробит всё-таки тэн — его сопротивление между контактами и корпусом плавало в пределах 10-15 кОм (должна быть бесконечность в нормальном состоянии).

Я решил собрать термостат на основе ESP8266, чтобы можно было управлять им с телефона. Реализованные функции:
- автоматическое включение/выключение тэна по датчику температуры;
- отображение текущей температуры воды;
- настройка требуемой температуры;
- настройка гистерезиса (диапазона температуры);
- удалённое включение/выключение;
- в качестве бонуса: отправка данных о температуре на ваш сервер для удалённого мониторинга.
Список необходимых компонентов:
- микроконтроллер ESP8266;
- твердотельное реле на 25 ампер;
- радиатор для реле;
- термодатчик DS18B20;
- дисплей TM1637;
- блок питания 5 вольт, не менее 500 мА;
- сдвоенный автоматический выключатель на 16-25 ампер (не обязательно);
- пара светодиодов;
- кабель медный сечением 2.5 мм2;
- различная мелочёвка в виде радиозапчастей;
- подходящий корпус;
- наличие WiFi точки дома;
- USB-UART адаптер для программирования.

ESP8266 можно брать любую, кроме первой.

Распиновка:

Не забываем, что напряжение питания ESP8266 составляет 3.3 В, поэтому необходим стабилизатор напряжения. Если у вас плата Nodemcu, то там он обычно уже есть. Вообще, для удобства работы с ESP8266 можно сделать свою отладочную плату, которую затем использовать при разработке различных устройств. Вот так, например, выглядит моя:


Твердотельное реле берём переменного тока с хорошим запасом. У меня реле Fotek SSR-25 DA, на 25 ампер. При этом ток тэна составляет 7 ампер (1.5 кВт). К такому реле обязательно нужен большой радиатор, иначе оно перегреется и выйдет из строя. Критическая температура для такого реле составляет 80 градусов, производитель рекомендует не превышать 60. Я взял алюминиевый радиатор от процессора. Для хорошего теплоконтакта используем термопасту.
Управляющее напряжение реле составляет от 3 до 32 вольт. При этом ток потребления не превышает нескольких миллиампер, поэтому его можно подключать напрямую к выходу микроконтроллера.

Обычное реле использовать не рекомендуется, так как его контакты будут подгорать. И, однажды, оно не сможет разомкнуть цепь — тэн перегреется. Поэтому, если уж решили использовать обычное реле, то только на свой страх и риск.
Блок питания — любой подходящий. Можно взять зарядное устройство от телефона. Я использовал преобразователь напряжения на 5 вольт, 4 ампера, купленный как-то давненько на eBay. Он имеет удобные винтовые зажимы для крепления проводов.
Автомат я поставил для возможности быстрого отключения тэна, при этом электроника остаётся работать.
Термодатчик DS18B20 удобнее брать в металлической капсуле. Но можно и обычный, тогда к нему необходимо припаять провода и заизолировать. Датчик должен входить в штатную трубку тэна.

Схема подключения (кликните для увеличения):

- VR1 — стабилизатор AMS1117-3.3;
- C1 — 47 мкФ 10В;
- C2 — 100nF;
- R1, R2 — резисторный делитель для понижения напряжения с UART адаптера. 1 кОм и 2 кОм соответственно;
- R3 — 10 кОм, подтяжка линии сброса;
- R4 — 10 кОм, подтяжка CH_PD;
- R5 — 4.7 кОм, подтяжка шины данных термодатчика;
- R6, R7 — 1 кОм, ограничивающие резисторы для светодиодов;
- R8 — 10 кОм, подтяжка GPIO15 на землю;
- LED1 — светодиод «Питание»;
- LED2 — светодиод «Нагрев»;
- S1 — автоматический выключатель;
- S2 — кнопка (либо джампер) прошивки. Необходимо замкнуть для прошивки;
- S3 — кнопка сброса.
Светодиод «Нагрев» мигает, когда на ТЭН подано питание.
Светодиод «Питание» в нормальном режиме всегда горит и начинает мигать при ошибках: потеря термодатчика (1 раз в секунду) или ошибка подключения к Wi-Fi (примерно 3 раза в секунду).
На дисплей TM1637 выводится текущая температура воды. Распиновка дисплея:

Монтируем реле и блок питания:

Собираем всю высоковольтную часть. Вход и соединения между реле и выходными автоматами делаем медным проводом сечением не менее 2.5 мм2.

Подключаем всё остальное:


Вот фотография моего ТЭНа. Хорошо видно сам нагревательный элемент и полую длинную трубку.

В трубку вставлялся штырь терморегулятора, который нам сейчас уже не нужен:

Вот именно в эту трубку и нужно поместить термодатчик. У датчика необходимо заизолировать контакты, обмазать его термопастой и пропихнуть в самый конец трубки. Провод закрепить так, чтобы он не вывалился из неё.
Ещё пара фотографий ТЭНа:


В итоге получился вот такой вот щиток:

Скетч для ESP8266. Прошивается через Arduino IDE с установленным фреймворком ESP8266.
///////////////////////////////////////////////////////////// // Author: Gladyshev Dmitriy (2016-2017) // // Create Date: 06.11.2016 // Design Name: Водонагреватель // Target Devices: ESP8266 // Tool versions: Arduino IDE 1.6.7 + ESP8266 2.3.0 // Description: Контроллер для водонагревателя с управлением через WiFi // Version: 1.0 // Link: https://19dx.ru/2017/08/esp8266-termostat-dlya-vodonagrevatelya-s-udalyonnym-upravleniem/ ///////////////////////////////////////////////////////////// #include <ESP8266WiFi.h> #include <DNSServer.h> #include <ESP8266WebServer.h> #include <OneWire.h> #include <EEPROM.h> #include <WiFiUdp.h> #include <TM1637Display.h> #include <WiFiManager.h> //https://github.com/tzapu/WiFiManager //----------------------- Настройка пинов ------------------------- #define PIN_RELAY 12 //реле #define PIN_DS 2 //термодатчик #define PIN_GREENLED 14 //светодиод "Питание" #define PIN_BLUELED 16 //светодиод "Нагрев" #define PIN_TM_CLK 4 //TM1637 CLK #define PIN_TM_DIO 5 //TM1637 DIO #define INTERVAL_UDP_SEND 60000 //интервал отправки данных по UDP в мс #define EEPROM_INIT_VALUE 11 //Сменить для инициализации EEPROM первоначальными значениями //Название точки доступа и пароль для режима настройки const char* config_ssid = "WaterHeater AP"; const char* config_password = "1234567890"; #define CONFIG_TIMEOUT 40 //время работы в режиме настройки при включении //----------------------- адреса EEPROM ------------------------- #define ADDR_SETUP 0 #define ADDR_TEMP 1 #define ADDR_ENABLED 2 #define ADDR_GISTEREZIS 3 #define ADDR_PORT1 4 #define ADDR_PORT2 5 #define ADDR_IP1 6 #define ADDR_IP2 7 #define ADDR_IP3 8 #define ADDR_IP4 9 const uint8_t SEG_CONF[] = { SEG_A | SEG_F | SEG_E | SEG_D, // C SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F, // O SEG_C | SEG_E | SEG_G, // n SEG_A | SEG_E | SEG_F | SEG_G // F }; const uint8_t SEG_ERR[] = { SEG_G, // - SEG_A | SEG_E | SEG_F | SEG_G | SEG_D, // E SEG_E | SEG_G, // r SEG_E | SEG_G // r }; const uint8_t SEG_LINE[] = { SEG_G, // - SEG_G, // - SEG_G, // - SEG_G, // - }; OneWire ds(PIN_DS); ESP8266WebServer server(80); WiFiUDP Udp; TM1637Display display(PIN_TM_CLK, PIN_TM_DIO); float celsius = 0; //Текущая температура int TempTarget; //Целевая температура bool Enabled; //Включено/выключено int Gisterezis; //Гистерезис int udpport; //UDP порт для пересылки данных byte udpip[4]; //IP для пересылки данных bool HeaterState = 0; //Текущее состояние нагревателя bool NeedCommit = false; //Флаг необходимости сохранения настроек в EEPROM bool SensorOK = false; //Состояние сенсора bool wifiOK = false; //Состояние wifi unsigned long ValidSensorTime = 0; //время последнего успешного чтения датчика /******************************************************************************* * Function Name : EEPROM_update * Description : Запись в EEPROM с предварительной проверкой. Если данные * совпадают, то повторно запись не производится, для экономии * ресурса. * Input : address - адрес EEPROM * value - записываемое значение *******************************************************************************/ void EEPROM_update(int address, uint8_t value) { if (EEPROM.read(address) != value) { EEPROM.write(address, value); NeedCommit = true; } } /******************************************************************************* * Function Name : EEPROM_commit * Description : Commit EEPROM, если это необходимо. ESP8266 не сразу делает * запись в EEPROM, а только по вызову commit() *******************************************************************************/ void EEPROM_commit() { if (NeedCommit) { EEPROM.commit(); NeedCommit = false; } } /******************************************************************************* * Function Name : handleRoot * Description : Обработка GET-запроса / * Информация об устройстве *******************************************************************************/ void handleRoot() { String message = "WaterHeater v1.0<br>"; message += "Author: Gladyshev Dmitriy<br>"; message += "Hardware: ESP8266<br>"; message += "<a href=\"http://19dx.ru\">http://19dx.ru</a><br>"; server.send(200, "text/html", message); } /******************************************************************************* * Function Name : handleGet * Description : Обработка GET-запроса /get * Получение данных о температурах и состоянии *******************************************************************************/ void handleGet() { /* Строка ответа: CurrentTemp | TargetTemp | Enabled | HeaterState | SensorError где: CurrentTemp - текущая температура (NN.N) TargetTemp - установленная температура (NN) Enabled - включено поддержание температуры (0/1) HeaterState - состояние нагрева (нагрев/ожидание = 1/0) SensorError - ошибка датчика (0/1) */ String message = String(celsius, 1); message += "|"; message += String(TempTarget); message += "|"; if (Enabled) { message += "1"; } else { message += "0"; } message += "|"; if (HeaterState) { message += "1"; } else { message += "0"; } message += "|"; if (SensorOK) { message += "0"; } else { message += "1"; } server.send(200, "text/plain", message); } /******************************************************************************* * Function Name : handleSet * Description : Обработка GET-запроса /set * Установка текущей температуре и состоянии *******************************************************************************/ void handleSet() { if (server.args() != 2) { return; } for (int i=0; i<2; i++) { if (server.argName(i) == "t") { String temp = server.arg(i); int t = temp.toInt(); if ((t>=30) && (t<=80)) { TempTarget = t; EEPROM_update(ADDR_TEMP, TempTarget); } } if (server.argName(i) == "s") { if (server.arg(i) == "0") { Enabled = false; } else { Enabled = true; } EEPROM_update(ADDR_ENABLED, Enabled); } } EEPROM_commit(); server.send(200, "text/plain", "OK"); } /******************************************************************************* * Function Name : handleGetSettings * Description : Обработка GET-запроса /getset * Получение настроек *******************************************************************************/ void handleGetSettings() { String message = String(Gisterezis); message += "|"; message += String(udpport); for (int i=0; i<4; i++) { message += "|"; message += String(udpip[i]); } server.send(200, "text/plain", message); } /******************************************************************************* * Function Name : handleSetSettings * Description : Обработка GET-запроса /setset * Сохранение настроек *******************************************************************************/ void handleSetSettings() { if (server.args() != 6) { return; } for (int i=0; i<6; i++) { if (server.argName(i) == "p") { String temp = server.arg(i); int p = temp.toInt(); if ((p>=1) && (p<=65535)) { udpport = p; byte t1 = p / 256; byte t2 = p % 256; EEPROM_update(ADDR_PORT1, t1); EEPROM_update(ADDR_PORT2, t2); } } if (server.argName(i) == "g") { String temp = server.arg(i); int p = temp.toInt(); if ((p>=5) && (p<=20)) { Gisterezis = p; EEPROM_update(ADDR_GISTEREZIS, p); } } if (server.argName(i) == "i1") { String temp = server.arg(i); int p = temp.toInt(); if ((p>=0) && (p<=255)) { udpip[0] = p; EEPROM_update(ADDR_IP1, p); } } if (server.argName(i) == "i2") { String temp = server.arg(i); int p = temp.toInt(); if ((p>=0) && (p<=255)) { udpip[1] = p; EEPROM_update(ADDR_IP2, p); } } if (server.argName(i) == "i3") { String temp = server.arg(i); int p = temp.toInt(); if ((p>=0) && (p<=255)) { udpip[2] = p; EEPROM_update(ADDR_IP3, p); } } if (server.argName(i) == "i4") { String temp = server.arg(i); int p = temp.toInt(); if ((p>=0) && (p<=255)) { udpip[3] = p; EEPROM_update(ADDR_IP4, p); } } } EEPROM_commit(); server.send(200, "text/plain", "OK"); } /******************************************************************************* * Function Name : handleNotFound * Description : Обработка ошибки 404 *******************************************************************************/ void handleNotFound() { String message = "File Not Found\n\n"; message += "URI: "; message += server.uri(); message += "\nMethod: "; message += (server.method() == HTTP_GET)?"GET":"POST"; message += "\nArguments: "; message += server.args(); message += "\n"; for (uint8_t i=0; i<server.args(); i++){ message += " " + server.argName(i) + ": " + server.arg(i) + "\n"; } server.send(404, "text/plain", message); } /******************************************************************************* * Function Name : WiFiEvent * Description : Обработка изменений состояния Wi-Fi соединения. * Вызывается автоматически. *******************************************************************************/ void WiFiEvent(WiFiEvent_t event) { Serial.printf("[WiFi-event] event: %d\n", event); switch(event) { case WIFI_EVENT_STAMODE_GOT_IP: wifiOK = true; Serial.println(F("[WiFi-event] WiFi connected")); Serial.print(F("IP address: ")); Serial.println(WiFi.localIP()); break; case WIFI_EVENT_STAMODE_DISCONNECTED: wifiOK = false; Serial.println(F("[WiFi-event] WiFi lost connection")); break; } } /******************************************************************************* * Function Name : RelayControl * Description : Управление состоянием реле и светодиодом "Нагрев" *******************************************************************************/ void RelayControl() { static unsigned long TimeBlinkBlue = 0; static bool StateBlinkBlue = false; if (Enabled && SensorOK) { if (celsius < (TempTarget-Gisterezis)) { digitalWrite(PIN_RELAY, HIGH); HeaterState = true; } else if (celsius > TempTarget) { digitalWrite(PIN_RELAY, LOW); HeaterState = false; } } else { digitalWrite(PIN_RELAY, LOW); HeaterState = false; } //Если включён нагрев, то индикатор "Нагрев" мигает. Иначе не горит. if (HeaterState) { unsigned long delta = millis() - TimeBlinkBlue; if (delta >= 500) { StateBlinkBlue = !StateBlinkBlue; digitalWrite(PIN_BLUELED, StateBlinkBlue); TimeBlinkBlue = millis(); } } else { digitalWrite(PIN_BLUELED, LOW); } } /******************************************************************************* * Function Name : PowerLEDcontrol * Description : Управление светодиодом "Питание" *******************************************************************************/ void PowerLEDcontrol() { static unsigned long TimeBlinkGreen = 0; static bool StateBlinkGreen = false; if (SensorOK && wifiOK) { //в нормальном режиме светодиод просто горит digitalWrite(PIN_GREENLED, HIGH); } else if (!SensorOK) { //мигание раз в секунду, если ошибка датчика unsigned long delta = millis() - TimeBlinkGreen; if (delta >= 500) { StateBlinkGreen = !StateBlinkGreen; digitalWrite(PIN_GREENLED, StateBlinkGreen); TimeBlinkGreen = millis(); } } else { //мигание раз в 300 мс, если ошибка подключения к Wi-Fi unsigned long delta = millis() - TimeBlinkGreen; if (delta >= 150) { StateBlinkGreen = !StateBlinkGreen; digitalWrite(PIN_GREENLED, StateBlinkGreen); TimeBlinkGreen = millis(); } } } /******************************************************************************* * Function Name : SendUDPCurrentState * Description : Отправка температур и состояния по UDP *******************************************************************************/ void SendUDPCurrentState() { /* CurrentTemp | TargetTemp | Enabled | HeaterState | SensorError */ static unsigned int TimeSend = 0; unsigned int delta = millis() - TimeSend; if ( (delta >= INTERVAL_UDP_SEND) && wifiOK ) { String message = String(celsius, 1); message += "|"; message += String(TempTarget); message += "|"; if (Enabled) { message += "1"; } else { message += "0"; } message += "|"; if (HeaterState) { message += "1"; } else { message += "0"; } message += "|"; if (SensorOK) { message += "0"; } else { message += "1"; } IPAddress ip(udpip[0], udpip[1], udpip[2], udpip[3]); Udp.beginPacket(ip, udpport); Udp.print(message); Udp.endPacket(); TimeSend = millis(); } } /******************************************************************************* * Function Name : SensorRead * Description : Чтение данных с термодатчика *******************************************************************************/ void SensorRead() { static unsigned long TimeDS = 0; unsigned long delta = millis() - TimeDS; bool SensorFound = true; if (delta >= 3000) { byte present = 0; byte type_s; byte data[12]; byte addr[8]; if ( !ds.search(addr)) { //Serial.println("1-wire scan ended."); ds.reset_search(); delay(250); SensorFound = false; //return; } /*Serial.print("ROM ="); for(int i = 0; i < 8; i++) { Serial.write(' '); Serial.print(addr[i], HEX); }*/ if (SensorFound) { if (OneWire::crc8(addr, 7) != addr[7]) { Serial.println("CRC is not valid!"); return; } // первый байт определяет чип switch (addr[0]) { case 0x10: //Serial.println(" Chip = DS18S20"); // or old DS1820 type_s = 1; break; case 0x28: //Serial.println(" Chip = DS18B20"); type_s = 0; break; case 0x22: //Serial.println(" Chip = DS1822"); type_s = 0; break; default: //Serial.println("Device is not a DS18x20 family device."); return; } ds.reset(); ds.select(addr); ds.write(0x44, 1); // start conversion, with parasite power on at the end //Задержка для конвертации данных датчиком unsigned long TimeT = millis(); while (millis() - TimeT < 1000) { delay(100); RelayControl(); PowerLEDcontrol(); } present = ds.reset(); ds.select(addr); ds.write(0xBE); // Read Scratchpad //Serial.print(" Data = "); //Serial.print(present, HEX); //Serial.print(" "); for (byte i = 0; i < 9; i++) { // we need 9 bytes data[i] = ds.read(); //Serial.print(data[i], HEX); //Serial.print(" "); } /*Serial.print(" CRC="); Serial.print(OneWire::crc8(data, 8), HEX); Serial.println();*/ // Convert the data to actual temperature // because the result is a 16 bit signed integer, it should // be stored to an "int16_t" type, which is always 16 bits // even when compiled on a 32 bit processor. int16_t raw = (data[1] << 8) | data[0]; if (type_s) { raw = raw << 3; // 9 bit resolution default if (data[7] == 0x10) { // "count remain" gives full 12 bit resolution raw = (raw & 0xFFF0) + 12 - data[6]; } } else { byte cfg = (data[4] & 0x60); // at lower res, the low bits are undefined, so let's zero them if (cfg == 0x00) raw = raw & ~7; // 9 bit resolution, 93.75 ms else if (cfg == 0x20) raw = raw & ~3; // 10 bit res, 187.5 ms else if (cfg == 0x40) raw = raw & ~1; // 11 bit res, 375 ms //// default is 12 bit resolution, 750 ms conversion time } celsius = (float)raw / 16.0; ValidSensorTime = millis(); } unsigned long deltaT = millis() - ValidSensorTime; if (deltaT > 15000) //от датчика не было ответа больше 15 секунд { display.setSegments(SEG_ERR); SensorOK = false; } else { if (celsius > 5) { display.showNumberDec((int) celsius, false); SensorOK = true; } else { display.setSegments(SEG_ERR); SensorOK = false; } } TimeDS = millis(); } } /******************************************************************************* * Function Name : debugOutput * Description : Вывод состояния для отладки *******************************************************************************/ void debugOutput() { static unsigned long dTime = millis(); unsigned long delta = millis() - dTime; if (delta >= 5000) { Serial.print("Sensor "); Serial.print(SensorOK ? "OK ;" : "FAIL ;"); Serial.print(" WiFi "); Serial.print(wifiOK ? "OK ;" : "FAIL ;"); Serial.print(" Temp = "); Serial.println(celsius); dTime = millis(); } } /******************************************************************************* * Function Name : Setup * Description : Инициализация устройств *******************************************************************************/ void setup() { Serial.begin(115200); delay(10); display.setBrightness(3); WiFiManager wifiManager; wifiManager.setTimeout(CONFIG_TIMEOUT); display.setSegments(SEG_CONF); //Запускаем точку доступа для настройки Serial.println(); Serial.println(); Serial.println(F("Starting config portal...")); wifiManager.startConfigPortal(config_ssid, config_password); display.setSegments(SEG_LINE); Serial.println(F("Stop config portal.")); wifiManager.setTimeout(5); Serial.print(F("Connecting to Wi-Fi... ")); if(!wifiManager.autoConnect(config_ssid, config_password)) { Serial.println(F("Failed to connect.")); wifiOK = false; delay(1000); } else { wifiOK = true; Serial.println(F("WiFi connected")); Serial.print(F("IP address: ")); Serial.println(WiFi.localIP()); } Serial.println(); EEPROM.begin(16); pinMode(PIN_RELAY, OUTPUT); pinMode(PIN_GREENLED, OUTPUT); pinMode(PIN_BLUELED, OUTPUT); digitalWrite(PIN_RELAY, LOW); digitalWrite(PIN_GREENLED, LOW); digitalWrite(PIN_BLUELED, LOW); //Устанавливаем начальные значения в EEPROM if (EEPROM.read(ADDR_SETUP) != EEPROM_INIT_VALUE) { Serial.print(F("Init EEPROM...")); EEPROM_update(ADDR_TEMP, 60); EEPROM_update(ADDR_ENABLED, 1); EEPROM_update(ADDR_GISTEREZIS, 10); //10222 EEPROM_update(ADDR_PORT1, 39); EEPROM_update(ADDR_PORT2, 238); EEPROM_update(ADDR_IP1, 192); EEPROM_update(ADDR_IP2, 168); EEPROM_update(ADDR_IP3, 1); EEPROM_update(ADDR_IP4, 2); EEPROM.write(ADDR_SETUP, EEPROM_INIT_VALUE); EEPROM.commit(); Serial.println(F(" completed.")); } TempTarget = EEPROM.read(ADDR_TEMP); Enabled = EEPROM.read(ADDR_ENABLED); Gisterezis = EEPROM.read(ADDR_GISTEREZIS); udpport = EEPROM.read(ADDR_PORT1)*256 + EEPROM.read(ADDR_PORT2); udpip[0] = EEPROM.read(ADDR_IP1); udpip[1] = EEPROM.read(ADDR_IP2); udpip[2] = EEPROM.read(ADDR_IP3); udpip[3] = EEPROM.read(ADDR_IP4); WiFi.onEvent(WiFiEvent); delay(1000); server.on("/", handleRoot); server.on("/get", handleGet); server.on("/set", handleSet); server.on("/getset", handleGetSettings); server.on("/setset", handleSetSettings); server.onNotFound(handleNotFound); server.begin(); ValidSensorTime = millis(); Udp.begin(udpport); } void loop() { //Обработка событий сервера server.handleClient(); //Работа с реле и индикаторами RelayControl(); PowerLEDcontrol(); //Отправка данных по UDP SendUDPCurrentState(); //Замер температуры SensorRead(); //Вывод в консоль для отладки debugOutput(); }
При включении питания ESP8266 запускает на 40 секунд точку доступа с именем «WaterHeater AP» и паролем «1234567890». При этом на дисплее появляется надпись «Conf». Параметры этой точки указаны в скетче в строках 36-38:
const char* config_ssid = "WaterHeater AP"; const char* config_password = "1234567890"; #define CONFIG_TIMEOUT 40 //время работы в режиме настройки при включении
Для того чтобы настроить точку доступа, к которой будет подключаться наш водонагреватель, подключаемся к точке WaterHeater AP на смартфоне/ноутбуке и переходим по адресу 192.168.4.1 .
Отобразится окно настройки. В нём нажимаем кнопку Configure WiFi и вводим данные нашей домашней точки доступа.
После сохранения настроек точка доступа «WaterHeater AP» пропадёт, и микроконтроллер подключится к точке, которую мы указали в вебинтерфейсе. Если понадобится изменить точку доступа, то нужно всего лишь выключить/включить питание ESP8266 (или нажать кнопку сброса) и снова зайти в вебинтерфейс.
Теперь водонагреватель готов к работе. На дисплее должна отображаться текущая температура воды.
Для мониторинга и настройки термостата с помощью MIT App Inventor было написано приложение для Android:
В нём видно текущую температуру. Также можно устанавливать требуемую температуру.
При необходимости можно отключить нагрев:
В настройках задаётся IP адрес водонагревателя (можно посмотреть в настройках роутера либо в любом сетевом сканере), гистерезис температуры и параметры сервера, на который необходимо отправлять данные (если это вам необходимо).

Для управления служат следующие HTTP GET-запросы:
/get — получение информации о текущей температуре воды, температуре установки, состояния;
/set?t={температура}&s={0/1} — установка температуры и состояния (включено/выключено);
/getset — получение текущих настроек;
/setset?p={порт}&g={гистерезис}&i1={ip1}&i2={ip2}&i3={ip3}&i4={ip4} — установка настроек.
Примеры запросов:
Установка температуры 75 градусов, водонагреватель включен:
/set?t=75&s=1
Сохранение настроек: гистерезис 10 градусов, ip-адрес сервера 192.168.1.10, порт 10200:
/setset?p=10200&g=10&ip1=192&ip2=168&ip3=1&ip4=10
Формат ответа на запрос /get:
{CurrentTemp} | {TargetTemp} | {Enabled} | {HeaterState} | {SensorError}
где:
- CurrentTemp — текущая температура (NN.N)
- TargetTemp — установленная температура (NN)
- Enabled — включено поддержание температуры (0/1)
- HeaterState — состояние нагрева (нагрев/ожидание = 1/0)
- SensorError — ошибка датчика (0/1)
Пример:
75.0|75|1|0|0
Пример ответа на запрос /getset:
10|10222|192|168|1|2
Здесь возвращается гистерезис в градусах, UDP порт и 4 числа IP-адреса сервера.
Скетч
Apk файл приложения
Проект для Appinventor
Приложение в Appinventor
Работа проверялась с Arduino IDE v1.6.7 + ESP8266 2.3.0
Библиотеки:
TM1637
WiFiManager
Библиотеки являются собственностью их авторов!
