Как стать автором
Обновить

Беспроводной программируемый по Wi-Fi комнатный термостат с монитором качества воздуха и другими полезными функциями

Время на прочтение 35 мин
Количество просмотров 30K

В системе автономного отопления моей квартиры работает выпускаемый серийно беспроводной комнатный термостат. Система, конечно, функционирует и без него: термостат был приобретен для экономии расхода газа и повышения комфорта.


Вещь очень полезная, но, на мой взгляд, несколько морально устаревшая. Было решено собрать нечто похожее на купленный термостат, добавив для начала в макет термостата более удобную настройку и подключение к Интернету.


Что в результате получилось – читайте дальше. Надеюсь, кроме меня проект будет интересен другим.


Знакомство


Возможности и характеристики:


  • Связь между узлами термостата осуществляется по воздуху на радиочастоте.
  • В течение суток термостат поддерживает постоянными три заданные значения температуры.
  • Настройки термостата (программа работы, граничные параметры воздуха, другие) задаются дистанционно через Wi-Fi с формы в браузере.
  • В термостат включена функция монитора качества воздуха с измерением температуры, уровня содержания углекислого газа и влажности воздуха.
  • Термостат укомплектован часами реального времени с синхронизацией часов с сервером точного времени через Интернет.
  • Управление термостатом осуществляется с интерфейса мобильного приложения Blynk. Кроме того, приложение Blynk принимает и отображает результаты измерения температуры, содержания СО2 и влажности воздуха.
  • Термостат автоматически переходит в автономный режим работы при отсутствии Wi-Fi.
  • С термостата отправляются сообщения на е-мейл, если температура, содержание СО2 или влажность воздуха находятся за пределами пороговых значений.
  • В термостате кроме температуры есть возможность поддержания в заданных пределах остальных измеряемых параметров воздуха.
  • По окончании отопительного сезона термостат не придется прятать: останутся в работе монитор качества воздуха с отправкой сообщений на почту и часы.

Термостат состоит из двух устройств. В первом устройстве формируется и передается на второе устройство сигнал управления нагревательным прибором или системой отопления, назовем это устройство анализатором. Второе устройство, принимает сигнал, дешифрирует его и управляет источником тепла – пусть это будет контактор. Связь между анализатором и контактором — беспроводная, на радиочастоте.



Сборка


Для сборки устройства понадобятся компоненты, перечень которых и их ориентировочная стоимость по ценам сайта AliExpress приведена в таблице.


Компонент Цена, $
анализатор
Wi-Fi плата NodeMCU CP2102 ESP8266 2,53
Датчик температуры и влажности DHT22 2,34
Датчик содержания СО2 MH Z-19 18,50
Часы RTC DS3231 1,00
Экран OLED LCD синий 0.96" I2C 128x64 1,95
RF модуль 433MHz, передатчик (цена комплекта: передатчик, приемник) 0,99
4-канальный преобразователь логических уровней 3,3В-5В (Logical Layer Converter) 0,28
Стабилизатор напряжения LM7805 (10 шт.) 0,79
Адаптер AC100-240V 50/60Hz DC12V 2A 10,70
Макетная плата (стеклотекстолит), контакты и др. 2,00
контактор
Модуль Arduino Pro Mini 5V 1,45
RF модуль 433MHz (приемник)    -
2-канальный модуль реле 0,98
Адаптер AC-DC HLK-PM01 4,29
Макетная плата (стеклотекстолит), контакты и др. 2,00
Всего (примерно): 50

Если планируется собирать термостат с минимальными габаритами, то нужно заменить 4-канальный преобразователь логических уровней на 2-канальный и 2-канальный модуль реле на 1-канальный.


Оба устройства собраны на стеклотекстолитовых макетных платах. Монтаж – навесной. Модули установлены на панельки, собранные из «гребенок» контактов. Такой подход имеет ряд преимуществ: компоненты легко демонтируются, легко меняется монтаж под новую версию скетча и, наконец, в корпусе самоделки не видно каким способом он выполнен.


Антенны у передатчика и приемника – это провод длиной 17,3 см. Повышенная мощность передатчика и простейшие антенны обеспечивают надежную связь в пределах квартиры.


Анализатор



Мозг анализатора – контроллер ESP8266 на плате модуля NodeMCU CP2102. Он принимает сигналы с датчиков и формирует сигналы управления передатчиком и экраном.



При установке датчика DHT22 на плате, измеренная температура на 1,5…2°С выше реальной (даже без корпуса!). Поэтому следует размещать датчик температуры подальше от элементов с большим тепловыделением LM7805 и NodeMCU CP2102. Кроме того, было бы неплохо установить стабилизатор напряжения LM7805 на радиатор и однозначно необходимо обеспечить хорошую конвекцию воздуха в корпусе для понижения температуры и уменьшения ошибки ее измерений. Другой вариант избавиться от ошибки — вынести датчик DHT22 за объем корпуса – этот вариант проще и я выбрал его.


В Интернете много нареканий на низкую точность измерения влажности датчика DHT22. На сегодня есть альтернатива: более современные датчики температуры и влажности HTU21D, Si7021, SHT21.


На анализатор подается постоянное напряжение 12В от адаптера AC/DC. Далее стабилизатор постоянного напряжения LM7805 формирует напряжение 5В. Напряжение питания передатчика — 12В. При тестировании устройства, когда анализатор и контактор находятся рядом на рабочем столе, питание анализатора можно организовать с USB-порта компьютера, подав напряжение на модуль NodeMCU CP2102 стандартным кабелем USB – microUSB. Напряжение питания NodeMCU CP2102 и MH Z-19 – 5В, питание остальных узлов схемы (3,3В) формирует стабилизатор модуля NodeMCU CP2102.


Датчик температуры и влажности DHT22 подключен к выводу D6 модуля NodeMCU CP2102. Часы DC3231 и дисплей 0.96" подключены к ESP8266 (на модуле NodeMCU CP2102) через двухпроводный интерфейс I2C, а выводы Tx, Rx датчика содержания СО2 MH Z-19 подключены к выводам Rx, Tx ESP8266 соответственно. Сигнал на передатчик поступает с NodeMCU CP2102 через преобразователь логических уровней, который преобразует сигнал с NodeMCU CP2102 с амплитудой около 3,3В в сигнал, амплитуда которого близка к напряжению питания передатчика 12В.


Если в модуле часов вы используете батарейку вместо аккумулятора, то не забудьте разорвать цепь заряда аккумулятора, иначе батарейка вздуется через несколько недель работы под напряжением. С автономным питанием часов точность хода 2 сек/год вам обеспечена.


Скетч анализатора для загрузки в ESP8266 находится под спойлером.


скетч анализатора
/*
 * Беспроводной программируемый по Wi-Fi комнатный термостат с монитором качества воздуха и другими полезными функциями (анализатор)
 */

#include <FS.h>
#include <Arduino.h>
#include <ESP8266WiFi.h>          //https://github.com/esp8266/Arduino

// Wifi Manager
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <WiFiManager.h>         //https://github.com/tzapu/WiFiManager

//e-mail
#include <ESP8266WiFiMulti.h>     //https://github.com/esp8266/Arduino/blob/master/libraries/ESP8266WiFi/src/ESP8266WiFiMulti.h
#include <ESP8266HTTPClient.h>
ESP8266WiFiMulti WiFiMulti;
char address[64] {"e-mail"};    //e-mail, address

// HTTP requests
#include <ESP8266HTTPClient.h>

// OTA updates
#include <ESP8266httpUpdate.h>
// Blynk
#include <BlynkSimpleEsp8266.h>

// Debounce
#include <Bounce2.h> //https://github.com/thomasfredericks/Bounce2

// JSON
#include <ArduinoJson.h>          //https://github.com/bblanchon/ArduinoJson

//clock
#include <pgmspace.h>
#include <TimeLib.h>
#include <WiFiUdp.h>
#include <Wire.h>
#include <RtcDS3231.h>       //https://github.com/Makuna/Rtc
RtcDS3231<TwoWire> Rtc(Wire);
#define countof(a) (sizeof(a) / sizeof(a[0]))

//timer
#include <SimpleTimer.h>
SimpleTimer timer; //  ссылка на таймер
unsigned int timerCO2; //период опроса MH-Z19
unsigned int timerBl;   //период отправки данных на Blynk
unsigned int timerMail; //период отправки сообщений на емейл

