Володимир Бойчук

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

В системі автономного опалення моєї квартири працює серійний бездротовий кімнатний термостат. Система, звичайно, функціонує і без нього: термостат був придбаний для економії споживання газу та підвищення комфорту.

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

Що в результаті вийшло - читайте далі. Сподіваюся, крім мене, проект буде цікавий іншим.

Знайомство

Можливості та характеристики:

- Зв'язок між вузлами термостата здійснюється по ефіру на радіочастоті.

- Протягом доби термостат підтримує постійними три задані значення температури.

- Налаштування термостата (програма роботи, граничні параметри повітря, інші) задаються дистанційно з форми в браузері через 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
Всього: 49,80

Якщо планується збирати термостат з мінімальними габаритами, то потрібно замінити 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 знаходиться під спойлером. Щоб розгорнути спойлер - натисніть лівою кнопкою мишки на напис "скетч аналізатора" нижче.

скетч аналізатора
  1. /*
  2. Бездротовий програмований по Wi-Fi кімнатний термостат з монітором якості повітря та іншими корисними функціями  (аналізатор)
  3. http://skorovoda.in.ua/thermostat/
  4.  */
  5.  
  6. #include <FS.h>
  7. #include <Arduino.h>
  8. #include <ESP8266WiFi.h>          //https://github.com/esp8266/Arduino
  9.  
  10. // Wifi Manager
  11. #include <DNSServer.h>
  12. #include <ESP8266WebServer.h>
  13. #include <WiFiManager.h>         //https://github.com/tzapu/WiFiManager
  14.  
  15. //e-mail
  16. #include <ESP8266WiFiMulti.h>     //https://github.com/esp8266/Arduino/blob/master/libraries/ESP8266WiFi/src/ESP8266WiFiMulti.h
  17. #include <ESP8266HTTPClient.h>
  18. ESP8266WiFiMulti WiFiMulti;
  19. char address[64] {"e-mail"};    //e-mail, address
  20.  
  21. // HTTP requests
  22. #include <ESP8266HTTPClient.h>
  23.  
  24. // OTA updates
  25. #include <ESP8266httpUpdate.h>
  26. // Blynk
  27. #include <BlynkSimpleEsp8266.h>
  28.  
  29. // Debounce
  30. #include <Bounce2.h> //https://github.com/thomasfredericks/Bounce2
  31.  
  32. // JSON
  33. #include <ArduinoJson.h>          //https://github.com/bblanchon/ArduinoJson
  34.  
  35. //clock
  36. #include <pgmspace.h>
  37. #include <TimeLib.h>
  38. #include <WiFiUdp.h>
  39. #include <Wire.h>
  40. #include <RtcDS3231.h>       //https://github.com/Makuna/Rtc
  41. RtcDS3231<TwoWire> Rtc(Wire);
  42. #define countof(a) (sizeof(a) / sizeof(a[0]))
  43.  
  44. //timer
  45. #include <SimpleTimer.h>
  46. SimpleTimer timer; //  ссылка на таймер
  47. unsigned int timerCO2; //период опроса MH-Z19
  48. unsigned int timerBl;   //период отправки данных на Blynk
  49. unsigned int timerMail; //период отправки сообщений на емейл
  50.  
  51. // GPIO Defines
  52. #define I2C_SDA 4 // D2 -  OLED
  53. #define I2C_SCL 5 // D1 -  OLED
  54. #define DHTPIN 12  //D6 cp2102
  55.  
  56. // Humidity/Temperature
  57. #include <DHT.h>
  58. #define DHTTYPE DHT22     // DHT 22
  59. DHT dht(DHTPIN, DHTTYPE);
  60.  
  61. #define mySerial Serial
  62.  
  63. // Use U8g2 for i2c OLED Lib
  64. #include <SPI.h>
  65. #include <U8g2lib.h>
  66. U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, I2C_SCL, I2C_SDA, U8X8_PIN_NONE);
  67. byte x {0};
  68. byte y {0};
  69.  
  70. // Blynk token
  71. char blynk_token[33] {"Blynk token"};
  72.  
  73. //Transmitter
  74. #include <RCSwitch.h>     
  75. RCSwitch transmitter = RCSwitch();   
  76. unsigned long TimeTransmitMax; // переменная для хранения точки отсчета времени передачи сигнала ВКЛ/ВЫКЛ передатчиком
  77.  
  78. // Setup Wifi connection
  79. WiFiManager wifiManager;
  80.  
  81. // Network credentials
  82. String ssid {"am-5108"};
  83. String pass {"vb" +  String(ESP.getFlashChipId())};
  84.  
  85. //flag for saving data
  86. bool shouldSaveConfig = false;
  87.  
  88. //переменные
  89. float  t {-100};  //температура
  90. int h {-1};    //влажность
  91. int co2 {-1};  //содержание co2
  92. float Chs = 0.2;  //чувствительность (гистерезис) термостата по температуре (диапазон: 0.1(большая тепловая инерция) - 0.4 (малая тепловая инерция))
  93. char Tmx[]{"25.0"},  Hmn[]{"35"}, Cmx[]{"1000"},  tZ[]{"2.0"}; //пороговые значения t, h и co2, час. пояс
  94. float Cmax, Tmax,  Hmin, tZone;
  95. char Temperature0[]{"20.0"}, Temperature1[]{"22.0"}, Temperature2[]{"19.0"};//температура стабилизации термостата во временных интервалах
  96. float TemperaturePoint0, TemperaturePoint1, TemperaturePoint2, TemperaturePoint1Mn, TemperaturePoint2Mn, TemperaturePoint1Pl, TemperaturePoint2Pl; 
  97. float TemperaturePointA0 = 21.0;  //температура стабилизации термостата в автономном режиме
  98. char Hour1[]{"6"}, Hour2[]{"22"};  //временные точки термостата, час
  99. float HourPoint1, HourPoint2; 
  100. float MinPoint1 = 0, MinPoint2 = 0;
  101. int n, j, m; //счетчик часов, минут
  102. int progr = 0; //счетчик программ работы термостата во времени суток
  103. int timeSummerWinter = 0;  // летнее(1)/зимнее(0) время
  104. int a = 1; //режим работы термостата: 1 - онлайн, 2 - автономный
  105. bool buttonBlynk = true; //признак ВКЛ(true)/ВЫКЛ(falce) виртуальной кнопки V(10) Blynk 
  106.  
  107. //NTP, clock  
  108. uint8_t hh,mm,ss;  //containers for current time
  109. char time_r[9];
  110.  char date_r[12];
  111. // NTP Servers:
  112. //static const char ntpServerName[] = "us.pool.ntp.org";
  113. static const char ntpServerName[] = "time.nist.gov";
  114.  
  115. WiFiUDP Udp;
  116. unsigned int localPort = 2390;  // local port to listen for UDP packets
  117.  
  118. time_t getNtpTime();
  119. void digitalClockDisplay();
  120. void printDigits(int digits);
  121. void sendNTPpacket(IPAddress &address);
  122.  
  123. void digitalClockDisplay()
  124. {
  125.   // digital clock display of the time
  126.   Serial.print(hour());
  127.   printDigits(minute());
  128.   printDigits(second());
  129.   Serial.print(" ");
  130.   Serial.print(day());
  131.   Serial.print(".");
  132.   Serial.print(month());
  133.   Serial.print(".");
  134.   Serial.print(year());
  135.   Serial.println(); 
  136. }
  137.  
  138. void printDigits(int digits)
  139. {
  140.   // utility for digital clock display: prints preceding colon and leading 0
  141.   Serial.print(":");
  142.   if (digits < 10)
  143.     Serial.print('0');
  144.   Serial.print(digits);
  145. }
  146. //NTP code
  147. const int NTP_PACKET_SIZE = 48; // NTP time is in the first 48 bytes of message
  148. byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold incoming & outgoing packets
  149. time_t getNtpTime()  { 
  150.   int tZoneI;
  151.   tZoneI = (int)tZone;
  152.  
  153.   IPAddress ntpServerIP; // NTP server's ip address
  154.  
  155.   while (Udp.parsePacket() > 0) ; // discard any previously received packets
  156.   Serial.println("Transmit NTP Request");
  157.   // get a random server from the pool
  158.   WiFi.hostByName(ntpServerName, ntpServerIP);
  159.   Serial.print(ntpServerName);
  160.   Serial.print(": ");
  161.   Serial.println(ntpServerIP);
  162.   sendNTPpacket(ntpServerIP);
  163.   uint32_t beginWait = millis();
  164.   while (millis() - beginWait < 1500) {
  165.     int size = Udp.parsePacket();
  166.     if (size >= NTP_PACKET_SIZE) {
  167.       Serial.println("Receive NTP Response");
  168.       Udp.read(packetBuffer, NTP_PACKET_SIZE);  // read packet into the buffer
  169.       unsigned long secsSince1900;
  170.       // convert four bytes starting at location 40 to a long integer
  171.       secsSince1900 =  (unsigned long)packetBuffer[40] << 24;
  172.       secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
  173.       secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
  174.       secsSince1900 |= (unsigned long)packetBuffer[43];
  175.       return secsSince1900 - 2208988800UL + tZoneI * SECS_PER_HOUR + timeSummerWinter * SECS_PER_HOUR;  //tZoneI
  176.     }
  177.   }
  178.   Serial.println("No NTP Response (:-()");
  179.   return 0; // return 0 if unable to get the time
  180. }
  181.  
  182. // send an NTP request to the time server at the given address
  183. void sendNTPpacket(IPAddress &address)
  184. {
  185.   // set all bytes in the buffer to 0
  186.   memset(packetBuffer, 0, NTP_PACKET_SIZE);
  187.   // Initialize values needed to form NTP request
  188.   // (see URL above for details on the packets)
  189.   packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  190.   packetBuffer[1] = 0;     // Stratum, or type of clock
  191.   packetBuffer[2] = 6;     // Polling Interval
  192.   packetBuffer[3] = 0xEC;  // Peer Clock Precision
  193.   // 8 bytes of zero for Root Delay & Root Dispersion
  194.   packetBuffer[12] = 49;
  195.   packetBuffer[13] = 0x4E;
  196.   packetBuffer[14] = 49;
  197.   packetBuffer[15] = 52;
  198.   // all NTP fields have been given values, now
  199.   // you can send a packet requesting a timestamp:
  200.   Udp.beginPacket(address, 123); //NTP requests are to port 123
  201.   Udp.write(packetBuffer, NTP_PACKET_SIZE);
  202.   Udp.endPacket();
  203. }
  204.  
  205. void synchronClockA() 
  206.  {
  207.    WiFiManager wifiManager;
  208.   Rtc.Begin();
  209.  
  210.   Serial.print("IP number assigned by DHCP is ");
  211.   Serial.println(WiFi.localIP());
  212.   Serial.println("Starting UDP");
  213.   Udp.begin(localPort);
  214.   Serial.print("Local port: ");
  215.   Serial.println(Udp.localPort());
  216.   Serial.println("waiting for sync");
  217.   setSyncProvider(getNtpTime);
  218.  
  219.   if(timeStatus() != timeNotSet){
  220.     digitalClockDisplay();
  221.     Serial.println("here is another way to set rtc");
  222.       time_t t = now();
  223.       char date_0[12];
  224.       snprintf_P(date_0, countof(date_0), PSTR("%s %02u %04u"), monthShortStr(month(t)), day(t), year(t));
  225.       Serial.println(date_0);
  226.       char time_0[9];
  227.       snprintf_P(time_0, countof(time_0), PSTR("%02u:%02u:%02u"), hour(t), minute(t), second(t));
  228.       Serial.println(time_0);
  229.       Serial.println("Now its time to set up rtc");
  230.       RtcDateTime compiled = RtcDateTime(date_0, time_0);
  231.  //      printDateTime(compiled);
  232.        Serial.println("");
  233.  
  234.         if (!Rtc.IsDateTimeValid()) 
  235.     {
  236.         // Common Cuases:
  237.         //    1) first time you ran and the device wasn't running yet
  238.         //    2) the battery on the device is low or even missing
  239.  
  240.         Serial.println("RTC lost confidence in the DateTime!");
  241.  
  242.         // following line sets the RTC to the date & time this sketch was compiled
  243.         // it will also reset the valid flag internally unless the Rtc device is
  244.         // having an issue
  245.       }
  246.      Rtc.SetDateTime(compiled);
  247.      RtcDateTime now = Rtc.GetDateTime();
  248.     if (now < compiled) 
  249.     {
  250.         Serial.println("RTC is older than compile time!  (Updating DateTime)");
  251.         Rtc.SetDateTime(compiled);
  252.     }
  253.     else if (now > compiled) 
  254.     {
  255.         Serial.println("RTC is newer than compile time. (this is expected)");
  256.     }
  257.     else if (now == compiled) 
  258.     {
  259.         Serial.println("RTC is the same as compile time! (not expected but all is fine)");
  260.     }
  261.  
  262.     // never assume the Rtc was last configured by you, so
  263.     // just clear them to your needed state
  264.     Rtc.Enable32kHzPin(false);
  265.     Rtc.SetSquareWavePin(DS3231SquareWavePin_ModeNone); 
  266.  }
  267. }
  268.  
  269. void synchronClock() {
  270.       Rtc.Begin();
  271.       wifiManager.autoConnect(ssid.c_str(), pass.c_str());
  272.       while (WiFi.status() != WL_CONNECTED) {
  273.         delay(500);
  274.         Serial.print(".");
  275.       }
  276.       Serial.println(" ");
  277.       Serial.print("IP number assigned by DHCP is ");
  278.       Serial.println(WiFi.localIP());
  279.       Serial.println("Starting UDP");
  280.       Udp.begin(localPort);
  281.       Serial.print("Local port: ");
  282.       Serial.println(Udp.localPort());
  283.       Serial.println("waiting for sync");
  284.       setSyncProvider(getNtpTime);
  285.  
  286.       if(timeStatus() != timeNotSet){
  287.         digitalClockDisplay();
  288.         Serial.println("here is another way to set rtc");
  289.           time_t t = now();
  290.           char date_0[12];
  291.           snprintf_P(date_0, countof(date_0), PSTR("%s %02u %04u"), monthShortStr(month(t)), day(t), year(t));
  292.           Serial.println(date_0);
  293.           char time_0[9];
  294.           snprintf_P(time_0, countof(time_0), PSTR("%02u:%02u:%02u"), hour(t), minute(t), second(t));
  295.           Serial.println(time_0);
  296.           Serial.println("Now its time to set up rtc");
  297.           RtcDateTime compiled = RtcDateTime(date_0, time_0);
  298.           Serial.println("");
  299.  
  300.             if (!Rtc.IsDateTimeValid()) 
  301.         {
  302.             // Common Cuases:
  303.             //    1) first time you ran and the device wasn't running yet
  304.             //    2) the battery on the device is low or even missing
  305.  
  306.             Serial.println("RTC lost confidence in the DateTime!");
  307.  
  308.             // following line sets the RTC to the date & time this sketch was compiled
  309.             // it will also reset the valid flag internally unless the Rtc device is
  310.             // having an issue
  311.          }
  312.          Rtc.SetDateTime(compiled);
  313.          RtcDateTime now = Rtc.GetDateTime();
  314.         if (now < compiled) 
  315.         {
  316.             Serial.println("RTC is older than compile time!  (Updating DateTime)");
  317.             Rtc.SetDateTime(compiled);
  318.         }
  319.         else if (now > compiled) 
  320.         {
  321.             Serial.println("RTC is newer than compile time. (this is expected)");
  322.         }
  323.         else if (now == compiled) 
  324.         {
  325.             Serial.println("RTC is the same as compile time! (not expected but all is fine)");
  326.         }
  327.  
  328.         // never assume the Rtc was last configured by you, so
  329.         // just clear them to your needed state
  330.         Rtc.Enable32kHzPin(false);
  331.         Rtc.SetSquareWavePin(DS3231SquareWavePin_ModeNone); 
  332.     }
  333. }
  334.  
  335. void Clock(){
  336.     RtcDateTime now = Rtc.GetDateTime();
  337.     //Print RTC time to Serial Monitor
  338.    hh = now.Hour();
  339.    mm = now.Minute(); 
  340.    ss = now.Second();
  341.  
  342.   sprintf(date_r, "%d.%d.%d", now.Day(), now.Month(), now.Year());
  343.   if  (mm < 10) sprintf(time_r, "%d:0%d", hh, mm);
  344.   else sprintf(time_r, "%d:%d", hh, mm);
  345.  
  346.    Serial.println(date_r);
  347.    Serial.println(time_r);
  348.    }
  349.  
  350. //callback notifying the need to save config
  351. void saveConfigCallback() {
  352.         Serial.println("Should save config");
  353.         shouldSaveConfig = true;
  354. }
  355.  
  356. void factoryReset() {
  357.         Serial.println("Resetting to factory settings");
  358.         wifiManager.resetSettings();
  359.        SPIFFS.format();
  360.        ESP.reset();
  361. }
  362.  
  363. void printString(String str) {
  364.         Serial.println(str);
  365. }
  366.  
  367. void readCO2() {
  368.       static byte cmd[9] = {0xFF,0x01,0x86,0x00,0x00,0x00,0x00,0x00,0x79}; //команда чтения
  369.       byte response[9];
  370.       byte crc = 0;
  371.         while (mySerial.available())mySerial.read();   //очистка буфера UART перед запросом 
  372.         memset(response, 09);// очистка ответа
  373.         mySerial.write(cmd,9);// запрос на содержание CO2
  374.         mySerial.readBytes(response, 9);//читаем 9 байт ответа сенсора
  375.         //расчет контрольной суммы
  376.         crc = 0;
  377.         for (int i = 1; i <= 7; i++)
  378.         {
  379.           crc += response[i];
  380.         }
  381.         crc = ((~crc)+1);
  382.         {
  383.         //проверка CRC
  384.         if ( !(response[0] == 0xFF && response[1] == 0x86 && response[8] == crc) ) 
  385.         {
  386.           Serial.println("CRC error");
  387.         } else 
  388.             {
  389.              //расчет значения CO2
  390.            co2 = (((unsigned int) response[2])<<8) + response[3]; 
  391.             Serial.println("CO2: " + String(co2) + "ppm");
  392.                  }
  393.         }
  394. }
  395.  
  396. void sendMeasurements() {
  397.       float  t1 {-100};
  398.       int h1 {-1}, i;
  399.         // Temperature
  400.         t1 = dht.readTemperature();
  401.               if ((t1 > -1) and (t1 < 100)) t = t1;
  402.         Serial.println("T: " + String(t) + "C");
  403.         // Humidity
  404.         h1 = dht.readHumidity();
  405.         if ((h1 > -1) and (h1 < 100))  h = h1;
  406.        Serial.println("H: " + String(h) + "%");
  407.          // CO2
  408.           readCO2(); 
  409. }
  410.  
  411. void sendToBlynk(){
  412.         Blynk.virtualWrite(V1, t);
  413.         Blynk.virtualWrite(V2, h);
  414.         Blynk.virtualWrite(V3, co2);
  415.         Blynk.virtualWrite(V4, TemperaturePoint0);
  416.   }
  417.  
  418. void noData() {
  419.         u8g2.setFont(u8g2_font_9x18_mf);
  420.         x = 48;
  421.         y = 40;
  422.         u8g2.drawStr(x, y, "***");
  423.         }
  424.  
  425. void drawOn() {
  426.         float TemperatureP0;
  427.         char Online_ch[]{"    Online"};       
  428.  
  429.         TemperatureP0 = TemperaturePoint0 - Chs;
  430.         dtostrf(TemperatureP0, 41, Temperature0); //преобразование float в char
  431.  
  432.         String Temperature0_i;
  433.         Temperature0_i = String(Temperature0); 
  434.         char Temperature0_i_m [16];
  435.         Temperature0_i.toCharArray(Temperature0_i_m, 16);
  436.         u8g2.clearBuffer();
  437.         String Temperature0_p;
  438.         String onl1 = "OnLine T<";
  439.         Temperature0_p = onl1 + Temperature0_i_m;
  440.         char Temperature0_p_m [16];
  441.         Temperature0_p.toCharArray(Temperature0_p_m, 16); 
  442.  
  443.         String Tmx_i;
  444.         Tmx_i = String(Tmx); 
  445.         char Tmx_i_m [16];
  446.         Tmx_i.toCharArray(Tmx_i_m, 16);
  447.         u8g2.clearBuffer();
  448.         String Tmx_p;
  449.         String onl2 = "OnLine T>";
  450.         Tmx_p = onl2 + Tmx_i_m;
  451.         char Tmx_p_m [16];
  452.         Tmx_p.toCharArray(Tmx_p_m, 16); 
  453.  
  454.         String Cmx_i;
  455.         Cmx_i = String(Cmx); 
  456.         char Cmx_i_m [16];
  457.         Cmx_i.toCharArray(Cmx_i_m, 16);
  458.         u8g2.clearBuffer();
  459.         String Cmx_p;
  460.         String onl3 = "OnL CO2>";
  461.         Cmx_p = onl3 + Cmx_i_m;
  462.         char Cmx_p_m [16];
  463.         Cmx_p.toCharArray(Cmx_p_m, 16);
  464.  
  465.         String Hmn_i;
  466.         Hmn_i = String(Hmn); 
  467.         char Hmn_i_m [16];
  468.         Hmn_i.toCharArray(Hmn_i_m, 16);
  469.         u8g2.clearBuffer();
  470.         String Hmn_p;
  471.         String onl4 = "OnLine H<";
  472.         Hmn_p = onl4 + Hmn_i_m;
  473.         char Hmn_p_m [16];
  474.         Hmn_p.toCharArray(Hmn_p_m, 16);
  475.  
  476. //string 3
  477.         u8g2.setFont(u8g2_font_9x18_mf);
  478.         x = 0;
  479.         y = 64;
  480.         u8g2.drawStr(x, y, Online_ch);
  481.         if ((hh>=HourPoint1) and (hh<=HourPoint2) and (t<TemperatureP0)) u8g2.drawStr(x, y, Temperature0_p_m); 
  482.         else
  483.         if (> Tmax) u8g2.drawStr(x, y, Tmx_p_m); 
  484.         else 
  485.         if (co2 > Cmax) u8g2.drawStr(x, y, Cmx_p_m);  
  486.         else  
  487.         if (< Hmin) u8g2.drawStr(x, y, Hmn_p_m);        
  488.  
  489.          switch((millis() / 100) % 4) {
  490. // Temperature 
  491.    case 0:          
  492.   { 
  493.     String info_t;
  494.     String paramT;
  495.     String tmpr = "T(";
  496.     String grad = "C):";
  497.     const char degree {176};
  498.      paramT = tmpr + degree + grad;
  499.      char paramT_m [12];
  500.      paramT.toCharArray(paramT_m, 12);
  501.  
  502.            info_t = String(t); 
  503.          char info_t_m [12];
  504.         info_t.toCharArray(info_t_m, 5);
  505. //string 1
  506.         u8g2.setFont(u8g2_font_9x18_mf);
  507.         x = 16;
  508.         y = u8g2.getAscent() - u8g2.getDescent();
  509.          u8g2.drawStr(x, y, paramT_m);
  510. //string 2 
  511.        if ((> -100) and (< 100)) {      
  512.         u8g2.setFont(u8g2_font_inb24_mf);
  513.         x = (128 - u8g2.getStrWidth(info_t_m))/2;
  514.         y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
  515.         u8g2.drawStr(x, y, info_t_m);
  516.        }
  517.       else noData();
  518.         }         
  519.                 break;
  520.  //Humidity             
  521.         case 1:
  522.       { 
  523.         String info_h;
  524.          info_h = String(h);  
  525.         char info_h_m [12];
  526.         info_h.toCharArray(info_h_m, 12);
  527. //string 1
  528.         u8g2.setFont(u8g2_font_9x18_mf);
  529.  
  530.         x = 16;
  531.         y = u8g2.getAscent() - u8g2.getDescent();
  532.         u8g2.drawStr(x, y, "H(%):");
  533. //string 2        
  534.     if ((> -1) and (< 100)){
  535.         u8g2.setFont(u8g2_font_inb24_mf);
  536.         x = (128 - u8g2.getStrWidth(info_h_m))/2;
  537.         y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
  538.         u8g2.drawStr(x, y, info_h_m);
  539.         }
  540.       else noData();        
  541.         }                   
  542.                 break;
  543. //CO2 
  544.    case 2:
  545.       { 
  546.         String info_co2;
  547.         info_co2 = String(co2);
  548.         char info_co2_m [12];
  549.         info_co2.toCharArray(info_co2_m, 12);
  550. //string 1
  551.         u8g2.setFont(u8g2_font_9x18_mf);
  552.         x = 8;
  553.         y = u8g2.getAscent() - u8g2.getDescent();
  554.         u8g2.drawStr(x, y, "CO2(ppm):");
  555. //string 2        
  556.       if  ((co2 > -1) and (co2 <= 2000)) {
  557.         u8g2.setFont(u8g2_font_inb24_mf);
  558.         x = (128 - u8g2.getStrWidth(info_co2_m))/2;
  559.         y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
  560.         u8g2.drawStr(x, y, info_co2_m);
  561.       }
  562.       else noData();
  563.         }                   
  564.                 break;                
  565. //time, date 
  566.    case 3:
  567.       { 
  568. //string 1
  569.         u8g2.setFont(u8g2_font_9x18_mf);
  570.         x = (128 - u8g2.getStrWidth(date_r))/2;
  571.         y = u8g2.getAscent() - u8g2.getDescent();
  572.         u8g2.drawStr(x, y, date_r);
  573. //string 2        
  574.         u8g2.setFont(u8g2_font_inb24_mf);
  575.         x = (128 - u8g2.getStrWidth(time_r))/2;
  576.         y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
  577.         u8g2.drawStr(x, y, time_r);
  578.         }                   
  579.                 break;
  580.      }     
  581.         u8g2.sendBuffer();
  582. }
  583.  
  584. void drawOff() {
  585.         float TemperatureP0A;
  586.         char OffLine_ch[]{"Offline Tst=21"};       
  587.  
  588.       TemperatureP0A = TemperaturePointA0 - Chs;
  589.     //  dtostrf(TemperatureP0A, 4, 1, TemperaturePointA0); //преобразование float в char
  590.  
  591.         String TemperaturePointA0_i;
  592.         TemperaturePointA0_i = String(TemperaturePointA0); 
  593.         char TemperaturePointA0_i_m [16];
  594.         TemperaturePointA0_i.toCharArray(TemperaturePointA0_i_m, 16);
  595.         u8g2.clearBuffer();
  596.         String TemperaturePointA0_p;
  597.         String onl1 = "Offline T<";
  598.         TemperaturePointA0_p = onl1 + TemperaturePointA0_i_m;
  599.         char TemperaturePointA0_p_m [16];
  600.         TemperaturePointA0_p.toCharArray(TemperaturePointA0_p_m, 16); 
  601. //string 3
  602.         u8g2.setFont(u8g2_font_9x18_mf);
  603.         x = 0;
  604.         y = 64;
  605.         u8g2.drawStr(x, y, OffLine_ch);
  606.         if (t<TemperatureP0A) u8g2.drawStr(x, y, TemperaturePointA0_p_m); 
  607.  
  608.          switch((millis() / 100) % 4) {
  609. // Temperature 
  610.    case 0:          
  611.   { 
  612.     String info_t;
  613.     String paramT;
  614.     String tmpr = "T(";
  615.     String grad = "C):";
  616.     const char degree {176};
  617.      paramT = tmpr + degree + grad;
  618.      char paramT_m [12];
  619.      paramT.toCharArray(paramT_m, 12);
  620.  
  621.            info_t = String(t); 
  622.          char info_t_m [12];
  623.         info_t.toCharArray(info_t_m, 5);
  624. //string 1
  625.         u8g2.setFont(u8g2_font_9x18_mf);
  626.         x = 16;
  627.         y = u8g2.getAscent() - u8g2.getDescent();
  628.          u8g2.drawStr(x, y, paramT_m);
  629. //string 2 
  630.        if ((> -100) and (< 100)) {      
  631.         u8g2.setFont(u8g2_font_inb24_mf);
  632.         x = (128 - u8g2.getStrWidth(info_t_m))/2;
  633.         y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
  634.         u8g2.drawStr(x, y, info_t_m);
  635.        }
  636.       else noData();
  637.    }         
  638.                 break;
  639.  //Humidity             
  640.         case 1:
  641.       { 
  642.         String info_h;
  643.          info_h = String(h);  
  644.         char info_h_m [12];
  645.         info_h.toCharArray(info_h_m, 12);
  646. //string 1
  647.         u8g2.setFont(u8g2_font_9x18_mf);
  648.  
  649.         x = 16;
  650.         y = u8g2.getAscent() - u8g2.getDescent();
  651.         u8g2.drawStr(x, y, "H(%):");
  652. //string 2        
  653.     if ((> -1) and (< 100)){
  654.         u8g2.setFont(u8g2_font_inb24_mf);
  655.         x = (128 - u8g2.getStrWidth(info_h_m))/2;
  656.         y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
  657.         u8g2.drawStr(x, y, info_h_m);
  658.         }
  659.       else noData();        
  660.         }                   
  661.                 break;
  662. //CO2 
  663.    case 2:
  664.       { 
  665.         String info_co2;
  666.         info_co2 = String(co2);
  667.         char info_co2_m [12];
  668.         info_co2.toCharArray(info_co2_m, 12);
  669. //string 1
  670.         u8g2.setFont(u8g2_font_9x18_mf);
  671.         x = 8;
  672.         y = u8g2.getAscent() - u8g2.getDescent();
  673.         u8g2.drawStr(x, y, "CO2(ppm):");
  674. //string 2        
  675.       if  ((co2 > -1) and (co2 <= 2000)) {
  676.         u8g2.setFont(u8g2_font_inb24_mf);
  677.         x = (128 - u8g2.getStrWidth(info_co2_m))/2;
  678.         y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
  679.         u8g2.drawStr(x, y, info_co2_m);
  680.       }
  681.       else noData();
  682.    }                   
  683.                 break;                
  684. //time, date 
  685.    case 3:
  686.       { 
  687. //string 1
  688.         u8g2.setFont(u8g2_font_9x18_mf);
  689.         x = (128 - u8g2.getStrWidth(date_r))/2;
  690.         y = u8g2.getAscent() - u8g2.getDescent();
  691.         u8g2.drawStr(x, y, date_r);
  692. //string 2        
  693.         u8g2.setFont(u8g2_font_inb24_mf);
  694.         x = (128 - u8g2.getStrWidth(time_r))/2;
  695.         y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
  696.         u8g2.drawStr(x, y, time_r);
  697.         }                   
  698.                 break;
  699.      }     
  700.         u8g2.sendBuffer();
  701. }
  702.  
  703. void drawOffBlynk() {
  704.       float TemperatureP0;
  705.       char OffBlynk_ch[]{"   OffBlynk"};       
  706.  
  707.       TemperatureP0 = TemperaturePoint0 - Chs;
  708.       dtostrf(TemperatureP0, 41, Temperature0); //преобразование float в char
  709.  
  710.         String Temperature0_i;
  711.         Temperature0_i = String(Temperature0); 
  712.         char Temperature0_i_m [16];
  713.         Temperature0_i.toCharArray(Temperature0_i_m, 16);
  714.         u8g2.clearBuffer();
  715.         String Temperature0_p;
  716.         String onl1 = "OffBL T<";
  717.         Temperature0_p = onl1 + Temperature0_i_m;
  718.         char Temperature0_p_m [16];
  719.         Temperature0_p.toCharArray(Temperature0_p_m, 16); 
  720.  
  721.         String Tmx_i;
  722.         Tmx_i = String(Tmx); 
  723.         char Tmx_i_m [16];
  724.         Tmx_i.toCharArray(Tmx_i_m, 16);
  725.         u8g2.clearBuffer();
  726.         String Tmx_p;
  727.         String onl2 = "OffBL T>";
  728.         Tmx_p = onl2 + Tmx_i_m;
  729.         char Tmx_p_m [16];
  730.         Tmx_p.toCharArray(Tmx_p_m, 16); 
  731.  
  732.         String Cmx_i;
  733.         Cmx_i = String(Cmx); 
  734.         char Cmx_i_m [16];
  735.         Cmx_i.toCharArray(Cmx_i_m, 16);
  736.         u8g2.clearBuffer();
  737.         String Cmx_p;
  738.         String onl3 = "OnL CO2>";
  739.         Cmx_p = onl3 + Cmx_i_m;
  740.         char Cmx_p_m [16];
  741.         Cmx_p.toCharArray(Cmx_p_m, 16);
  742.  
  743.         String Hmn_i;
  744.         Hmn_i = String(Hmn); 
  745.         char Hmn_i_m [16];
  746.         Hmn_i.toCharArray(Hmn_i_m, 16);
  747.         u8g2.clearBuffer();
  748.         String Hmn_p;
  749.         String onl4 = "OffBL H<";
  750.         Hmn_p = onl4 + Hmn_i_m;
  751.         char Hmn_p_m [16];
  752.         Hmn_p.toCharArray(Hmn_p_m, 16);
  753.  
  754. //string 3
  755.         u8g2.setFont(u8g2_font_9x18_mf);
  756.         x = 0;
  757.         y = 64;
  758.         u8g2.drawStr(x, y, OffBlynk_ch);
  759.         if ((hh>=HourPoint1) and (hh<=HourPoint2) and (t<TemperatureP0)) u8g2.drawStr(x, y, Temperature0_p_m); 
  760.         else
  761.         if (> Tmax) u8g2.drawStr(x, y, Tmx_p_m); 
  762.         else 
  763.         if (co2 > Cmax) u8g2.drawStr(x, y, Cmx_p_m);  
  764.         else  
  765.         if (< Hmin) u8g2.drawStr(x, y, Hmn_p_m);        
  766.  
  767.          switch((millis() / 100) % 4) {
  768. // Temperature 
  769.    case 0:          
  770.       { 
  771.         String info_t;
  772.         String paramT;
  773.         String tmpr = "T(";
  774.         String grad = "C):";
  775.         const char degree {176};
  776.          paramT = tmpr + degree + grad;
  777.          char paramT_m [12];
  778.          paramT.toCharArray(paramT_m, 12);
  779.  
  780.            info_t = String(t); 
  781.          char info_t_m [12];
  782.         info_t.toCharArray(info_t_m, 5);
  783. //string 1
  784.         u8g2.setFont(u8g2_font_9x18_mf);
  785.         x = 16;
  786.         y = u8g2.getAscent() - u8g2.getDescent();
  787.          u8g2.drawStr(x, y, paramT_m);
  788. //string 2 
  789.        if ((> -100) and (< 100)) {      
  790.         u8g2.setFont(u8g2_font_inb24_mf);
  791.         x = (128 - u8g2.getStrWidth(info_t_m))/2;
  792.         y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
  793.         u8g2.drawStr(x, y, info_t_m);
  794.        }
  795.       else noData();
  796.         }         
  797.                 break;
  798.  //Humidity             
  799.         case 1:
  800.       { 
  801.         String info_h;
  802.          info_h = String(h);  
  803.         char info_h_m [12];
  804.         info_h.toCharArray(info_h_m, 12);
  805. //string 1
  806.         u8g2.setFont(u8g2_font_9x18_mf);
  807.  
  808.         x = 16;
  809.         y = u8g2.getAscent() - u8g2.getDescent();
  810.         u8g2.drawStr(x, y, "H(%):");
  811. //string 2        
  812.     if ((> -1) and (< 100)){
  813.         u8g2.setFont(u8g2_font_inb24_mf);
  814.         x = (128 - u8g2.getStrWidth(info_h_m))/2;
  815.         y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
  816.         u8g2.drawStr(x, y, info_h_m);
  817.         }
  818.       else noData();        
  819.         }                   
  820.                 break;
  821. //CO2 
  822.    case 2:
  823.       { 
  824.         String info_co2;
  825.         info_co2 = String(co2);
  826.         char info_co2_m [12];
  827.         info_co2.toCharArray(info_co2_m, 12);
  828. //string 1
  829.         u8g2.setFont(u8g2_font_9x18_mf);
  830.         x = 8;
  831.         y = u8g2.getAscent() - u8g2.getDescent();
  832.         u8g2.drawStr(x, y, "CO2(ppm):");
  833. //string 2        
  834.       if  ((co2 > -1) and (co2 <= 2000)) {
  835.         u8g2.setFont(u8g2_font_inb24_mf);
  836.         x = (128 - u8g2.getStrWidth(info_co2_m))/2;
  837.         y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
  838.         u8g2.drawStr(x, y, info_co2_m);
  839.       }
  840.       else noData();
  841.         }                   
  842.                 break;                
  843. //time, date 
  844.    case 3:
  845.       { 
  846. //string 1
  847.         u8g2.setFont(u8g2_font_9x18_mf);
  848.         x = (128 - u8g2.getStrWidth(date_r))/2;
  849.         y = u8g2.getAscent() - u8g2.getDescent();
  850.         u8g2.drawStr(x, y, date_r);
  851. //string 2        
  852.         u8g2.setFont(u8g2_font_inb24_mf);
  853.         x = (128 - u8g2.getStrWidth(time_r))/2;
  854.         y = y + 2 + u8g2.getAscent() - u8g2.getDescent();
  855.         u8g2.drawStr(x, y, time_r);
  856.         }                   
  857.                 break;
  858.    }     
  859.         u8g2.sendBuffer();
  860. }
  861.  
  862. void drawBoot(String msg = "Loading...") {
  863.         u8g2.clearBuffer();
  864.         u8g2.setFont(u8g2_font_9x18_mf);
  865.         x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
  866.         y = 32 + u8g2.getAscent() / 2;
  867.         u8g2.drawStr(x, y, msg.c_str());
  868.         u8g2.sendBuffer();
  869. }
  870.  
  871. void drawConnectionDetails(String ssid, String pass, String url) {
  872.         String msg {""};
  873.         u8g2.clearBuffer();
  874.  
  875.         msg = "Connect to WiFi:";
  876.         u8g2.setFont(u8g2_font_7x13_mf);
  877.         x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
  878.         y = u8g2.getAscent() - u8g2.getDescent();
  879.         u8g2.drawStr(x, y, msg.c_str());
  880.  
  881.         msg = "net: " + ssid;
  882.         x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
  883.         y = y + 1 + u8g2.getAscent() - u8g2.getDescent();
  884.         u8g2.drawStr(x, y, msg.c_str());
  885.  
  886.         msg = "pw: "+ pass;
  887.         x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
  888.         y = y + 1 + u8g2.getAscent() - u8g2.getDescent();
  889.         u8g2.drawStr(x, y, msg.c_str());
  890.  
  891.         msg = "Open browser:";
  892.         x = (128 - u8g2.getStrWidth(msg.c_str())) / 2;
  893.         y = y + 1 + u8g2.getAscent() - u8g2.getDescent();
  894.         u8g2.drawStr(x, y, msg.c_str());
  895.  
  896.         // URL
  897.         // u8g2.setFont(u8g2_font_6x12_mf);
  898.         x = (128 - u8g2.getStrWidth(url.c_str())) / 2;
  899.         y = y + 1 + u8g2.getAscent() - u8g2.getDescent();
  900.         u8g2.drawStr(x, y, url.c_str());
  901.  
  902.         u8g2.sendBuffer();
  903. }
  904.  
  905. bool loadConfigS(){
  906.         Blynk.config(address);  
  907.         Serial.print("e-mail: ");  
  908.         Serial.println(  address );   
  909.  
  910.         Blynk.config(Tmx);  
  911.         Serial.print("T max: ");  
  912.         Serial.println(  Tmx );   
  913.  
  914.         Blynk.config(Cmx);  
  915.         Serial.print("CO2 max: ");  
  916.         Serial.println(  Cmx );
  917.  
  918.         Blynk.config(Temperature0);  
  919.         Serial.print("Temperature 0: ");  
  920.         Serial.println(  Temperature0 );   
  921.  
  922.         Blynk.config(Temperature1);  
  923.         Serial.print("Temperature1: ");  
  924.         Serial.println(  Temperature1 );   
  925.  
  926.         Blynk.config(Temperature2);  
  927.         Serial.print("Temperature2: ");  
  928.         Serial.println(  Temperature2 ); 
  929.  
  930.         Blynk.config(Hmn);  
  931.         Serial.print("H min: ");  
  932.         Serial.println(  Hmn );    
  933.  
  934.         Blynk.config(Hour1);  
  935.         Serial.print("Hour 1: ");  
  936.         Serial.println(  Hour1 );      
  937.  
  938.         Blynk.config(Hour2);  
  939.         Serial.print("Hour 2: ");  
  940.         Serial.println(  Hour2 ); 
  941.  
  942.         Blynk.config(tZ);  
  943.         Serial.print("Time Zone: ");  
  944.         Serial.println(  tZ );
  945.  
  946.         Blynk.config(blynk_token, "blynk-cloud.com"8442);
  947.         Serial.print("token: " );
  948.         Serial.println(  blynk_token  );  
  949. }
  950.  
  951. bool loadConfig() {
  952.         Serial.println("Load config...");
  953.         File configFile = SPIFFS.open("/config.json""r");
  954.         if (!configFile) {
  955.                 Serial.println("Failed to open config file");
  956.                 return false;
  957.         }
  958.  
  959.         size_t size = configFile.size();
  960.         if (size > 1024) {
  961.                 Serial.println("Config file size is too large");
  962.                 return false;
  963.         }
  964.  
  965.         // Allocate a buffer to store contents of the file.
  966.         std::unique_ptr<char[]> buf(new char[size]);
  967.  
  968.         // We don't use String here because ArduinoJson library requires the input
  969.         // buffer to be mutable. If you don't use ArduinoJson, you may as well
  970.         // use configFile.readString instead.
  971.         configFile.readBytes(buf.get(), size);
  972.  
  973.         StaticJsonBuffer<200> jsonBuffer;
  974.         JsonObject &json = jsonBuffer.parseObject(buf.get());
  975.  
  976.         if (!json.success()) {
  977.                 Serial.println("Failed to parse config file");
  978.                 return false;
  979.         }
  980.  
  981.         // Save parameters
  982.         strcpy(blynk_token, json["blynk_token"]);
  983.         strcpy(address, json["address"]);
  984.         strcpy(Tmx, json["Tmx"]);        
  985.         strcpy(Cmx, json["Cmx"]);
  986.         strcpy(Temperature0, json["Temperature0"]); 
  987.         strcpy(Temperature1, json["Temperature1"]); 
  988.         strcpy(Temperature2, json["Temperature2"]);       
  989.         strcpy(Hmn, json["Hmn"]); 
  990.         strcpy(Hour1, json["Hour1"]);
  991.         strcpy(Hour2, json["Hour2"]);
  992.         strcpy(tZ, json["tZ"]);
  993. }
  994.  
  995. void configModeCallback (WiFiManager *wifiManager) {
  996.         String url {"http://192.168.4.1"};
  997.         printString("Connect to WiFi:");
  998.         printString("net: " + ssid);
  999.         printString("pw: "+ pass);
  1000.         printString("Open browser:");
  1001.         printString(url);
  1002.         printString("to setup device");
  1003.  
  1004.         drawConnectionDetails(ssid, pass, url);
  1005. }
  1006.  
  1007. void setupWiFi() {
  1008.         //set config save notify callback
  1009.         wifiManager.setSaveConfigCallback(saveConfigCallback);
  1010.         // Custom parameters
  1011.          WiFiManagerParameter custom_tZ("tZ""Time Zone", tZ, 5);
  1012.          wifiManager.addParameter(&custom_tZ);        
  1013.          WiFiManagerParameter custom_Temperature0("Temperature0""Temperature 0", Temperature0, 5);  
  1014.          wifiManager.addParameter(&custom_Temperature0);             
  1015.          WiFiManagerParameter custom_Hour1("Hour1""Hour 1", Hour1, 5);  
  1016.          wifiManager.addParameter(&custom_Hour1);          
  1017.          WiFiManagerParameter custom_Temperature1("Temperature1""Temperature 1", Temperature1, 5);  
  1018.          wifiManager.addParameter(&custom_Temperature1);
  1019.          WiFiManagerParameter custom_Hour2("Hour2""Hour 2", Hour2, 5);
  1020.          wifiManager.addParameter(&custom_Hour2);          
  1021.          WiFiManagerParameter custom_Temperature2("Temperature2""Temperature 2", Temperature2, 5);  
  1022.          wifiManager.addParameter(&custom_Temperature2);        
  1023.          WiFiManagerParameter custom_Cmx("Cmx""Cmax", Cmx, 7);
  1024.          wifiManager.addParameter(&custom_Cmx);       
  1025.          WiFiManagerParameter custom_Hmn("Hmn""Hmin", Hmn, 5);  
  1026.          wifiManager.addParameter(&custom_Hmn); 
  1027.          WiFiManagerParameter custom_Tmx("Tmx""Tmax", Tmx,5);
  1028.          wifiManager.addParameter(&custom_Tmx);                   
  1029.          WiFiManagerParameter custom_address("address""E-mail", address, 64);
  1030.          wifiManager.addParameter(&custom_address);
  1031.          WiFiManagerParameter custom_blynk_token("blynk_token""Blynk Token", blynk_token, 34);
  1032.          wifiManager.addParameter(&custom_blynk_token);         
  1033.  
  1034.          wifiManager.setAPCallback(configModeCallback);
  1035.  
  1036.         wifiManager.setTimeout(180);
  1037.  
  1038.         if (!wifiManager.autoConnect(ssid.c_str(), pass.c_str())) {
  1039.         a++;
  1040.         Serial.println("mode OffLINE :(");
  1041.         loadConfigS();
  1042.         synchronClockA();         
  1043.         }
  1044.  
  1045.         //save the custom parameters to FS
  1046.         if (shouldSaveConfig) {
  1047.                 Serial.println("saving config");
  1048.                 DynamicJsonBuffer jsonBuffer;
  1049.                 JsonObject &json = jsonBuffer.createObject();
  1050.  
  1051.                 json["blynk_token"] = custom_blynk_token.getValue();                  
  1052.                 json["address"] = custom_address.getValue(); 
  1053.                 json["Tmx"] = custom_Tmx.getValue(); 
  1054.                 json["Cmx"] = custom_Cmx.getValue();
  1055.                 json["Temperature0"] = custom_Temperature0.getValue(); 
  1056.                 json["Temperature1"] = custom_Temperature1.getValue();
  1057.                 json["Temperature2"] = custom_Temperature2.getValue(); 
  1058.                 json["Hmn"] = custom_Hmn.getValue(); 
  1059.                 json["Hour1"] = custom_Hour1.getValue();    
  1060.                 json["Hour2"] = custom_Hour2.getValue();
  1061.                 json["tZ"] = custom_tZ.getValue();                
  1062.  
  1063.                 File configFile = SPIFFS.open("/config.json""w");
  1064.                 if (!configFile) {
  1065.                         Serial.println("failed to open config file for writing");
  1066.                 }
  1067.  
  1068.                 json.printTo(Serial);
  1069.                 json.printTo(configFile);
  1070.                 configFile.close();
  1071.                 //end save
  1072.         }
  1073.  
  1074.         //if you get here you have connected to the WiFi
  1075.         Serial.println("WiFi connected");
  1076.         Serial.print("IP address: ");
  1077.         Serial.println(WiFi.localIP());
  1078. }
  1079.  
  1080. BLYNK_WRITE(V10) {
  1081.         if (param.asInt() == 1)
  1082.          {
  1083.           buttonBlynk = true; 
  1084.           Blynk.virtualWrite(V10, HIGH);
  1085.           drawBoot("Thermo ON"); 
  1086.           }
  1087.          else 
  1088.           {
  1089.           buttonBlynk = false; 
  1090.           Blynk.virtualWrite(V10, LOW);
  1091.           drawBoot("Thermo OFF");
  1092.           }
  1093. }
  1094.  
  1095. void mailer() {
  1096.        // wait for WiFi connection
  1097.        if((WiFiMulti.run() == WL_CONNECTED)) {
  1098.         HTTPClient http;
  1099.         Serial.print("[HTTP] begin...\n");
  1100.         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()));
  1101.         Serial.print("[HTTP] GET...\n");
  1102.         // start connection and send HTTP header
  1103.         int httpCode = http.GET();
  1104.  
  1105.         // httpCode will be negative on error
  1106.         if(httpCode > 0) {
  1107.             // HTTP header has been send and Server response header has been handled
  1108.             Serial.printf("[HTTP] GET... code: %d\n", httpCode);
  1109.  
  1110.             // file found at server
  1111.             if(httpCode == HTTP_CODE_OK) {
  1112.                 String payload = http.getString();
  1113.                 Serial.println(payload);
  1114.             }
  1115.         } else {
  1116.             Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
  1117.         }
  1118.         http.end();
  1119.     }
  1120. }
  1121.  
  1122. void HystTemperatureA() {
  1123.       float TemperaturePointA0Mn, TemperaturePointA0Pl;
  1124.  
  1125.        TemperaturePointA0Mn = TemperaturePointA0-Chs;
  1126.        TemperaturePointA0Pl = TemperaturePointA0+Chs;
  1127.  
  1128.         if (t<TemperaturePointA0Mn) {  
  1129.         if (millis() - TimeTransmitMax > 120000){ 
  1130.         TimeTransmitMax = millis(); 
  1131.         transmitter.send(B11111110, 8);
  1132.         Serial.println ("t<TemperaturePointA0Mn Thermostat ON");
  1133.         }
  1134.        }
  1135.        else if (millis() - TimeTransmitMax > 120000)
  1136.        { 
  1137.         TimeTransmitMax = millis(); 
  1138.         transmitter.send(B10000000, 8);
  1139.         Serial.println ("t>TemperaturePointA0Mn Thermostat OFF");
  1140.         }
  1141.        if (t<TemperaturePointA0Pl) {  
  1142.         if (millis() - TimeTransmitMax > 120000){ 
  1143.         TimeTransmitMax = millis(); 
  1144.         transmitter.send(B11111110, 8);
  1145.         Serial.println ("t<TemperaturePointA0Pl Thermostat ON");
  1146.         }
  1147.        }
  1148.        else if (millis() - TimeTransmitMax > 120000)
  1149.        { 
  1150.         TimeTransmitMax = millis(); 
  1151.         transmitter.send(B10000000, 8);
  1152.         Serial.println ("t>TemperaturePointA0Pl Thermostat OFF");
  1153.         }
  1154. }
  1155.  
  1156. void HystTemperature() {
  1157. float TemperaturePoint0Mn, TemperaturePoint0Pl;
  1158.  
  1159.          TemperaturePoint0Mn = TemperaturePoint0-Chs;
  1160.          TemperaturePoint0Pl = TemperaturePoint0+Chs;
  1161.  
  1162.           if (t<TemperaturePoint0Mn) {  
  1163.           if (millis() - TimeTransmitMax > 120000){ 
  1164.           TimeTransmitMax = millis(); 
  1165.           transmitter.send(B11111110, 8);
  1166.           Serial.println ("t<TemperaturePoint0Mn Thermostat ON");
  1167.           }
  1168.          }
  1169.          else if (millis() - TimeTransmitMax > 120000) { 
  1170.           TimeTransmitMax = millis(); 
  1171.           transmitter.send(B10000000, 8);
  1172.           Serial.println ("t>TemperaturePoint0Mn Thermostat OFF");
  1173.           }
  1174.           if (t<TemperaturePoint0Pl) {  
  1175.           if (millis() - TimeTransmitMax > 120000){ 
  1176.           TimeTransmitMax = millis(); 
  1177.           transmitter.send(B11111110, 8);
  1178.           Serial.println ("t<TemperaturePoint0Pl Thermostat ON");
  1179.           }
  1180.          }
  1181.          else if (millis() - TimeTransmitMax > 120000)
  1182.          { 
  1183.           TimeTransmitMax = millis(); 
  1184.           transmitter.send(B10000000, 8);
  1185.           Serial.println ("t>TemperaturePoint0Pl Thermostat OFF");
  1186.           }
  1187. }
  1188.  
  1189. void TransmitterA(){
  1190.         transmitter.send(B10101010, 8);  //B10101010 - признак работающего передатчика  
  1191.         HystTemperatureA();
  1192. } 
  1193.  
  1194. void Transmitter(){
  1195.           transmitter.send(B10101010, 8);  //B10101010 - признак работающего передатчика  
  1196.  
  1197.           if (n>=24) n = 0; 
  1198.           if (m>=60) m = 0;
  1199.  
  1200.           progr = 0;
  1201.  
  1202.           if ((hh >= HourPoint1) and (hh < HourPoint2)){
  1203.             progr = 1;
  1204.              if (mm >= MinPoint1) progr = 1;
  1205.              if (mm < MinPoint2) progr = 1;
  1206.           }
  1207.           else if (hh >= HourPoint2) {
  1208.              progr = 2;
  1209.              if (mm >= MinPoint2) progr = 2;
  1210.           }
  1211.  
  1212.           if (buttonBlynk==true)           {
  1213.           Serial.println ("BLynk: Термостат ВКЛ"); 
  1214.  
  1215.           if (progr == 0) {
  1216.           TemperaturePoint0 = TemperaturePoint0;
  1217.           HystTemperature();
  1218.           Serial.println ("Термостатирование: t = " + String(TemperaturePoint0));
  1219.            }
  1220.           else if (progr == 1) {
  1221.           TemperaturePoint0 = TemperaturePoint1;
  1222.           HystTemperature();
  1223.           Serial.println ("Термостатирование: t = " + String(TemperaturePoint0));
  1224.            }
  1225.           else if (progr == 2){
  1226.           TemperaturePoint0 = TemperaturePoint2;
  1227.           HystTemperature();
  1228.           Serial.println ("Термостатирование: t = " + String(TemperaturePoint0));
  1229.             }
  1230.           }
  1231.           else { 
  1232.             transmitter.send(B10000000, 8);
  1233.             Serial.println ("BLynk: Термостат ВЫКЛ");
  1234.             }  
  1235.  
  1236.           if (co2 > Cmax)  { 
  1237.               transmitter.send(B11111101, 8);
  1238.               Serial.println("co2 > Cmax"); }
  1239.               else transmitter.send(B00000010, 8); 
  1240.  
  1241.           if (< Hmin) { 
  1242.                  transmitter.send(B11111011, 8);
  1243.                  Serial.println("h < Hmin"); }
  1244.                  else transmitter.send(B00000100, 8);
  1245.  
  1246.            if (> Tmax)  {
  1247.               transmitter.send(B11110111, 8);
  1248.               Serial.println("t > Tmax"); }
  1249.               else  transmitter.send(B00001000, 8);
  1250.           }           
  1251.  
  1252. void connectBlynk(){     
  1253.        if(String(blynk_token)== "Blynk token"){
  1254.        drawBoot("OFFBLYNK!");
  1255.        delay (3000);
  1256.         } else {
  1257.         drawBoot("Connect. Blynk");
  1258.         Serial.println("Connecting to blynk...");
  1259.         while (Blynk.connect() == false) {
  1260.         delay(500);
  1261.         Serial.println("Connecting to blynk...");
  1262.         }    
  1263.     } 
  1264. }
  1265.  
  1266. void setup() {
  1267.       // factoryReset();   //форматирование RAM
  1268.  
  1269.         mySerial.begin(9600);
  1270.         Serial.begin(115200);
  1271.  
  1272.         transmitter.enableTransmit(2);  
  1273.  
  1274.         u8g2.begin();   // инициализация экрана
  1275.  
  1276.         drawBoot("Loading...");
  1277.  
  1278.         // инициализация файловой системы
  1279.         if (!SPIFFS.begin()) {
  1280.         Serial.println("Failed to mount file system");
  1281.         ESP.reset(); }
  1282.  
  1283.         // загрузка параметров
  1284.         drawBoot("Connect. WiFi");
  1285.         setupWiFi();
  1286.  
  1287.         timerCO2 = timer.setInterval(15000, readCO2); 
  1288.         buttonBlynk = true;    
  1289.  
  1290.        if(== 1){  
  1291.         // Load config
  1292.         drawBoot("Load Config");
  1293.         if (!loadConfig()) {
  1294.                 Serial.println("Failed to load config");
  1295.                 factoryReset();
  1296.         } else {
  1297.                 Serial.println("Config loaded");
  1298.         }
  1299.         Blynk.config(address);  
  1300.         Serial.print("e-mail: ");  
  1301.         Serial.println(address);   
  1302.  
  1303.         Blynk.config(Tmx);  
  1304.         Serial.print("T max: ");  
  1305.         Serial.println(Tmx);   
  1306.  
  1307.         Blynk.config(Cmx);  
  1308.         Serial.print("CO2 max: ");  
  1309.         Serial.println(Cmx);
  1310.  
  1311.         Blynk.config(Temperature0);  
  1312.         Serial.print("Temperature 0: ");  
  1313.         Serial.println(Temperature0);   
  1314.  
  1315.         Blynk.config(Temperature1);  
  1316.         Serial.print("Temperature1: ");  
  1317.         Serial.println(Temperature1);   
  1318.  
  1319.         Blynk.config(Temperature2);  
  1320.         Serial.print("Temperature2: ");  
  1321.         Serial.println(Temperature2); 
  1322.  
  1323.         Blynk.config(Hmn);  
  1324.         Serial.print("H min: ");  
  1325.         Serial.println(Hmn);    
  1326.  
  1327.         Blynk.config(Hour1);  
  1328.         Serial.print("Hour 1: ");  
  1329.         Serial.println(Hour1);      
  1330.  
  1331.         Blynk.config(Hour2);  
  1332.         Serial.print("Hour 2: ");  
  1333.         Serial.println(Hour2); 
  1334.  
  1335.         Blynk.config(tZ);  
  1336.         Serial.print("Time Zone: ");  
  1337.         Serial.println(tZ);        
  1338.  
  1339.         Blynk.config(blynk_token, "blynk-cloud.com"8442);
  1340.         Serial.print("token: " );
  1341.         Serial.println(blynk_token);
  1342.  
  1343.         //преобразование char в float
  1344.         Tmax = atof (Tmx);   
  1345.         Cmax = atof (Cmx);
  1346.         TemperaturePoint0 = atof (Temperature0); 
  1347.         TemperaturePoint1 = atof (Temperature1);  
  1348.         TemperaturePoint2 = atof (Temperature2);
  1349.         Hmin = atof (Hmn);  
  1350.         HourPoint1 = atof (Hour1); 
  1351.         HourPoint2 = atof (Hour2); 
  1352.         tZone = atof (tZ);
  1353.  
  1354.         //синхронизация часов
  1355.         drawBoot("Clock synchr.");
  1356.         synchronClock(); 
  1357.  
  1358.        //периодичность вызова функций
  1359.        timerCO2 = timer.setInterval(15000, readCO2); 
  1360.        timerBl = timer.setInterval(5000, sendToBlynk);
  1361.  
  1362.         connectBlynk();   // подключение до Blynk     
  1363.         Blynk.virtualWrite(V10, HIGH); //установка кнопки V10 в состояние ВКЛ
  1364.         buttonBlynk = true;
  1365.         }        
  1366. }
  1367.  
  1368. void loop(){
  1369.        if (== 2) {  
  1370.        Serial.println(":( OffLINE");
  1371.        timer.run();
  1372.        Clock();
  1373.        sendMeasurements();
  1374.        TransmitterA(); 
  1375.        drawOff();
  1376.        delay(1000);  
  1377.        } 
  1378.        else if (== 1)  {  
  1379.        Serial.println(":) OnLINE"); 
  1380.        timer.run();
  1381.        Clock();
  1382.        Blynk.run();
  1383.        BLYNK_WRITE(V10);    
  1384.        Transmitter();
  1385.        sendMeasurements();
  1386.  
  1387.       if(String(blynk_token) == "Blynk token") drawOffBlynk(); else drawOn();   
  1388.  
  1389.        if (j>=24) j =0;    
  1390.        if (hh == j){
  1391.           if ((mm==30) and ((ss<30) )){
  1392.             if ((> Tmax) or (co2 > Cmax) or (< 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();                    
  1393.           }
  1394.         }  
  1395.        j++;                   
  1396.    }
  1397. }

Якщо хоча б один з параметрів повітря знаходиться за межами запрограмованих порогових значень, то пристрій в половині кожної години відправляє повідомлення на е-мейл:

Повідомлення на е-мейл відправляються php-скриптом. Скрипт завантажений на мій поштовий сервер. Він знадобиться, якщо планується відправка повідомлень з іншого ресурсу.

php-скрипт
  1. <?php
  2.  
  3. // тест -  http://skorovoda.in.ua/php/aqm42.php?mymail=my_login@my.site.net&t=22.2&h=55&co2=666
  4.  
  5. $EMAIL=0;
  6. $TEMPER=0;
  7. $vlaga=0;
  8. $carbon=0;
  9. $device=0;
  10. $EMAIL=$_GET["mymail"];
  11. $device=$_GET["ID"];
  12. echo $EMAIL;
  13. $TEMPER=$_GET["t"];
  14. $vlaga=$_GET["h"];
  15. $carbon=$_GET["co2"];
  16. $mdate = date("H:i d.m.y");
  17. echo <<<END
  18. <p>Температура:  $TEMPER °С<p>
  19. <p>Влажность: $vlaga %<p>
  20. <p>Содержание углекислого газа:  $carbon ppm<p>
  21. <p>--------------------<p>
  22. <p>Метеостанция №:  $device<p>
  23. END;
  24. echo <<<END
  25. <p>$mdate</p>
  26. END;
  27. mail($EMAIL, "Air Quality Monitor " .$device. " v.051018","   Данное сообщение сформировано монитором качества воздуха №" .$device. " автоматически.  Один или несколько параметров воздуха в помещении (температура, влажность или содержание углекислого газа) находятся за пределами заданных граничных значений. === Температура: ".$TEMPER."°C === "."Влажность: ".$vlaga."% === "."Содержание углекислого газа: ".$carbon." ppm === "."Проанализируйте информацию! === Время, дата: ".$mdate,"From: my_sensors@air-monitor.info \n")
  28.  
  29. ?>

Контактор

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

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

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

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

Скетч контактора для завантаження в Arduino Pro Mini - під спойлером. Щоб розгорнути спойлер - натисніть лівою кнопкою мишки на напис "скетч контактора" нижче.

скетч контактора
  1. /*
  2. Бездротовий програмований по Wi-Fi кімнатний термостат з монітором якості повітря та іншими корисними функціями  (контактор)
  3. http://skorovoda.in.ua/thermostat/
  4.  */
  5.  
  6. #include <RCSwitch.h>     //https://github.com/sui77/rc-switch
  7. RCSwitch mySwitch = RCSwitch();
  8.  
  9. void setup() {
  10.       pinMode(13, OUTPUT);
  11.       pinMode(3, OUTPUT);
  12.       pinMode(4, OUTPUT);
  13.       pinMode(5, OUTPUT);
  14.       pinMode(6, OUTPUT);
  15.  
  16.       digitalWrite(3, HIGH);
  17.       digitalWrite(4, HIGH);
  18.       digitalWrite(5, HIGH);
  19.       digitalWrite(6, HIGH);
  20.       digitalWrite(13, LOW);
  21.  
  22.       mySwitch.enableReceive(0);
  23.     }
  24.  
  25. void loop() {
  26.        if( mySwitch.available() ){
  27.         int value = mySwitch.getReceivedValue();
  28.  
  29.               //t < Tmin
  30.         if(value == B11111110) digitalWrite(4, LOW);
  31.         else if (value == B10000000) digitalWrite(4, HIGH);
  32.  
  33.                 //co2 > Cmax  
  34.         if(value == B11111101) digitalWrite(5, LOW);
  35.         else if (value == B00000010) digitalWrite(5, HIGH);
  36.  
  37.              //h < Hmin
  38.         if(value == B11111011) digitalWrite(6, LOW);
  39.         else if (value == B00000100) digitalWrite(6, HIGH);
  40.  
  41.             //t > Tmax
  42.         if(value == B11110111)digitalWrite(3, LOW);
  43.         else if (value == B00001000) digitalWrite(3, HIGH);
  44.  
  45.         //светодиод D13 Arduino - указывает на наличие связи передатчик-приемник (мигает - связь есть)
  46.         if(value == B10101010) digitalWrite(13, HIGH);  // B10101010 - код включенного передатчика, генерируется в анализаторе без условий  
  47.         else  digitalWrite(13, LOW); 
  48.  
  49.         mySwitch.resetAvailable();
  50.   }
  51. }

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

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

Крок 1:

Включимо аналізатор.

Спочатку треба набратися терпіння і почекати хвилини три. Термостат автоматично перейде в автономний режим роботи - без підключення по 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 залишити порожніми.

При виборі порогових значень можна орієнтуватися на показники, які я знайшов в Інтернеті:

Комфортна температура вночі під час сну: 19 ... 21 °С, вдень: 22 ... 23 °С.

Оптимальною відносною вологістю в холодну пору року вважається вологість 30 ... 45%, а в тепле - 30 ... 60%. Граничні максимальні показники вологості: взимку вона не повинна перевищувати 60%, а влітку - 65%.

Максимальний рівень вмісту вуглекислого газу в приміщеннях не повинен перевищувати 1000 ppm. Рекомендований рівень для спалень, дитячих кімнат - не більше 600 ppm. 1400 ppm - межа допустимого вмісту СО2 в приміщенні. Якщо його більше, то якість повітря вважається низькою.

За замовчуванням добова програма термостатування (вдень - висока температура, вночі - низька) задана з припущення, що днем ​​хтось із мешканців знаходиться в приміщенні, наприклад, працює вдома. Програму легко змінити під свої реалії.

Поле e-mail можна не заповнювати. Тоді надана можливість отримувати листи на електронну пошту про вихід параметрів повітря за порогові значення буде втрачена. Без введеного ключа Blynk'а - неможливо управляти термостатом і отримувати інформацію про параметри повітря дистанційно. Втім, термостат не "розгубиться", якщо залишаться незаповненими поля з граничними значеннями параметрів повітря, тоді за ним залишиться тільки одна функція: термостатування.

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

Якщо ви зберегли в пам'яті ESP8266 (кнопка Save), аналізатор підключиться до мережі і почне роботу.

Якщо помилилися (буває!), або вирішили змінити налаштування, знову доведеться двічі завантажити скетч в ESP8266. Перший раз - з розкоментуваним в Setup'і рядком factoryReset (); а другий - з закоментованим, далі - повторити крок 2.

Крок 3:

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

При стійкому радіозв'язку між аналізатором і контактором - світлодіод D13 на платі Arduino блимає з частотою близько 1 Гц.

Якщо контактор прийняв з аналізатора команду на включення обігрівального приладу або опалювальної системи - замкнуться нормально розімкнуті контакти реле і загориться відповідний світлодіод на модулі реле.

Якщо немає проблем з «холостим ходом» контактора, то підключаємо обігрівальний прилад або електроніку системи опалення. Обігрівальний прилад слід підключати проводом певного перерізу. Показник для розрахунку перетину мідного дроту - 5 А/мм2.

 

Крок 4:

Прийшов час запустити на смартфоні додаток Blynk. В Інтернеті багато інформації про програму Blynk, тому немає сенсу її повторювати.

Змінні для Blynk (щоб не шукати їх в скетчі аналізатора): температура - V1, вологість - V2, вміст СО2 - V3, температура термостатування - V4, віртуальна кнопка - V10.

На моєму смартфоні інтерфейс Blynk'a (його можна змінювати) має вигляд:

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

Сигнал з віртуальної кнопки Термостат формується тільки в момент натискання на кнопку. При натисканні на кнопку на екрані аналізатора миготить повідомлення Тhermo OFF! або Thermo ON! - в залежності від попереднього стану кнопки. Це повідомлення актуально при тестуванні термостата.

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

Синхронна з пилою температури зубчаста крива вологості на графіку - ще одне підтвердження відомого факту, що відкритий ТЕН сушить повітря, а піки на кривій вмісту СО2 - свідоцтво моїх короткочасних візитів в приміщення.

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

Наміри

Надалі планую працювати над удосконаленням термостата. Завдань - сила-силенна:

- Доповнити термостат датчиком температури з бездротовим зв'язком для вимірювання температури на вулиці.

- Замінити пару приймач-передавач RF іншою парою з більшою дальністю зв'язку при напрузі живлення 3...5В. В ідеалі - хотілося б зібрати аналізатор з живленням від двох батарей АА протягом опалювального сезону.

- Піти від ручного форматування пам'яті ESP8266 перед кожною зміною налаштувань термостата через повторне завантаження скетчу.

- Розширити програмований цикл роботи термостата з добового до тижневого.

- Замінити монохромний екран на кольоровий і з більшою роздільною здатністю. Це дозволить показувати всю інформацію про роботу термостата одним кадром, а вихід параметрів повітря за межі встановлених меж - зміною кольору.

- Потім зайнятися друкованими платами і презентабельним зовнішнім виглядом термостата.

Що ще можна поліпшити? Приймаються пропозиції, зауваження. Прислухаюсь до конструктивної критики.

Висновки

  • Завдяки підключенню до Інтернету, функціонал термостата значно розширився. Крім основної функції, в ньому реалізований ряд інших: від відправки повідомлень на е-мейл - до можливості автоматичної підтримки якості повітря в приміщенні.
  • В термостаті з'явилася нова якість: ним можна управляти дистанційно - через Інтернет.
  • Радує легкість, з якою програмується термостат: потрібно лише заповнити форму на сторінці браузера.
  • З'явилася можливість зберігати в пам'яті термостата персональні дані, як це робиться, наприклад, в роутерах.

Увага!

Автор не несе відповідальності за можливий негатив при повторенні проекту. Ви відповідаєте за все, що робите.

Тут - повна версія статті.