В системе автономного отопления моей квартиры работает выпускаемый серийно беспроводной комнатный термостат. Система, конечно, функционирует и без него: термостат был приобретен для экономии расхода газа и повышения комфорта.
Вещь очень полезная, но, на мой взгляд, несколько морально устаревшая. Было решено собрать нечто похожее на купленный термостат, добавив для начала в макет термостата более удобную настройку и подключение к Интернету.
Что в результате получилось – читайте дальше. Надеюсь, кроме меня проект будет интересен другим.
Знакомство
Возможности и характеристики:
- Связь между узлами термостата осуществляется по воздуху на радиочастоте.
- В течение суток термостат поддерживает постоянными три заданные значения температуры.
- Настройки термостата (программа работы, граничные параметры воздуха, другие) задаются дистанционно через 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 за объем корпуса – этот вариант проще и я выбрал его.
На анализатор подается постоянное напряжение 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")
?>
Контактор
Управление в контакторе осуществляет модуль 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…5В. В идеале – хотелось бы собрать контактор с питанием от двух батареек АА на протяжении отопительного сезона.
- Уйти от ручного форматирования памяти ESP8266 перед каждым изменением настроек термостата через повторную загрузку скетча.
- Расширить программируемый цикл работы термостата с суточного до недельного.
- Заменить монохромный экран на цветной и с большим разрешением. Это позволит показывать всю информацию о работе термостата одним кадром, а выход параметров воздуха за пределы установленных границ – изменением цвета.
- Затем заняться печатными платами и презентабельным внешним видом термостата.
Что еще можно улучшить? Принимаются предложения, замечания. Прислушаюсь к конструктивной критике.
Выводы
- Благодаря подключению к Интернету, функционал термостата значительно расширился. Кроме основной функции, в нем реализован целый ряд других: от отправки оповещений на е-мейл — до возможности автоматического поддержания качества воздуха в помещении.
- В термостате появилось новое качество: им можно управлять через Интернет.
- Радует легкость, с которой программируется термостат: требуется лишь заполнить форму на странице браузера.
- Появилась возможность сохранять в памяти термостата персональные данные, как это делается, например, в роутерах.
Мои закладки по теме с Хабра
1. Wi-Fi термометр на ESP8266 + DS18B20 всего за 4$
4. Обзор инфракрасного датчика CO2 MH-Z19
5. Измеряем концентрацию CO2 в квартире с помощью MH-Z19
6. Практический опыт использования Blynk для датчика СО2. Часть 1
Тут же хочу выразить свою благодарность Сергею Сильнову (@kumekay). Он поделился со мной идеей ввода переменных в ESP8266 через Wi-Fi. Идею Сергей реализовал в устройстве, которое подробно описано в публикации «Компактный монитор домашнего воздуха (CO2, температура, влажность, давление) с Wi-Fi и мобильным интерфейсом». Боюсь, что без подсказки Сергея этот проект не скоро имел бы хеппи-энд.
P.S. Макет из проекта достойно занял место старого термостата, поскольку тот на четвертый отопительный сезон стал изредка «забывать» включать-выключать систему отопления.
Внимание!
Автор не несет ответственности за возможный негатив при повторении проекта. Вы отвечаете за все, что делаете.
ximik666
Отлично сделано. Дома стоит что-то подобное: ESP8266, датчик СО2, датчик пыли, температура и влажности, корпус распечатан на 3д принтере. Данные отправляет на сервер в SQLite, там все собирается и формируются отчеты. Поначалу было интересно смотреть, сейчас уже забросил.
Cadil_TM Автор
Спасибо за высокую оценку моей работы. Поразил последний снимок. На мой взгляд, пульт управления атомным реактором — попроще. Очередных успехов!
ximik666
Изучал Javascript и HTML, поэтому так получилось).
GipsyIF
Полезная вещь.
Но две точки — это мало. Надо поднять температуру до 23 утром (допустим в 6 утра, чтоб к 6:30 было уже тепло), затем после 8 снизить, причём можно и ниже 18 (все ушли в школу/на работу). Потом так же вечером — перед приходом с работы поднять до 23, перед сном — снизить до 18.
И суточный — это тоже мало. Я бы переместил задачу «Расширить программируемый цикл работы термостата с суточного до недельного» на первое место. В воскресенье надо держать тепло целый день. В идеале — подтянуть с интернета каледарь с праздничными днями.
Ну или как вариант вместо календаря и недельного цикла поставить датчик движения. Если никто по квартире днём не двигаеся в течении 30 минут — значит можно снижать температуру.
Cadil_TM Автор
«Ну или как вариант вместо календаря и недельного цикла поставить датчик движения. Если никто по квартире днём не двигаеся в течении 30 минут — значит можно снижать температуру.»
Отличная идея! Я планировал через датчик движения выключать экран. Вы посмотрели шире.
Относительно двух точек — это единственная проблема из перечисленных, к которой не знаю как подступиться.
Все, на мой взгляд, упирается в размер SPIFFS ESP-12. Буду благодарен за помощь.
avs24rus
Устанавливая не дешевый датчик MH Z-19, в свое устройство, зачем ставить такую дрянь как DHT22? Ну ведь уже много кто и много где, писали, что хуже датчик — поискать надо!
Ну ведь полно более годных сенсоров, например Si7021 и подобные ему.
Cadil_TM Автор
Я об точности измерения влажности DHT22 узнал после ее покупки и убедился на своем опыте. Обязательно заменю!
undisclosed
Если не нужны очень точные значения влажности, то зачем платить больше?
Большинству достаточно точности: Сухо/Норм/Влажно. DHT22 — гораздо точнее этого)
Кстати особого выбора по CO2 нет и дешевле MH Z-19 нет ничего на алике.
Anynickname
avs24rus
Я имел ввиду, что этот датчик — самый дорогой в данном устройстве, пожалуй его стоимость сопоставима со стоимостью всего устройства.
То что он самый не дорогой из аналогичных, я в курсе.
Мне самому случайно перепала парочка К30.
Cadil_TM Автор
«И судя по графику, есть подозрение, что здесь показывает он не очень точно ...»
Кривая графика СО2 уходит за нижний край диапазона шкалы. Скриншот был сделан около года назад — тогда не обратил внимания, а сейчас, к сожалению, не могу воспроизвести его в другом масштабе — Blynk не хранит данные так долго.
REPISOT
Опять схема в виде цветных линий между пимпочками. За это и не люблю Ардуинщиков. Купил датчик на другой плате (или другую Ардуину) — и думай, куда чего подключать.
И почему не купить адаптер сразу на 5в? Передатчик имеет питание 3-12В.
Cadil_TM Автор
Передатчик с питанием 5В обеспечивает связь только до 2м при прямой видимости.
REPISOT
2м? Не, не верю. Ищите косяки монтажа, паразитные экраны, делайте правильную антенну.
Cadil_TM Автор
«2м? Не, не верю. Ищите косяки монтажа, паразитные экраны, делайте правильную антенну.»
Я тоже не верил. Пока сам немного не помучился и не открыл новую тему на одном из форумов. Там меня убедили, что можно верить в полученный результат.
Другой пример. Мой серийный термостат на время покупки без проблем работал в любой точке квартиры через две стены, а на 4-м году эксплуатации (со сбоями!) перекрывает расстояние около 3м на прямой видимости. Причем, проблема носит массовый характер. Наберите в Гугле: беспроводной комнатный термостат compu****q7rf и сами убедитесь в этом.
На форумах пишут — причина уменьшения дальности — уход частоты. Но отчего уходит частота, какой элемент пары плывет со временем? — внятного объяснения я не нашел. Возможно вы знаете ответ?
shadson
Что думаете с корпусом делать?
Cadil_TM Автор
До корпуса, как до неба. Пока не знаю. Возможно, 3D принтер.
shadson
Тяжело будет нормальный корпус вокруг экрана сгородить.
У меня похожая задача, пока остановился на том, что всю электронику и потроха прячу в страшном стандартном корпусе где-то там глубоко в шкафу, а вместо экрана управления на стену планшет в облегающем кейсе.
rautate
Я сделал два корпуса, 3Д принтер, и банально — маленькая картонная коробка :)))
Корпус, желательно из ПЛА.
Но у меня он сейчас в картонной коробке, почему? есть предположение что корпус «фонит». т.е. искажает данные датчиков.
avs24rus
Буквально пару месяцев назад я все таки решил проблему с корпусом для 7" дисплейного модуля. Заказал 3Д печать. Получилось дорого, но вполне достойно.
Cadil_TM Автор
Круто!
rautate
Сделал похожее, только для меня было важно чтобы всё было в одной коробке. Поэтому я использовал малинку (Raspberry Pi 3), где и сохраняется измерения (температура, влажность, CO2, CO, NH4, VOC, формальдегид и количество пыли) — в MongoDB. Как фронт-енд простенькое решение на ReactJS, данные читаются через NodeJS.
Датчики — MH Z-19 для CO2
ZH03A (поменял на PMS-7003, более точный) — Пыль
ZE8-CH20 — Формальдегид
Bosch BME680 — VOC, влажность, температура, давление
Есть и Wi-Fi подключение (у малинки он есть встроенный). Т.е. могу использовать и с подключением и без.
Для меня было важно, смотреть информацию за большой промежуток времени, ну скажем год. И чтобы не зависел от каких-то стороних серверов. Поэтому все на месте сохраняется. 64Гб флэшка пока хватает.
Cadil_TM Автор
У вас сложное специализированное устройство. Успехов!
Я же пытаюсь сделать простой и информативный прибор, чтобы им мог пользоваться даже далекий от электроники и программирования человек.
shadson
del
SergLine
По поводу точности измерения влажности: посмотрите в сторону BME280, как показывает практика, DHT22 не стоит применять!
Радиоинтерфейс: при использовании SX1278 (Lora) не будет проблем с дальностью связи!
Cadil_TM Автор
«Радиоинтерфейс: при использовании SX1278 (Lora) не будет проблем с дальностью связи!»
Мне уже не терпится нажать кнопку «Купить одним кликом» в интернет-магазине. Сдерживает только цена пары.
Успехов!
Bonio
А я как раз задумал уличный wifi термометр на BME280 и ESP8266 делать, но пока застрял на этапе сборки корпуса. Хотел его из серой канализационной трубы сделать, но столкнулся с тем, что она ничем не клеится, вообще ни чем!
avs24rus
Термоклей тоже нет?
Bonio
Термоклей это не надежно. Да и он не возьмет, я уже что только не перепробовал.
Bonio
Сейчас попробовал термоклей, он и правка, кажется, лучше всех держит. Спасибо.
PR200SD
«Часы RTC DS3231»
Зачем они в этой системе?