// GPIO Defines
#define I2C_SDA 4 // D2 -  OLED
#define I2C_SCL 5 // D1 -  OLED
#define DHTPIN 12  //D6 cp2102

// Humidity/Temperature
#include <DHT.h>
#define DHTTYPE DHT22     // DHT 22
DHT dht(DHTPIN, DHTTYPE);

#define mySerial Serial

// Use U8g2 for i2c OLED Lib
#include <SPI.h>
#include <U8g2lib.h>
U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, I2C_SCL, I2C_SDA, U8X8_PIN_NONE);
byte x {0};
byte y {0};

// Blynk token
char blynk_token[33] {"Blynk token"};

//Transmitter
#include <RCSwitch.h>     
RCSwitch transmitter = RCSwitch();   
unsigned long TimeTransmitMax; // переменная для хранения точки отсчета времени передачи сигнала ВКЛ/ВЫКЛ передатчиком

// Setup Wifi connection
WiFiManager wifiManager;

// Network credentials
String ssid {"am-5108"};
String pass {"vb" +  String(ESP.getFlashChipId())};

//flag for saving data
bool shouldSaveConfig = false;

//переменные
float  t {-100};  //температура
int h {-1};    //влажность
int co2 {-1};  //содержание co2
float Chs = 0.2;  //чувствительность (гистерезис) термостата по температуре (диапазон: 0.1(большая тепловая инерция) - 0.4 (малая тепловая инерция))
char Tmx[]{"25.0"},  Hmn[]{"35"}, Cmx[]{"1000"},  tZ[]{"2.0"}; //пороговые значения t, h и co2, час. пояс
float Cmax, Tmax,  Hmin, tZone;
char Temperature0[]{"20.0"}, Temperature1[]{"22.0"}, Temperature2[]{"19.0"};//температура стабилизации термостата во временных интервалах
float TemperaturePoint0, TemperaturePoint1, TemperaturePoint2, TemperaturePoint1Mn, TemperaturePoint2Mn, TemperaturePoint1Pl, TemperaturePoint2Pl; 
float TemperaturePointA0 = 21.0;  //температура стабилизации термостата в автономном режиме
char Hour1[]{"6"}, Hour2[]{"22"};  //временные точки термостата, час
float HourPoint1, HourPoint2; 
float MinPoint1 = 0, MinPoint2 = 0;
int n, j, m; //счетчик часов, минут
int progr = 0; //счетчик программ работы термостата во времени суток
int timeSummerWinter = 0;  // летнее(1)/зимнее(0) время
int a = 1; //режим работы термостата: 1 - онлайн, 2 - автономный
bool buttonBlynk = true; //признак ВКЛ(true)/ВЫКЛ(falce) виртуальной кнопки V(10) Blynk 

//NTP, clock  
uint8_t hh,mm,ss;  //containers for current time
char time_r[9];
 char date_r[12];
// NTP Servers:
//static const char ntpServerName[] = "us.pool.ntp.org";
static const char ntpServerName[] = "time.nist.gov";

WiFiUDP Udp;
unsigned int localPort = 2390;  // local port to listen for UDP packets

time_t getNtpTime();
void digitalClockDisplay();
void printDigits(int digits);
void sendNTPpacket(IPAddress &address);

void digitalClockDisplay()
{
  // digital clock display of the time
  Serial.print(hour());
  printDigits(minute());
  printDigits(second());
  Serial.print(" ");
  Serial.print(day());
  Serial.print(".");
  Serial.print(month());
  Serial.print(".");
  Serial.print(year());
  Serial.println(); 
}

void printDigits(int digits)
{
  // utility for digital clock display: prints preceding colon and leading 0
  Serial.print(":");
  if (digits < 10)
    Serial.print('0');
  Serial.print(digits);
}
//NTP code
const int NTP_PACKET_SIZE = 48; // NTP time is in the first 48 bytes of message
byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold incoming & outgoing packets
time_t getNtpTime()  { 
  int tZoneI;
  tZoneI = (int)tZone;

  IPAddress ntpServerIP; // NTP server's ip address

  while (Udp.parsePacket() > 0) ; // discard any previously received packets
  Serial.println("Transmit NTP Request");
  // get a random server from the pool
  WiFi.hostByName(ntpServerName, ntpServerIP);
  Serial.print(ntpServerName);
  Serial.print(": ");
  Serial.println(ntpServerIP);
  sendNTPpacket(ntpServerIP);
  uint32_t beginWait = millis();
  while (millis() - beginWait < 1500) {
    int size = Udp.parsePacket();
    if (size >= NTP_PACKET_SIZE) {
      Serial.println("Receive NTP Response");
      Udp.read(packetBuffer, NTP_PACKET_SIZE);  // read packet into the buffer
      unsigned long secsSince1900;
      // convert four bytes starting at location 40 to a long integer
      secsSince1900 =  (unsigned long)packetBuffer[40] << 24;
      secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
      secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
      secsSince1900 |= (unsigned long)packetBuffer[43];
      return secsSince1900 - 2208988800UL + tZoneI * SECS_PER_HOUR + timeSummerWinter * SECS_PER_HOUR;  //tZoneI
    }
  }
  Serial.println("No NTP Response (:-()");
  return 0; // return 0 if unable to get the time
}

// send an NTP request to the time server at the given address
void sendNTPpacket(IPAddress &address)
{
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12] = 49;
  packetBuffer[13] = 0x4E;
  packetBuffer[14] = 49;
  packetBuffer[15] = 52;
  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:
  Udp.beginPacket(address, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

void synchronClockA() 
 {
   WiFiManager wifiManager;
  Rtc.Begin();

  Serial.print("IP number assigned by DHCP is ");
  Serial.println(WiFi.localIP());
  Serial.println("Starting UDP");
  Udp.begin(localPort);
  Serial.print("Local port: ");
  Serial.println(Udp.localPort());
  Serial.println("waiting for sync");
  setSyncProvider(getNtpTime);

  if(timeStatus() != timeNotSet){
    digitalClockDisplay();
    Serial.println("here is another way to set rtc");
      time_t t = now();
      char date_0[12];
      snprintf_P(date_0, countof(date_0), PSTR("%s %02u %04u"), monthShortStr(month(t)), day(t), year(t));
      Serial.println(date_0);
      char time_0[9];
      snprintf_P(time_0, countof(time_0), PSTR("%02u:%02u:%02u"), hour(t), minute(t), second(t));
      Serial.println(time_0);
      Serial.println("Now its time to set up rtc");
      RtcDateTime compiled = RtcDateTime(date_0, time_0);
 //      printDateTime(compiled);
       Serial.println("");

        if (!Rtc.IsDateTimeValid()) 
    {
        // Common Cuases:
        //    1) first time you ran and the device wasn't running yet
        //    2) the battery on the device is low or even missing

        Serial.println("RTC lost confidence in the DateTime!");

        // following line sets the RTC to the date & time this sketch was compiled
        // it will also reset the valid flag internally unless the Rtc device is
        // having an issue
      }
     Rtc.SetDateTime(compiled);
     RtcDateTime now = Rtc.GetDateTime();
    if (now < compiled) 
    {
        Serial.println("RTC is older than compile time!  (Updating DateTime)");
        Rtc.SetDateTime(compiled);
    }
    else if (now > compiled) 
    {
        Serial.println("RTC is newer than compile time. (this is expected)");
    }
    else if (now == compiled) 
    {
        Serial.println("RTC is the same as compile time! (not expected but all is fine)");
    }

    // never assume the Rtc was last configured by you, so
    // just clear them to your needed state
    Rtc.Enable32kHzPin(false);
    Rtc.SetSquareWavePin(DS3231SquareWavePin_ModeNone); 
 }
}

void synchronClock() {
      Rtc.Begin();
      wifiManager.autoConnect(ssid.c_str(), pass.c_str());
      while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
      }
      Serial.println(" ");
      Serial.print("IP number assigned by DHCP is ");
      Serial.println(WiFi.localIP());
      Serial.println("Starting UDP");
      Udp.begin(localPort);
      Serial.print("Local port: ");
      Serial.println(Udp.localPort());
      Serial.println("waiting for sync");
      setSyncProvider(getNtpTime);

      if(timeStatus() != timeNotSet){
        digitalClockDisplay();
        Serial.println("here is another way to set rtc");
          time_t t = now();
          char date_0[12];
          snprintf_P(date_0, countof(date_0), PSTR("%s %02u %04u"), monthShortStr(month(t)), day(t), year(t));
          Serial.println(date_0);
          char time_0[9];
          snprintf_P(time_0, countof(time_0), PSTR("%02u:%02u:%02u"), hour(t), minute(t), second(t));
          Serial.println(time_0);
          Serial.println("Now its time to set up rtc");
          RtcDateTime compiled = RtcDateTime(date_0, time_0);
          Serial.println("");

            if (!Rtc.IsDateTimeValid()) 
        {
            // Common Cuases:
            //    1) first time you ran and the device wasn't running yet
            //    2) the battery on the device is low or even missing

            Serial.println("RTC lost confidence in the DateTime!");

            // following line sets the RTC to the date & time this sketch was compiled
            // it will also reset the valid flag internally unless the Rtc device is
            // having an issue
         }
         Rtc.SetDateTime(compiled);
         RtcDateTime now = Rtc.GetDateTime();
        if (now < compiled) 
        {
            Serial.println("RTC is older than compile time!  (Updating DateTime)");
            Rtc.SetDateTime(compiled);
        }
        else if (now > compiled) 
        {
            Serial.println("RTC is newer than compile time. (this is expected)");
        }
        else if (now == compiled) 
        {
            Serial.println("RTC is the same as compile time! (not expected but all is fine)");
        }

        // never assume the Rtc was last configured by you, so
        // just clear them to your needed state
        Rtc.Enable32kHzPin(false);
        Rtc.SetSquareWavePin(DS3231SquareWavePin_ModeNone); 
    }
}

