В системе автономного отопления моей квартиры работает выпускаемый серийно беспроводной комнатный термостат. Система, конечно, функционирует и без него: термостат был приобретен для экономии расхода газа и повышения комфорта.
Вещь очень полезная, но, на мой взгляд, несколько морально устаревшая. Было решено собрать нечто похожее на купленный термостат, добавив для начала в макет термостата более удобную настройку и подключение к Интернету.
Что в результате получилось – читайте дальше. Надеюсь, кроме меня проект будет интересен другим.
Знакомство
Возможности и характеристики:
- Связь между узлами термостата осуществляется по воздуху на радиочастоте.
- В течение суток термостат поддерживает постоянными три заданные значения температуры.
- Настройки термостата (программа работы, граничные параметры воздуха, другие) задаются дистанционно через 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
// тест - 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 оставить пустыми.
При выборе пороговых значений параметров воздуха ориентируйтесь на показатели, которые я нашел в Интернете:
- Комфортная температура ночью во время сна 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’e строкой 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В. В идеале – хотелось бы собрать анализатор с питанием от двух батареек АА на протяжении отопительного сезона.
- Уйти от ручного форматирования памяти ESP8266 перед каждым изменением настроек термостата через повторную загрузку скетча.
- Расширить программируемый цикл работы термостата с суточного до недельного.
- Заменить монохромный экран на цветной и с большим разрешением. Это позволит показывать всю информацию о работе термостата одним кадром, а выход параметров воздуха за пределы установленных границ – изменением цвета.
- Затем заняться печатными платами и презентабельным внешним видом термостата.
Что еще можно улучшить? Принимаются предложения, замечания. Прислушаюсь к конструктивной критике.
Выводы
- Благодаря подключению к Интернету, функционал термостата значительно расширился. Кроме основной функции, в нем реализован целый ряд других: от отправки оповещений на е-мейл — до возможности автоматического поддержания качества воздуха в помещении.
- В термостате появилось новое качество: им можно управлять через Интернет.
- Радует легкость, с которой программируется термостат: требуется лишь заполнить форму на странице браузера.
- Появилась возможность сохранять в памяти термостата персональные данные, как это делается, например, в роутерах.
Внимание!
Автор не несет ответственности за возможный негатив при повторении проекта. Вы отвечаете за все, что делаете.
P.S.
1. Макет из проекта достойно занял место старого термостата, поскольку тот в четвертом отопительном сезоне стал изредка «забывать» включать-выключать систему отопления.
2. О подходах в решении некоторых из перечисленных выше задач можно познакомиться в других моих статьях на Хабре:
- Два в одном: программируемый по Wi-Fi монитор качества воздуха и стрелочные часы
- Автономная метеостанция на контроллере ATMEGA328P и питанием от батареек с беспроводным выносным датчиком
- Снова о автономной Arduino-метеостанции на батарейках
- Победа над nRF24L01: на три шага ближе
Мои закладки по теме с Хабра
- Wi-Fi термометр на ESP8266 + DS18B20 всего за 4$
- Компактный монитор домашнего воздуха (CO2, температура, влажность, давление) с Wi-Fi и мобильным интерфейсом
- Использование RF-модулей
- Обзор инфракрасного датчика CO2 MH-Z19
- Измеряем концентрацию CO2 в квартире с помощью MH-Z19
- Практический опыт использования Blynk для датчика СО2. Часть 1
- Тёмная сторона MH-Z19