ESP8266: термостат для водонагревателя с удалённым управлением

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

P.S. Чуть позже выяснилось, что пробит всё-таки тэн — его сопротивление между контактами и корпусом плавало в пределах 10-15 кОм (должна быть бесконечность в нормальном состоянии).

Я решил собрать термостат на основе ESP8266, чтобы можно было управлять им с телефона. Реализованные функции:

  • автоматическое включение/выключение тэна по датчику температуры;
  • отображение текущей температуры воды;
  • настройка требуемой температуры;
  • настройка гистерезиса (диапазона температуры);
  • удалённое включение/выключение;
  • в качестве бонуса: отправка данных о температуре на ваш сервер для удалённого мониторинга.

Список необходимых компонентов:

  • микроконтроллер ESP8266;
  • твердотельное реле на 25 ампер;
  • радиатор для реле;
  • термодатчик DS18B20;
  • дисплей TM1637;
  • блок питания 5 вольт, не менее 500 мА;
  • сдвоенный автоматический выключатель на 16-25 ампер (не обязательно);
  • пара светодиодов;
  • кабель медный сечением 2.5 мм2;
  • различная мелочёвка в виде радиозапчастей;
  • подходящий корпус;
  • наличие WiFi точки дома;
  • USB-UART адаптер для программирования.
Внимание!
Устройство работает с сетевым напряжением 220 вольт. Будьте осторожны!

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-адреса сервера.

Файлы
Скетч (22.46 KB)
Apk файл приложения (1.55 MB)
Проект для Appinventor (67.79 KB)
Приложение в Appinventor

Работа проверялась с Arduino IDE v1.6.7 + ESP8266 2.3.0

Библиотеки:
TM1637 (1.36 MB)
WiFiManager (35.73 KB)

Библиотеки являются собственностью их авторов!

Предупреждение!
Автор не несёт ответственности за возможную порчу оборудования. Всё, что вы делаете — вы делаете на свой страх и риск!