void Clock(){
    RtcDateTime now = Rtc.GetDateTime();
    //Print RTC time to Serial Monitor
   hh = now.Hour();
   mm = now.Minute(); 
   ss = now.Second();

  sprintf(date_r, "%d.%d.%d", now.Day(), now.Month(), now.Year());
  if  (mm < 10) sprintf(time_r, "%d:0%d", hh, mm);
  else sprintf(time_r, "%d:%d", hh, mm);

   Serial.println(date_r);
   Serial.println(time_r);
   }

//callback notifying the need to save config
void saveConfigCallback() {
        Serial.println("Should save config");
        shouldSaveConfig = true;
}

void factoryReset() {
        Serial.println("Resetting to factory settings");
        wifiManager.resetSettings();
       SPIFFS.format();
       ESP.reset();
}

void printString(String str) {
        Serial.println(str);
}

void readCO2() {
      static byte cmd[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79}; //команда чтения
      byte response[9];
      byte crc = 0;
        while (mySerial.available())mySerial.read();   //очистка буфера UART перед запросом 
        memset(response, 0, 9);// очистка ответа
        mySerial.write(cmd,9);// запрос на содержание CO2
        mySerial.readBytes(response, 9);//читаем 9 байт ответа сенсора
        //расчет контрольной суммы
        crc = 0;
        for (int i = 1; i <= 7; i++)
        {
          crc += response[i];
        }
        crc = ((~crc)+1);
        {
        //проверка CRC
        if ( !(response[0] == 0xFF && response[1] == 0x86 && response[8] == crc) ) 
        {
          Serial.println("CRC error");
        } else 
            {
             //расчет значения CO2
           co2 = (((unsigned int) response[2])<<8) + response[3]; 
            Serial.println("CO2: " + String(co2) + "ppm");
                 }
        }
}

void sendMeasurements() {
      float  t1 {-100};
      int h1 {-1}, i;
        // Temperature
        t1 = dht.readTemperature();
              if ((t1 > -1) and (t1 < 100)) t = t1;
        Serial.println("T: " + String(t) + "C");
        // Humidity
        h1 = dht.readHumidity();
        if ((h1 > -1) and (h1 < 100))  h = h1;
       Serial.println("H: " + String(h) + "%");
         // CO2
          readCO2(); 
}

void sendToBlynk(){
        Blynk.virtualWrite(V1, t);
        Blynk.virtualWrite(V2, h);
        Blynk.virtualWrite(V3, co2);
        Blynk.virtualWrite(V4, TemperaturePoint0);
  }

void noData() {
        u8g2.setFont(u8g2_font_9x18_mf);
        x = 48;
        y = 40;
        u8g2.drawStr(x, y, "***");
        }

void drawOn() {
        float TemperatureP0;
        char Online_ch[]{"    Online"};       

        TemperatureP0 = TemperaturePoint0 - Chs;
        dtostrf(TemperatureP0, 4, 1, Temperature0); //преобразование float в char

        String Temperature0_i;
        Temperature0_i = String(Temperature0); 
        char Temperature0_i_m [16];
        Temperature0_i.toCharArray(Temperature0_i_m, 16);
        u8g2.clearBuffer();
        String Temperature0_p;
        String onl1 = "OnLine T<";
        Temperature0_p = onl1 + Temperature0_i_m;
        char Temperature0_p_m [16];
        Temperature0_p.toCharArray(Temperature0_p_m, 16); 

        String Tmx_i;
        Tmx_i = String(Tmx); 
        char Tmx_i_m [16];
        Tmx_i.toCharArray(Tmx_i_m, 16);
        u8g2.clearBuffer();
        String Tmx_p;
        String onl2 = "OnLine T>";
        Tmx_p = onl2 + Tmx_i_m;
        char Tmx_p_m [16];
        Tmx_p.toCharArray(Tmx_p_m, 16); 

        String Cmx_i;
        Cmx_i = String(Cmx); 
        char Cmx_i_m [16];
        Cmx_i.toCharArray(Cmx_i_m, 16);
        u8g2.clearBuffer();
        String Cmx_p;
        String onl3 = "OnL CO2>";
        Cmx_p = onl3 + Cmx_i_m;
        char Cmx_p_m [16];
        Cmx_p.toCharArray(Cmx_p_m, 16);

        String Hmn_i;
        Hmn_i = String(Hmn); 
        char Hmn_i_m [16];
        Hmn_i.toCharArray(Hmn_i_m, 16);
        u8g2.clearBuffer();
        String Hmn_p;
        String onl4 = "OnLine H<";
        Hmn_p = onl4 + Hmn_i_m;
        char Hmn_p_m [16];
        Hmn_p.toCharArray(Hmn_p_m, 16);

//string 3
        u8g2.setFont(u8g2_font_9x18_mf);
        x = 0;
        y = 64;
        u8g2.drawStr(x, y, Online_ch);
        if ((hh>=HourPoint1) and (hh<=HourPoint2) and (t<TemperatureP0)) u8g2.drawStr(x, y, Temperature0_p_m); 
        else
        if (t > Tmax) u8g2.drawStr(x, y, Tmx_p_m); 
        else 
        if (co2 > Cmax) u8g2.drawStr(x, y, Cmx_p_m);  
        else  
        if (h < Hmin) u8g2.drawStr(x, y, Hmn_p_m);        

         switch((millis() / 100) % 4) {
// Temperature 
   case 0:          
  { 
    String info_t;
    String paramT;
    String tmpr = "T(";
    String grad = "C):";
    const char degree {176};
     paramT = tmpr + degree + grad;
     char paramT_m [12];
     paramT.toCharArray(paramT_m, 12);

           info_t = String(t); 
         char info_t_m [12];
        info_t.toCharArray(info_t_m, 5);
//string 1
        u8g2.setFont(u8g2_font_9x18_mf);
        x = 16;
        y = u8g2.getAscent() - u8g2.getDescent();
         u8g2.drawStr(x, y, paramT_m);
//string 2 
       if ((t > -100) and (t < 100)) {      
        u8g2.setFont(u8g2_font_inb24_mf);
        x = (128 - u8g2.getStrWidth(info_t_m))/2;
        y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, info_t_m);
       }
      else noData();
        }         
                break;
 //Humidity             
        case 1:
      { 
        String info_h;
         info_h = String(h);  
        char info_h_m [12];
        info_h.toCharArray(info_h_m, 12);
//string 1
        u8g2.setFont(u8g2_font_9x18_mf);

        x = 16;
        y = u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, "H(%):");
//string 2        
    if ((h > -1) and (h < 100)){
        u8g2.setFont(u8g2_font_inb24_mf);
        x = (128 - u8g2.getStrWidth(info_h_m))/2;
        y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, info_h_m);
        }
      else noData();        
        }                   
                break;
//CO2 
   case 2:
      { 
        String info_co2;
        info_co2 = String(co2);
        char info_co2_m [12];
        info_co2.toCharArray(info_co2_m, 12);
//string 1
        u8g2.setFont(u8g2_font_9x18_mf);
        x = 8;
        y = u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, "CO2(ppm):");
//string 2        
      if  ((co2 > -1) and (co2 <= 2000)) {
        u8g2.setFont(u8g2_font_inb24_mf);
        x = (128 - u8g2.getStrWidth(info_co2_m))/2;
        y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, info_co2_m);
      }
      else noData();
        }                   
                break;                
//time, date 
   case 3:
      { 
//string 1
        u8g2.setFont(u8g2_font_9x18_mf);
        x = (128 - u8g2.getStrWidth(date_r))/2;
        y = u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, date_r);
//string 2        
        u8g2.setFont(u8g2_font_inb24_mf);
        x = (128 - u8g2.getStrWidth(time_r))/2;
        y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, time_r);
        }                   
                break;
     }     
        u8g2.sendBuffer();
}

void drawOff() {
        float TemperatureP0A;
        char OffLine_ch[]{"Offline Tst=21"};       

      TemperatureP0A = TemperaturePointA0 - Chs;
    //  dtostrf(TemperatureP0A, 4, 1, TemperaturePointA0); //преобразование float в char

        String TemperaturePointA0_i;
        TemperaturePointA0_i = String(TemperaturePointA0); 
        char TemperaturePointA0_i_m [16];
        TemperaturePointA0_i.toCharArray(TemperaturePointA0_i_m, 16);
        u8g2.clearBuffer();
        String TemperaturePointA0_p;
        String onl1 = "Offline T<";
        TemperaturePointA0_p = onl1 + TemperaturePointA0_i_m;
        char TemperaturePointA0_p_m [16];
        TemperaturePointA0_p.toCharArray(TemperaturePointA0_p_m, 16); 
//string 3
        u8g2.setFont(u8g2_font_9x18_mf);
        x = 0;
        y = 64;
        u8g2.drawStr(x, y, OffLine_ch);
        if (t<TemperatureP0A) u8g2.drawStr(x, y, TemperaturePointA0_p_m); 

         switch((millis() / 100) % 4) {
// Temperature 
   case 0:          
  { 
    String info_t;
    String paramT;
    String tmpr = "T(";
    String grad = "C):";
    const char degree {176};
     paramT = tmpr + degree + grad;
     char paramT_m [12];
     paramT.toCharArray(paramT_m, 12);

           info_t = String(t); 
         char info_t_m [12];
        info_t.toCharArray(info_t_m, 5);
//string 1
        u8g2.setFont(u8g2_font_9x18_mf);
        x = 16;
        y = u8g2.getAscent() - u8g2.getDescent();
         u8g2.drawStr(x, y, paramT_m);
//string 2 
       if ((t > -100) and (t < 100)) {      
        u8g2.setFont(u8g2_font_inb24_mf);
        x = (128 - u8g2.getStrWidth(info_t_m))/2;
        y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, info_t_m);
       }
      else noData();
   }         
                break;
 //Humidity             
        case 1:
      { 
        String info_h;
         info_h = String(h);  
        char info_h_m [12];
        info_h.toCharArray(info_h_m, 12);
//string 1
        u8g2.setFont(u8g2_font_9x18_mf);

        x = 16;
        y = u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, "H(%):");
//string 2        
    if ((h > -1) and (h < 100)){
        u8g2.setFont(u8g2_font_inb24_mf);
        x = (128 - u8g2.getStrWidth(info_h_m))/2;
        y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, info_h_m);
        }
      else noData();        
        }                   
                break;
//CO2 
   case 2:
      { 
        String info_co2;
        info_co2 = String(co2);
        char info_co2_m [12];
        info_co2.toCharArray(info_co2_m, 12);
//string 1
        u8g2.setFont(u8g2_font_9x18_mf);
        x = 8;
        y = u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, "CO2(ppm):");
//string 2        
      if  ((co2 > -1) and (co2 <= 2000)) {
        u8g2.setFont(u8g2_font_inb24_mf);
        x = (128 - u8g2.getStrWidth(info_co2_m))/2;
        y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, info_co2_m);
      }
      else noData();
   }                   
                break;                
//time, date 
   case 3:
      { 
//string 1
        u8g2.setFont(u8g2_font_9x18_mf);
        x = (128 - u8g2.getStrWidth(date_r))/2;
        y = u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, date_r);
//string 2        
        u8g2.setFont(u8g2_font_inb24_mf);
        x = (128 - u8g2.getStrWidth(time_r))/2;
        y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, time_r);
        }                   
                break;
     }     
        u8g2.sendBuffer();
}

void drawOffBlynk() {
      float TemperatureP0;
      char OffBlynk_ch[]{"   OffBlynk"};       

      TemperatureP0 = TemperaturePoint0 - Chs;
      dtostrf(TemperatureP0, 4, 1, Temperature0); //преобразование float в char

        String Temperature0_i;
        Temperature0_i = String(Temperature0); 
        char Temperature0_i_m [16];
        Temperature0_i.toCharArray(Temperature0_i_m, 16);
        u8g2.clearBuffer();
        String Temperature0_p;
        String onl1 = "OffBL T<";
        Temperature0_p = onl1 + Temperature0_i_m;
        char Temperature0_p_m [16];
        Temperature0_p.toCharArray(Temperature0_p_m, 16); 

        String Tmx_i;
        Tmx_i = String(Tmx); 
        char Tmx_i_m [16];
        Tmx_i.toCharArray(Tmx_i_m, 16);
        u8g2.clearBuffer();
        String Tmx_p;
        String onl2 = "OffBL T>";
        Tmx_p = onl2 + Tmx_i_m;
        char Tmx_p_m [16];
        Tmx_p.toCharArray(Tmx_p_m, 16); 

        String Cmx_i;
        Cmx_i = String(Cmx); 
        char Cmx_i_m [16];
        Cmx_i.toCharArray(Cmx_i_m, 16);
        u8g2.clearBuffer();
        String Cmx_p;
        String onl3 = "OnL CO2>";
        Cmx_p = onl3 + Cmx_i_m;
        char Cmx_p_m [16];
        Cmx_p.toCharArray(Cmx_p_m, 16);

        String Hmn_i;
        Hmn_i = String(Hmn); 
        char Hmn_i_m [16];
        Hmn_i.toCharArray(Hmn_i_m, 16);
        u8g2.clearBuffer();
        String Hmn_p;
        String onl4 = "OffBL H<";
        Hmn_p = onl4 + Hmn_i_m;
        char Hmn_p_m [16];
        Hmn_p.toCharArray(Hmn_p_m, 16);

//string 3
        u8g2.setFont(u8g2_font_9x18_mf);
        x = 0;
        y = 64;
        u8g2.drawStr(x, y, OffBlynk_ch);
        if ((hh>=HourPoint1) and (hh<=HourPoint2) and (t<TemperatureP0)) u8g2.drawStr(x, y, Temperature0_p_m); 
        else
        if (t > Tmax) u8g2.drawStr(x, y, Tmx_p_m); 
        else 
        if (co2 > Cmax) u8g2.drawStr(x, y, Cmx_p_m);  
        else  
        if (h < Hmin) u8g2.drawStr(x, y, Hmn_p_m);        

         switch((millis() / 100) % 4) {
// Temperature 
   case 0:          
      { 
        String info_t;
        String paramT;
        String tmpr = "T(";
        String grad = "C):";
        const char degree {176};
         paramT = tmpr + degree + grad;
         char paramT_m [12];
         paramT.toCharArray(paramT_m, 12);

           info_t = String(t); 
         char info_t_m [12];
        info_t.toCharArray(info_t_m, 5);
//string 1
        u8g2.setFont(u8g2_font_9x18_mf);
        x = 16;
        y = u8g2.getAscent() - u8g2.getDescent();
         u8g2.drawStr(x, y, paramT_m);
//string 2 
       if ((t > -100) and (t < 100)) {      
        u8g2.setFont(u8g2_font_inb24_mf);
        x = (128 - u8g2.getStrWidth(info_t_m))/2;
        y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, info_t_m);
       }
      else noData();
        }         
                break;
 //Humidity             
        case 1:
      { 
        String info_h;
         info_h = String(h);  
        char info_h_m [12];
        info_h.toCharArray(info_h_m, 12);
//string 1
        u8g2.setFont(u8g2_font_9x18_mf);

        x = 16;
        y = u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, "H(%):");
//string 2        
    if ((h > -1) and (h < 100)){
        u8g2.setFont(u8g2_font_inb24_mf);
        x = (128 - u8g2.getStrWidth(info_h_m))/2;
        y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, info_h_m);
        }
      else noData();        
        }                   
                break;
//CO2 
   case 2:
      { 
        String info_co2;
        info_co2 = String(co2);
        char info_co2_m [12];
        info_co2.toCharArray(info_co2_m, 12);
//string 1
        u8g2.setFont(u8g2_font_9x18_mf);
        x = 8;
        y = u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, "CO2(ppm):");
//string 2        
      if  ((co2 > -1) and (co2 <= 2000)) {
        u8g2.setFont(u8g2_font_inb24_mf);
        x = (128 - u8g2.getStrWidth(info_co2_m))/2;
        y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, info_co2_m);
      }
      else noData();
        }                   
                break;                
//time, date 
   case 3:
      { 
//string 1
        u8g2.setFont(u8g2_font_9x18_mf);
        x = (128 - u8g2.getStrWidth(date_r))/2;
        y = u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, date_r);
//string 2        
        u8g2.setFont(u8g2_font_inb24_mf);
        x = (128 - u8g2.getStrWidth(time_r))/2;
        y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, time_r);
        }                   
                break;
   }     
        u8g2.sendBuffer();
}

void drawBoot(String msg = "Loading...") {
        u8g2.clearBuffer();
        u8g2.setFont(u8g2_font_9x18_mf);
        x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
        y = 32 + u8g2.getAscent() / 2;
        u8g2.drawStr(x, y, msg.c_str());
        u8g2.sendBuffer();
}

void drawConnectionDetails(String ssid, String pass, String url) {
        String msg {""};
        u8g2.clearBuffer();

        msg = "Connect to WiFi:";
        u8g2.setFont(u8g2_font_7x13_mf);
        x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
        y = u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, msg.c_str());

        msg = "net: " + ssid;
        x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
        y = y + 1 + u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, msg.c_str());

        msg = "pw: "+ pass;
        x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
        y = y + 1 + u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, msg.c_str());

        msg = "Open browser:";
        x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
        y = y + 1 + u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, msg.c_str());

        // URL
        // u8g2.setFont(u8g2_font_6x12_mf);
        x = (128 - u8g2.getStrWidth(url.c_str())) / 2;
        y = y + 1 + u8g2.getAscent() - u8g2.getDescent();
        u8g2.drawStr(x, y, url.c_str());

        u8g2.sendBuffer();
}

bool loadConfigS(){
        Blynk.config(address);  
        Serial.print("e-mail: ");  
        Serial.println(  address );   

        Blynk.config(Tmx);  
        Serial.print("T max: ");  
        Serial.println(  Tmx );   

        Blynk.config(Cmx);  
        Serial.print("CO2 max: ");  
        Serial.println(  Cmx );

        Blynk.config(Temperature0);  
        Serial.print("Temperature 0: ");  
        Serial.println(  Temperature0 );   

        Blynk.config(Temperature1);  
        Serial.print("Temperature1: ");  
        Serial.println(  Temperature1 );   

        Blynk.config(Temperature2);  
        Serial.print("Temperature2: ");  
        Serial.println(  Temperature2 ); 

        Blynk.config(Hmn);  
        Serial.print("H min: ");  
        Serial.println(  Hmn );    

        Blynk.config(Hour1);  
        Serial.print("Hour 1: ");  
        Serial.println(  Hour1 );      

        Blynk.config(Hour2);  
        Serial.print("Hour 2: ");  
        Serial.println(  Hour2 ); 

        Blynk.config(tZ);  
        Serial.print("Time Zone: ");  
        Serial.println(  tZ );

        Blynk.config(blynk_token, "blynk-cloud.com", 8442);
        Serial.print("token: " );
        Serial.println(  blynk_token  );  
}

bool loadConfig() {
        Serial.println("Load config...");
        File configFile = SPIFFS.open("/config.json", "r");
        if (!configFile) {
                Serial.println("Failed to open config file");
                return false;
        }

        size_t size = configFile.size();
        if (size > 1024) {
                Serial.println("Config file size is too large");
                return false;
        }

        // Allocate a buffer to store contents of the file.
        std::unique_ptr<char[]> buf(new char[size]);

        // We don't use String here because ArduinoJson library requires the input
        // buffer to be mutable. If you don't use ArduinoJson, you may as well
        // use configFile.readString instead.
        configFile.readBytes(buf.get(), size);

        StaticJsonBuffer<200> jsonBuffer;
        JsonObject &json = jsonBuffer.parseObject(buf.get());

        if (!json.success()) {
                Serial.println("Failed to parse config file");
                return false;
        }

        // Save parameters
        strcpy(blynk_token, json["blynk_token"]);
        strcpy(address, json["address"]);
        strcpy(Tmx, json["Tmx"]);        
        strcpy(Cmx, json["Cmx"]);
        strcpy(Temperature0, json["Temperature0"]); 
        strcpy(Temperature1, json["Temperature1"]); 
        strcpy(Temperature2, json["Temperature2"]);       
        strcpy(Hmn, json["Hmn"]); 
        strcpy(Hour1, json["Hour1"]);
        strcpy(Hour2, json["Hour2"]);
        strcpy(tZ, json["tZ"]);
}

void configModeCallback (WiFiManager *wifiManager) {
        String url {"http://192.168.4.1"};
        printString("Connect to WiFi:");
        printString("net: " + ssid);
        printString("pw: "+ pass);
        printString("Open browser:");
        printString(url);
        printString("to setup device");

        drawConnectionDetails(ssid, pass, url);
}

void setupWiFi() {
        //set config save notify callback
        wifiManager.setSaveConfigCallback(saveConfigCallback);
        // Custom parameters
         WiFiManagerParameter custom_tZ("tZ", "Time Zone", tZ, 5);
         wifiManager.addParameter(&custom_tZ);        
         WiFiManagerParameter custom_Temperature0("Temperature0", "Temperature 0", Temperature0, 5);  
         wifiManager.addParameter(&custom_Temperature0);             
         WiFiManagerParameter custom_Hour1("Hour1", "Hour 1", Hour1, 5);  
         wifiManager.addParameter(&custom_Hour1);          
         WiFiManagerParameter custom_Temperature1("Temperature1", "Temperature 1", Temperature1, 5);  
         wifiManager.addParameter(&custom_Temperature1);
         WiFiManagerParameter custom_Hour2("Hour2", "Hour 2", Hour2, 5);
         wifiManager.addParameter(&custom_Hour2);          
         WiFiManagerParameter custom_Temperature2("Temperature2", "Temperature 2", Temperature2, 5);  
         wifiManager.addParameter(&custom_Temperature2);        
         WiFiManagerParameter custom_Cmx("Cmx", "Cmax", Cmx, 7);
         wifiManager.addParameter(&custom_Cmx);       
         WiFiManagerParameter custom_Hmn("Hmn", "Hmin", Hmn, 5);  
         wifiManager.addParameter(&custom_Hmn); 
         WiFiManagerParameter custom_Tmx("Tmx", "Tmax", Tmx,5);
         wifiManager.addParameter(&custom_Tmx);                   
         WiFiManagerParameter custom_address("address", "E-mail", address, 64);
         wifiManager.addParameter(&custom_address);
         WiFiManagerParameter custom_blynk_token("blynk_token", "Blynk Token", blynk_token, 34);
         wifiManager.addParameter(&custom_blynk_token);         

         wifiManager.setAPCallback(configModeCallback);

        wifiManager.setTimeout(180);

        if (!wifiManager.autoConnect(ssid.c_str(), pass.c_str())) {
        a++;
        Serial.println("mode OffLINE :(");
        loadConfigS();
        synchronClockA();         
        }

        //save the custom parameters to FS
        if (shouldSaveConfig) {
                Serial.println("saving config");
                DynamicJsonBuffer jsonBuffer;
                JsonObject &json = jsonBuffer.createObject();

                json["blynk_token"] = custom_blynk_token.getValue();                  
                json["address"] = custom_address.getValue(); 
                json["Tmx"] = custom_Tmx.getValue(); 
                json["Cmx"] = custom_Cmx.getValue();
                json["Temperature0"] = custom_Temperature0.getValue(); 
                json["Temperature1"] = custom_Temperature1.getValue();
                json["Temperature2"] = custom_Temperature2.getValue(); 
                json["Hmn"] = custom_Hmn.getValue(); 
                json["Hour1"] = custom_Hour1.getValue();    
                json["Hour2"] = custom_Hour2.getValue();
                json["tZ"] = custom_tZ.getValue();                

                File configFile = SPIFFS.open("/config.json", "w");
                if (!configFile) {
                        Serial.println("failed to open config file for writing");
                }

                json.printTo(Serial);
                json.printTo(configFile);
                configFile.close();
                //end save
        }

        //if you get here you have connected to the WiFi
        Serial.println("WiFi connected");
        Serial.print("IP address: ");
        Serial.println(WiFi.localIP());
}

BLYNK_WRITE(V10) {
        if (param.asInt() == 1)
         {
          buttonBlynk = true; 
          Blynk.virtualWrite(V10, HIGH);
          drawBoot("Thermo ON"); 
          }
         else 
          {
          buttonBlynk = false; 
          Blynk.virtualWrite(V10, LOW);
          drawBoot("Thermo OFF");
          }
}

void mailer() {
       // wait for WiFi connection
       if((WiFiMulti.run() == WL_CONNECTED)) {
        HTTPClient http;
        Serial.print("[HTTP] begin...\n");
        http.begin("http://skorovoda.in.ua/php/aqm42.php?mymail="+String(address)+"&t="+String(t) +"&h="+String(h)+"&co2="+String(co2)+"&ID="+String(ESP.getChipId()));
        Serial.print("[HTTP] GET...\n");
        // start connection and send HTTP header
        int httpCode = http.GET();

        // httpCode will be negative on error
        if(httpCode > 0) {
            // HTTP header has been send and Server response header has been handled
            Serial.printf("[HTTP] GET... code: %d\n", httpCode);

            // file found at server
            if(httpCode == HTTP_CODE_OK) {
                String payload = http.getString();
                Serial.println(payload);
            }
        } else {
            Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
        }
        http.end();
    }
}

void HystTemperatureA() {
      float TemperaturePointA0Mn, TemperaturePointA0Pl;

       TemperaturePointA0Mn = TemperaturePointA0-Chs;
       TemperaturePointA0Pl = TemperaturePointA0+Chs;

        if (t<TemperaturePointA0Mn) {  
        if (millis() - TimeTransmitMax > 120000){ 
        TimeTransmitMax = millis(); 
        transmitter.send(B11111110, 8);
        Serial.println ("t<TemperaturePointA0Mn Thermostat ON");
        }
       }
       else if (millis() - TimeTransmitMax > 120000)
       { 
        TimeTransmitMax = millis(); 
        transmitter.send(B10000000, 8);
        Serial.println ("t>TemperaturePointA0Mn Thermostat OFF");
        }
       if (t<TemperaturePointA0Pl) {  
        if (millis() - TimeTransmitMax > 120000){ 
        TimeTransmitMax = millis(); 
        transmitter.send(B11111110, 8);
        Serial.println ("t<TemperaturePointA0Pl Thermostat ON");
        }
       }
       else if (millis() - TimeTransmitMax > 120000)
       { 
        TimeTransmitMax = millis(); 
        transmitter.send(B10000000, 8);
        Serial.println ("t>TemperaturePointA0Pl Thermostat OFF");
        }
}

void HystTemperature() {
float TemperaturePoint0Mn, TemperaturePoint0Pl;

         TemperaturePoint0Mn = TemperaturePoint0-Chs;
         TemperaturePoint0Pl = TemperaturePoint0+Chs;

          if (t<TemperaturePoint0Mn) {  
          if (millis() - TimeTransmitMax > 120000){ 
          TimeTransmitMax = millis(); 
          transmitter.send(B11111110, 8);
          Serial.println ("t<TemperaturePoint0Mn Thermostat ON");
          }
         }
         else if (millis() - TimeTransmitMax > 120000) { 
          TimeTransmitMax = millis(); 
          transmitter.send(B10000000, 8);
          Serial.println ("t>TemperaturePoint0Mn Thermostat OFF");
          }
          if (t<TemperaturePoint0Pl) {  
          if (millis() - TimeTransmitMax > 120000){ 
          TimeTransmitMax = millis(); 
          transmitter.send(B11111110, 8);
          Serial.println ("t<TemperaturePoint0Pl Thermostat ON");
          }
         }
         else if (millis() - TimeTransmitMax > 120000)
         { 
          TimeTransmitMax = millis(); 
          transmitter.send(B10000000, 8);
          Serial.println ("t>TemperaturePoint0Pl Thermostat OFF");
          }
}

void TransmitterA(){
        transmitter.send(B10101010, 8);  //B10101010 - признак работающего передатчика  
        HystTemperatureA();
} 

void Transmitter(){
          transmitter.send(B10101010, 8);  //B10101010 - признак работающего передатчика  

          if (n>=24) n = 0; 
          if (m>=60) m = 0;

          progr = 0;

          if ((hh >= HourPoint1) and (hh < HourPoint2)){
            progr = 1;
             if (mm >= MinPoint1) progr = 1;
             if (mm < MinPoint2) progr = 1;
          }
          else if (hh >= HourPoint2) {
             progr = 2;
             if (mm >= MinPoint2) progr = 2;
          }

          if (buttonBlynk==true)           {
          Serial.println ("BLynk: Термостат ВКЛ"); 

          if (progr == 0) {
          TemperaturePoint0 = TemperaturePoint0;
          HystTemperature();
          Serial.println ("Термостатирование: t = " + String(TemperaturePoint0));
           }
          else if (progr == 1) {
          TemperaturePoint0 = TemperaturePoint1;
          HystTemperature();
          Serial.println ("Термостатирование: t = " + String(TemperaturePoint0));
           }
          else if (progr == 2){
          TemperaturePoint0 = TemperaturePoint2;
          HystTemperature();
          Serial.println ("Термостатирование: t = " + String(TemperaturePoint0));
            }
          }
          else { 
            transmitter.send(B10000000, 8);
            Serial.println ("BLynk: Термостат ВЫКЛ");
            }  

          if (co2 > Cmax)  { 
              transmitter.send(B11111101, 8);
              Serial.println("co2 > Cmax"); }
              else transmitter.send(B00000010, 8); 

          if (h < Hmin) { 
                 transmitter.send(B11111011, 8);
                 Serial.println("h < Hmin"); }
                 else transmitter.send(B00000100, 8);

           if (t > Tmax)  {
              transmitter.send(B11110111, 8);
              Serial.println("t > Tmax"); }
              else  transmitter.send(B00001000, 8);
          }           

void connectBlynk(){     
       if(String(blynk_token)== "Blynk token"){
       drawBoot("OFFBLYNK!");
       delay (3000);
        } else {
        drawBoot("Connect. Blynk");
        Serial.println("Connecting to blynk...");
        while (Blynk.connect() == false) {
        delay(500);
        Serial.println("Connecting to blynk...");
        }    
    } 
}

void setup() {
      // factoryReset();   //форматирование RAM

        mySerial.begin(9600);
        Serial.begin(115200);

        transmitter.enableTransmit(2);  

        u8g2.begin();   // инициализация экрана

        drawBoot("Loading...");

        // инициализация файловой системы
        if (!SPIFFS.begin()) {
        Serial.println("Failed to mount file system");
        ESP.reset(); }

        // загрузка параметров
        drawBoot("Connect. WiFi");
        setupWiFi();

        timerCO2 = timer.setInterval(15000, readCO2); 
        buttonBlynk = true;    

       if(a == 1){  
        // Load config
        drawBoot("Load Config");
        if (!loadConfig()) {
                Serial.println("Failed to load config");
                factoryReset();
        } else {
                Serial.println("Config loaded");
        }
        Blynk.config(address);  
        Serial.print("e-mail: ");  
        Serial.println(address);   

        Blynk.config(Tmx);  
        Serial.print("T max: ");  
        Serial.println(Tmx);   

        Blynk.config(Cmx);  
        Serial.print("CO2 max: ");  
        Serial.println(Cmx);

        Blynk.config(Temperature0);  
        Serial.print("Temperature 0: ");  
        Serial.println(Temperature0);   

        Blynk.config(Temperature1);  
        Serial.print("Temperature1: ");  
        Serial.println(Temperature1);   

        Blynk.config(Temperature2);  
        Serial.print("Temperature2: ");  
        Serial.println(Temperature2); 

        Blynk.config(Hmn);  
        Serial.print("H min: ");  
        Serial.println(Hmn);    

        Blynk.config(Hour1);  
        Serial.print("Hour 1: ");  
        Serial.println(Hour1);      

        Blynk.config(Hour2);  
        Serial.print("Hour 2: ");  
        Serial.println(Hour2); 

        Blynk.config(tZ);  
        Serial.print("Time Zone: ");  
        Serial.println(tZ);        

        Blynk.config(blynk_token, "blynk-cloud.com", 8442);
        Serial.print("token: " );
        Serial.println(blynk_token);

        //преобразование char в float
        Tmax = atof (Tmx);   
        Cmax = atof (Cmx);
        TemperaturePoint0 = atof (Temperature0); 
        TemperaturePoint1 = atof (Temperature1);  
        TemperaturePoint2 = atof (Temperature2);
        Hmin = atof (Hmn);  
        HourPoint1 = atof (Hour1); 
        HourPoint2 = atof (Hour2); 
        tZone = atof (tZ);

        //синхронизация часов
        drawBoot("Clock synchr.");
        synchronClock(); 

       //периодичность вызова функций
       timerCO2 = timer.setInterval(15000, readCO2); 
       timerBl = timer.setInterval(5000, sendToBlynk);

        connectBlynk();   // подключение до Blynk     
        Blynk.virtualWrite(V10, HIGH); //установка кнопки V10 в состояние ВКЛ
        buttonBlynk = true;
        }        
}

void loop(){
       if (a == 2) {  
       Serial.println(":( OffLINE");
       timer.run();
       Clock();
       sendMeasurements();
       TransmitterA(); 
       drawOff();
       delay(1000);  
       } 
       else if (a == 1)  {  
       Serial.println(":) OnLINE"); 
       timer.run();
       Clock();
       Blynk.run();
       BLYNK_WRITE(V10);    
       Transmitter();
       sendMeasurements();

      if(String(blynk_token) == "Blynk token") drawOffBlynk(); else drawOn();   

       if (j>=24) j =0;    
       if (hh == j){
          if ((mm==30) and ((ss<30) )){
            if ((t > Tmax) or (co2 > Cmax) or (h < Hmin) or ((progr == 0) and (t<(TemperaturePoint0-1.0)) or ((progr == 1) and (t<(TemperaturePoint1-1.0)) or ((progr == 2) and (t<(TemperaturePoint2-1.0)))))) mailer();                    
          }
        }  
       j++;                   
   }
}

Если хотя бы один из параметров воздуха находится за пределами запрограммированных пороговых значений, то устройство в половине каждого часа отравляет на е-мейл письмо:



Сообщения на е-мейл отправляются php-скриптом. Скрипт загружен на мой почтовый сервер. Он понадобится, если планируется отправка сообщений с другого ресурса.


php-скрипт
<?php
// тест -  http://skorovoda.in.ua/php/aqm42.php?mymail=my_login@my.site.net&t=22.2&h=55&co2=666
$EMAIL=0;
$TEMPER=0;
$vlaga=0;
$carbon=0;
$device=0;
$EMAIL=$_GET["mymail"];
$device=$_GET["ID"];
echo $EMAIL;
$TEMPER=$_GET["t"];
$vlaga=$_GET["h"];
$carbon=$_GET["co2"];
$mdate = date("H:i d.m.y");
echo <<<END
<p>Температура:  $TEMPER °С<p>
<p>Влажность: $vlaga %<p>
<p>Содержание углекислого газа:  $carbon ppm<p>
<p>--------------------<p>
<p>Метеостанция №:  $device<p>
END;
echo <<<END
<p>$mdate</p>
END;
mail($EMAIL, "Air Quality Monitor " .$device. " v.051018","   Данное сообщение сформировано монитором качества воздуха №" .$device. " автоматически.  Один или несколько параметров воздуха в помещении (температура, влажность или содержание углекислого газа) находятся за пределами заданных граничных значений. === Температура: ".$TEMPER."°C === "."Влажность: ".$vlaga."% === "."Содержание углекислого газа: ".$carbon." ppm === "."Проанализируйте информацию! === Время, дата: ".$mdate,"From: my_sensors@air-monitor.info \n")

?>

С 10 августа 2020г. будет прекращена аренда домена и дискового пространства под этот домен, поэтому попрошу поискать другое место для размещения php-скрипта. Извинения тем, кто уже пользуется моим почтовым сервером.



Контактор



Управление в контакторе осуществляет модуль Arduino Pro Mini. Он принимает сигнал с RF приемника и вырабатывает сигналы превышения пороговых значений параметров воздуха.



Напряжение питания всех узлов контактора 5В поступает с адаптера AC/DC HLK-PM01.


В современных бытовых газовых котлах нормально разомкнутые контакты реле (красный, желтый провода на схеме) подключить вместо съемной перемычки в котле.


Сигналы с выводов контроллера 6 (h >Hmin), 5 (co2 > CO2max), 3 (t > Tmax) можно использовать для организации автоматического увлажнения, принудительной вентиляции или кондиционирования воздуха. Преимущество заключается в том, что отпадает необходимость в прокладке кабеля для передачи сигнала управления с датчика на ту или иную систему – достаточно разместить контактор неподалеку от одного из концов провода питания или управления системой.


Я, например, планирую кроме управления котлом отопления подключить к контактору еще и кухонную вытяжку — котел и вытяжка расположены рядом.


Скетч контактора для загрузки в Arduino Pro Mini — под спойлером.


скетч контактора
/*
 * Беспроводной программируемый по Wi-Fi комнатный термостат с монитором качества воздуха и другими полезными функциями (контактор)
 */

#include <RCSwitch.h>     //https://github.com/sui77/rc-switch
RCSwitch mySwitch = RCSwitch();

void setup() {
      pinMode(13, OUTPUT);
      pinMode(3, OUTPUT);
      pinMode(4, OUTPUT);
      pinMode(5, OUTPUT);
      pinMode(6, OUTPUT);

      digitalWrite(3, HIGH);
      digitalWrite(4, HIGH);
      digitalWrite(5, HIGH);
      digitalWrite(6, HIGH);
      digitalWrite(13, LOW);

      mySwitch.enableReceive(0);
    }

void loop() {
       if( mySwitch.available() ){
        int value = mySwitch.getReceivedValue();

              //t < Tmin
        if(value == B11111110) digitalWrite(4, LOW);
        else if (value == B10000000) digitalWrite(4, HIGH);

                //co2 > Cmax  
        if(value == B11111101) digitalWrite(5, LOW);
        else if (value == B00000010) digitalWrite(5, HIGH);

             //h < Hmin
        if(value == B11111011) digitalWrite(6, LOW);
        else if (value == B00000100) digitalWrite(6, HIGH);

            //t > Tmax
        if(value == B11110111)digitalWrite(3, LOW);
        else if (value == B00001000) digitalWrite(3, HIGH);

        //светодиод D13 Arduino - указывает на наличие связи передатчик-приемник (мигает - связь есть)
        if(value == B10101010) digitalWrite(13, HIGH);  // B10101010 - код включенного передатчика, генерируется в анализаторе без условий  
        else  digitalWrite(13, LOW); 

        mySwitch.resetAvailable();
  }
}


Запуск термостата в работу


Пришло время включить термостат.


Шаг 1:


Сначала включим анализатор.



Вначале надо набраться терпения и, ничего не предпринимая, выждать 3 минуты. Термостат автоматически перейдет в автономный режим работы – без подключения по Wi-Fi к домашней сети и Интернету. Через 3 минуты на экране анализатора в трех строках начнет мелькать все, что ворочает термостат.



Первые две строки на экране не требуют комментариев. В третьей строке – режим работы термостата (Offline, Online или OffBlynk) и информация о выходе за пределы установленных пороговых значений параметров воздуха. Например, Offline CO2>1000 — термостат работает в автономном режиме, а измеренное содержание СО2 выше заданного порогового значения 1000 ppm.


Часы в автономном режиме будут показывать неправильное время. Они еще не синхронизированы с сервером точного времени, а также не выполнен ввод часового пояса – это в следующем шаге.


В автономном режиме установлена температура термостатирования 21°С на протяжении суток.


Шаг 2:


Освоившись с автономным режимом, выключим и снова включим адаптер AC/DC анализатора. На экране появится знакомое сообщение, к которому успели привыкнуть за три минуты ожидания автономного режима.


Устройство подняло точку доступа am-5108. Найдем эту точку в списке доступных сетей и подключимся к ней, пароль – на экране. Затем откроем в браузере страницу http://192.168.4.1.



Нажмем кнопку Configure WiFi (No Scan). Откроется страница с формой настроек термостата:



Эта же форма с незаполненными полями и комментариями:



Укажем в форме имя и пароль своей домашней сети, ключ идентификации BLynk, электронную почту. Изменим заданные по умолчанию часовой пояс, время (часы) и температуру для временных точек, а также пороговые значения температуры, влажности и содержания СО2.


Сутки двумя временными точками разбиты на три временных диапазона — первый: с 00 час 00 мин до точки 1 (Hour 1, Minute 1), второй: с точки 1 (Hour 1, Minute 1) до точки 2 (Hour 2, Minute 2) и третий: с точки 2 (Hour 2, Minute 2) до 00 час 00 мин. Полей для ввода минут на форме нет, минуты для точек 1,2 можно изменить в скетче (переменные MinPoint1, MinPoint2). В каждом из трех временных диапазонов можно задать свою температуру термостатирования — Temperature 0, Temperature 1 и Temperature 2. Если планируется поддерживать постоянной одну и ту же температуру в течение суток, то достаточно задать значение Temperature 0, а поля для точек 1,2 оставить пустыми.


При выборе пороговых значений параметров воздуха ориентируйтесь на показатели, которые я нашел в Интернете:


  1. Комфортная температура ночью во время сна 19…21°С, днем — 22…23°С.
  2. Оптимальной относительной влажностью в холодное время года считается влажность 30…45%, а в теплое – 30…60%. Предельные максимальные показатели влажности: зимой она не должна превышать 60%, а летом – 65%.
  3. Максимальный уровень содержания углекислого газа в помещениях не должен превышать 1000 ppm. Рекомендованный уровень для спален, детских комнат – не более 600 ppm. Отметка 1400 ppm – предел допустимого содержания СО2 в помещении. Если его больше, то качество воздуха считается низким.

По умолчанию суточная программа термостатирования (днем – высокая температура, ночью – низкая) задана из предположения, что днем кто-то из жильцов находится в помещении, например, работает на дому. Программу легко изменить под свои реалии.


Поле e-mail можно не заполнять. Тогда предоставленная возможность получать письма на электронную почту о выходе параметров воздуха за пороговые значения будет утрачена. Без введенного ключа Blynk’а – невозможно управлять термостатом и получать информацию о параметрах воздуха на удалении. Впрочем, термостат не «растеряется», если останутся незаполненными поля с предельными значениями параметров воздуха, тогда за ним останется только одна функция: термостатирование.


И еще. Все числа вводите, пожалуйста, в формате переменных с плавающей запятой, далее преобразование в нужный формат выполняются в скетче. Исключение: временные точки 1,2 (час) — формат целого числа.


После сохранения настроек в памяти ESP8266 (кнопка Save), анализатор подключится к сети и начнет работу.


Если ошиблись (бывает!) или решили изменить настройки, снова придется дважды загрузить скетч в ESP8266. Первый раз – с раскомментированной в Setup’e строкой factoryReset(); а второй — с закомментированной, затем повторить шаг 2.


Шаг 3:


Теперь можно включить контактор.


При устойчивой радиосвязи между анализатором и контактором – светодиод D13 на плате Arduino мигает с частотой около 1Гц.


Если контактор принял с анализатора команду на включение обогревательного прибора или отопительной системы — замкнутся нормально разомкнутые контакты реле и загорится соответствующий ему светодиод на модуле реле.


Если нет проблем с «холостым ходом» контактора, то подключаем обогревательный прибор или электронику системы отопления. Обогревательный прибор следует подключать проводом определенного сечения. Удельный показатель для расчета сечения медного провода — 5 А/мм2.



Шаг 4:


Пришло время запустить на смартфоне приложение Blynk. В Интернете много информации о приложении Blynk – нет смысла ее повторять.


Переменные для Blynk (чтобы не искать их в скетче анализатора): температура — V1, влажность – V2, содержание СО2V3, температура термостатирования – V4, виртуальная кнопка — V10.


На моем смартфоне интерфейс Blynk’a (его можно изменять) имеет вид:



На графике – измеренная температура (белый), температура термостатирования (желтый), интервал времени – сутки. Переменные влажности и содержания СО2 на график не выведены, поскольку две дополнительные шкалы сильно ограничивают поле графика, где можно рассмотреть сами кривые.


Сигнал с виртуальной кнопки ТЕРМОСТАТ формируется только в момент нажатия на кнопку. При нажатии на кнопку на экране анализатора мелькает сообщение Тhermo OFF! или Thermo ON! – в зависимости от предыдущего состояния кнопки. Это сообщение актуально при тестировании термостата.


Скриншот ниже иллюстрирует процесс обогрева тепловентилятором мощностью 2 кВт/час помещения площадью около 5-ти квадратных метров с начальной температурой 16°С. Здесь — температура (желтый), влажность (синий) и содержание СО2 (красный).



Синхронная с пилой температуры зубчатая кривая влажности на графике — еще одно подтверждение известному факту, что открытый ТЭН сушит воздух, а пики на кривой содержания СО2 – свидетельство моих кратковременных визитов в помещение.


Теперь протестируем работу системы оповещений на е-мейл. Введем в адресную строку браузера закомментированную строку с http-адресом из кода php-скрипта. Если вы не забыли в настройках указать свой е-мейл, а в окне браузера — информация, как на картинке ниже, то проблем с приемом оповещений скорее всего не будет. Тест особенно полезен при переносе php-скрипта с моего сервера на другой.



Намерения


В дальнейшем планирую поработать над усовершенствованием термостата (как говорят, совершенству нет предела!)


Задач — уйма:


  • Дополнить термостат датчиком температуры с беспроводной связью для измерения температуры на улице.
  • Заменить пару приемник-передатчик RF другой парой с большей дальностью связи при напряжении питания не более 3В. В идеале – хотелось бы собрать анализатор с питанием от двух батареек АА на протяжении отопительного сезона.
  • Уйти от ручного форматирования памяти ESP8266 перед каждым изменением настроек термостата через повторную загрузку скетча.
  • Расширить программируемый цикл работы термостата с суточного до недельного.
  • Заменить монохромный экран на цветной и с большим разрешением. Это позволит показывать всю информацию о работе термостата одним кадром, а выход параметров воздуха за пределы установленных границ – изменением цвета.
  • Затем заняться печатными платами и презентабельным внешним видом термостата.

Что еще можно улучшить? Принимаются предложения, замечания. Прислушаюсь к конструктивной критике.


Выводы


  • Благодаря подключению к Интернету, функционал термостата значительно расширился. Кроме основной функции, в нем реализован целый ряд других: от отправки оповещений на е-мейл — до возможности автоматического поддержания качества воздуха в помещении.
  • В термостате появилось новое качество: им можно управлять через Интернет.
  • Радует легкость, с которой программируется термостат: требуется лишь заполнить форму на странице браузера.
  • Появилась возможность сохранять в памяти термостата персональные данные, как это делается, например, в роутерах.

Внимание!
Автор не несет ответственности за возможный негатив при повторении проекта. Вы отвечаете за все, что делаете.


P.S.


1. Макет из проекта достойно занял место старого термостата, поскольку тот в четвертом отопительном сезоне стал изредка «забывать» включать-выключать систему отопления.


2. О подходах в решении некоторых из перечисленных выше задач можно познакомиться в других моих статьях на Хабре:





Мои закладки по теме с Хабра


Теги:
Хабы:
+19
Комментарии 35
Комментарии Комментарии 35

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн