// электроотопление по ночному тарифу
//пробую спрогнозировать количество нужного тепла на следующие сутки на основе прогноза температуры на сутки
//запускается веб сервер на точке доступа, все параметры (пока кроме телеграмм) вводятся на странице настройки
//возможно обновление по "воздуху" OTA
// параметры хранятся в EEPROM. В случае пустой памяти (первый запуск) - пишутся дефолтные данные, чтоб не висло
// SSID и WiFi_pass можна задать через сериал в формате: wifissid <значение>, wifipass <значение>, по одному в строке. В EEPROM не сохраняет, сохранить можно на странице настроек
//телеметрию можно посмотреть в вебинтерфейсе, на дисплее, так и запросить через телеграм /status
//команды доступные через телеграм: /status, /stop, /pusk/, /help, /ok, /heating50, / heating100
//при недоступности прогноза для работы используется датчик 6 "Улица"
// средний/средние датчики используются для нагрева нужного кол-ва воды на основе прогноза
//греется 25, 50, 75, 100% обьема в соответствии с настройками (зависит от Т прогноза или улицы)
//возможен принудительный нагрев 50 или 100% обьема командой в телеге или веб странице
//опрашиваются два входных пина: один для нагрева на мах мощности (реле времени ночной тариф), второй - термостат в доме (нагрев на мin мощности до откл термостата)
// есть пин для термостата в доме для нагрева на мин мощности вне ночного тарифа
//на схеме вместо реле светодиоды, что один хрен
//остановить работу автоматики можно через телеграм /stop или на веб странице. После остановки нужен перезапуск или команда /pusk
//в телеграм шлются ошибки
//для избежания гемора с адресами датчиков DS18B20 и взаимозаменяемости - каждый датчик на свой пин
// пины выбраны под Waveshare ESP32-S3 Relay 6CH
//автоматика работает от бесперебойного питания
//в настройках можно включить/отключить детекцию напряжения
//при включеной детекции и отсутствии напряжения сети на котле - насос котла и котел отключается, работает только цирк насос системы
//при включеной детекции сообщения вкл/выкл 220В приходят в телеграм
//для детекции напряжения на входе использован https://arduino.ua/ru/prod2141-detektor-napryajeniya-220v
//при наличии критических ошибок или откл питания сигнал "работа" отключается, насос системы вкл, контроль передается на резервную автоматику запитаную через НЗ контакт реле "Работа"
#include <OneWire.h>
#include <DallasTemperature.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include <UniversalTelegramBot.h>
#include <Update.h> // include Update library
#include <WebServer.h> // include WebServer library
#include <EEPROM.h>
//дисплей---------------
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define I2C_SDA 15
#define I2C_SCL 16
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
int displayCount = 0;
//дисплей---------------
WiFiClientSecure client; //инициализация ClientSecure
UniversalTelegramBot bot("", client); //инициализация TelegramBot
WebServer server(80); // запуск вебсервер прт 80
float temp1, temp2, temp3, temp4, temp5, temp6; //переменные температур с датчиков
String ErrorDt; //переменная ошибки датчиков
const char* ap_ssid = "Automatika"; //ssid парль точки доступа
const char* ap_password = "12345678";
const long wifiCheckInterval = 30000; // Период проверки состояния WiFi (в миллисекундах)
unsigned long previousWiFiCheckMillis = 0;
unsigned long previousRabotaCheckMillis = 0;
unsigned long telega_errors_millis = 0;
int telega_errors = 0;
int telega_errors_stop = 0;
unsigned long updateTime;
unsigned long lastTime = 0;
int telega_interval = 1000;
unsigned long telega_millis = 0;
int manualStop = 0;
int heating50 = 0;
int heating100 = 0;
int helloMesage = 0;
int rabotaError = 0;
float temp_c = 20; //средняя температура на след сутки для старта
int errorWeather = 5; //стартовый флаг ошибки получения прогноза
int error220v = 0;
unsigned long intervall220 = 0;
unsigned long intervallDisp = 0;
// Переменные для отслеживания состояния насоса
unsigned long pumpStopTime = 0;
bool pumpShouldStop = false;
bool powerSignal;
bool thermostatSignal;
bool pump2Power;
String inputString = ""; // строка для хранения данных из Serial
String wifissid = ""; // переменная для хранения WiFi SSID из Serial
String wifipass = ""; // переменная для хранения пароля WiFi из Serial
// Запуск RS485
//#define RXD2 18 // does nothing on sending ESP32
//#define TXD2 17
#define RGB_PIN 38 //RGB pin. С ESP32-S3 тут почемуто GRB :-)
const int buzzerPin = 21; //пин бузера
// Пины управления
#define PIN_HEATER_LOW 1 // мин мощность котла
#define PIN_HEATER_HIGH 2 // мах мощность котла
#define PIN_PUMP 41 //циркуляц насос котла
#define PIN_PUMP2 45 //циркуляц насос системы (отключен когда реле включено, для дублируюющей автоматики)
#define PIN_THERMOSTAT 47 //входной пин для включения нагрева на мин мощности (термостат в доме)
#define PIN_POWER 12 //входной пин для включения нагрева на мах мощности (реле времени ночной тариф)
#define PIN_VALVE 42 //вентиль к теплоаккумулятору (закрыт при нагреве при мин мощ днем)
#define PIN_RABOTA 46 // индикация работы системы без ошибок "Работа"
#define PIN_220V 48 //под датчик напряжения 220В https://arduino.ua/ru/prod2141-detektor-napryajeniya-220v
// Пины подключения датчиков температуры
#define ONE_WIRE_BUS_1 3
#define ONE_WIRE_BUS_2 4
#define ONE_WIRE_BUS_3 5
#define ONE_WIRE_BUS_4 6
#define ONE_WIRE_BUS_5 7
#define ONE_WIRE_BUS_6 8
// Создаем объекты OneWire для каждого датчика
OneWire oneWire1(ONE_WIRE_BUS_1);
OneWire oneWire2(ONE_WIRE_BUS_2);
OneWire oneWire3(ONE_WIRE_BUS_3);
OneWire oneWire4(ONE_WIRE_BUS_4);
OneWire oneWire5(ONE_WIRE_BUS_5);
OneWire oneWire6(ONE_WIRE_BUS_6);
// Создаем объекты DallasTemperature для каждого датчика
DallasTemperature sensors1(&oneWire1);
DallasTemperature sensors2(&oneWire2);
DallasTemperature sensors3(&oneWire3);
DallasTemperature sensors4(&oneWire4);
DallasTemperature sensors5(&oneWire5);
DallasTemperature sensors6(&oneWire6);
//адреса памяти записи конфигов
#define EEPROM_SIZE 650
#define SSID_ADDR 0
#define PASS_ADDR 33
#define KEY_ADDR 66
#define LAT_ADDR 99
#define LON_ADDR 132
#define MIN_T_ADDR 165
#define MAX_T_ADDR 198
#define PUMP_STOP_ADDR 231
#define TEMP_100_ADDR 264
#define TEMP_75_ADDR 297
#define TEMP_50_ADDR 330
#define TEMP_25_ADDR 363
#define RABOTA_INT_ADDR 396
#define DELAY_WEATHER_ADDR 429
#define TELEGA_ERRORS_ADDR 462
#define PIN_220V_USE_ADDR 495
#define CHAT_ID_ADDRESS 528 // Начальный адрес для хранения CHAT_ID
#define TOKEN_ADDRESS 561 // Начальный адрес для хранения BOTtoken
//размер ячеек для записи конфигов
char ssid[33];
char password[33];
char mapApiKey[33];
char lat[33];
char lon[33];
char minT[33];
char maxT[33];
char pumpStopDelay[33];
char tempStop100[33];
char tempStop75[33];
char tempStop50[33];
char tempStop25[33];
char rabotaCheckInterval[33];
char timerDelayWeather[33];
char telega_errors_interval[33];
char pin220Use[33];
char CHAT_ID[33];
char BOTtoken[55];
// страница OTA Update
const char* updatePage =
"<html><meta charset='UTF-8'>"
"<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>"
"<style>"
"body { font-family: Arial, sans-serif; margin: 20px; }"
"h1 { color: #2c3e50; }"
"p { margin-bottom: 10px; }"
"a { color: #2980b9; text-decoration: none; }"
"a:hover { text-decoration: underline; }"
"input { padding: 5px; border: 1px solid #ddd; text-align: left; }"
"input[type=submit] { background-color: #3498db; color: white; padding: 10px 20px; border: none; cursor: pointer; }"
"input[type=submit]:hover { background-color: #2980b9; }"
"</style>"
"<body><h1>OTA Update</h1>"
"<p><a href='/'>Головна</a> | <a href='/setup'>Налаштування</a></p>"
"<form method='POST' action='#' enctype='multipart/form-data' id='upload_form'>"
"<input type='file' name='update'>"
"<input type='submit' value='Update'>"
"</form>"
"<div id='prg'>Progress: 0%</div>"
"<script>"
"$('form').submit(function(e){"
"e.preventDefault();"
"var form = $('#upload_form')[0];"
"var data = new FormData(form);"
" $.ajax({"
"url: '/update',"
"type: 'POST',"
"data: data,"
"contentType: false,"
"processData:false,"
"xhr: function() {"
"var xhr = new window.XMLHttpRequest();"
"xhr.upload.addEventListener('progress', function(evt) {"
"if (evt.lengthComputable) {"
"var per = evt.loaded / evt.total;"
"$('#prg').html('progress: ' + Math.round(per*100) + '%');"
"}"
"}, false);"
"return xhr;"
"},"
"success:function(d, s) {"
"console.log('success!')"
"},"
"error: function (a, b, c) {"
"}"
"});"
"});"
"</script>"
"</body></html>";
// Конец Для обновления по воздуху
/********************************************************** setup *********************************************************/
void setup() {
// Настройка сериал порта для вывода данных
Serial.begin(115200);
//дисплей---------------
Wire.begin(I2C_SDA, I2C_SCL);
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Don't proceed, loop forever
}
display.cp437(true);
display.clearDisplay();
int x = 10; int y = 10;
display.fillCircle(x, y, 5, SSD1306_WHITE); // Левый верхний кружок сердечка
display.fillCircle(x + 10, y, 5, SSD1306_WHITE); // Правый верхний кружок сердечка
display.fillTriangle(x - 7, y, x + 17, y, x + 5, y + 20, SSD1306_WHITE); // Нижняя часть сердечк
//display.drawBitmap(32, 16, heart_bmp, 12, 12, SSD1306_WHITE);
// Рисуем текст "LOVE" под сердечком
display.setTextSize(2); // Размер текста
display.setTextColor(SSD1306_WHITE);
display.setCursor(10, 32); // Позиция текста ниже сердечка
display.println(utf8rus("Вiтання,"));
display.println(utf8rus(" Кохання!"));
display.display();
//дисплей---------------
// Запуск RS485
// Serial2.begin(9600, RXD2, TXD2);
// pinMode(RXD2, INPUT);
// pinMode(TXD2, OUTPUT);
// Serial2.write("data");
// String receivedIntA = Serial2.read();
if (!EEPROM.begin(EEPROM_SIZE)) { //перезапуск если непрочитана память
Serial.println("Failed to initialize EEPROM\nRestarting...");
delay(1000);
ESP.restart();
}
readEEPROM(); //читаем память
// Память пуста? Проверяем, есть ли уже сохранённые значения? если нет - пишем
if (String(maxT).length() == 0 || String(tempStop25).length() == 0) {
Serial.println("EEPROM пуст. Записываем дефолтные значения.");
writeStringToEEPROM(SSID_ADDR, "Internet");
writeStringToEEPROM(PASS_ADDR, "12345678");
writeStringToEEPROM(MIN_T_ADDR, "40");
writeStringToEEPROM(MAX_T_ADDR, "80");
writeStringToEEPROM(PUMP_STOP_ADDR, "60");
writeStringToEEPROM(TEMP_100_ADDR, "0");
writeStringToEEPROM(TEMP_75_ADDR, "5");
writeStringToEEPROM(TEMP_50_ADDR, "10");
writeStringToEEPROM(TEMP_25_ADDR, "15");
writeStringToEEPROM(RABOTA_INT_ADDR, "5");
writeStringToEEPROM(DELAY_WEATHER_ADDR, "60");
writeStringToEEPROM(TELEGA_ERRORS_ADDR, "3");
writeStringToEEPROM(PIN_220V_USE_ADDR, "0");
Serial.println("Дефолтные значения записаны. Перезапуск");
ESP.restart();
}
bot = UniversalTelegramBot(BOTtoken, client); // Инициализируем бот с токеном
neopixelWrite(RGB_PIN,0,0,0); // Off / black тушим светодиод RGB
/*
neopixelWrite(RGB_PIN,RGB_BRIGHTNESS,0,0); // Red /У Waveshare ESP32-S3 зеленый и красный спутаны местами
neopixelWrite(RGB_PIN,0,RGB_BRIGHTNESS,0); // Green
neopixelWrite(RGB_PIN,0,0,RGB_BRIGHTNESS); // Blue
neopixelWrite(RGB_PIN,0,0,0); // Off / black
*/
tone(buzzerPin, 1000, 100); tone(buzzerPin, 2500, 100); //бип бузера при старте
//noTone(buzzerPin);
//создаем точку доступа
WiFi.softAP(ap_ssid, ap_password);
Serial.println("Access Point started\nIP Address: " + WiFi.softAPIP().toString());
connectToWiFi();//подкл вайфай
// Инициализация датчиков
sensors1.begin();
sensors2.begin();
sensors3.begin();
sensors4.begin();
sensors5.begin();
sensors6.begin();
// Настройка пинов
pinMode(PIN_HEATER_LOW, OUTPUT);
pinMode(PIN_HEATER_HIGH, OUTPUT);
pinMode(PIN_PUMP, OUTPUT);
pinMode(PIN_PUMP2, OUTPUT);
pinMode(PIN_THERMOSTAT, INPUT);
pinMode(PIN_POWER, INPUT);
pinMode(PIN_VALVE, OUTPUT);
pinMode(PIN_RABOTA, OUTPUT);
pinMode(PIN_220V, INPUT);
// Установка начальных состояний
digitalWrite(PIN_HEATER_LOW, LOW); // мин мощность котла
digitalWrite(PIN_HEATER_HIGH, LOW); // мах мощность котла
digitalWrite(PIN_PUMP, LOW); //циркуляц насос котла
digitalWrite(PIN_PUMP2, HIGH); //циркуляц насос системы (отключен когда реле включено, для дублируюющей автоматики)
digitalWrite(PIN_VALVE, LOW); //вентиль к теплоаккумулятору открыт
digitalWrite(PIN_RABOTA, LOW);// индикация работы системы без ошибок
//страницы веб сервера
server.on("/", handleRoot);
server.on("/setup", handleSetup);
server.on("/restart", handleReboot);
server.on("/update", HTTP_GET, []() { server.send(200, "text/html", updatePage); });
server.on("/update", HTTP_POST, []() {
server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
ESP.restart();
}, []() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
if (!Update.begin(UPDATE_SIZE_UNKNOWN)) Update.printError(Serial);
} else if (upload.status == UPLOAD_FILE_WRITE) {
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) Update.printError(Serial);
} else if (upload.status == UPLOAD_FILE_END) {
if (Update.end(true)) Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
else Update.printError(Serial);
}
});
inputString.reserve(200); // выделяем память для строки команд через сериал
Serial.println("Введіть SSID в форматі: wifissid <значення>");
Serial.println("Введіть WiFi pass в форматі: wifissid <значення>");
Serial.println("Не забудьте зберегти налаштування");
// start the web server
server.begin();
rabotaKotla();
if (WiFi.status() == WL_CONNECTED) {
loadJson();
}
}
/********************************************************** loop *********************************************************/
void loop() {
unsigned long currentMillis = millis();
// Повторение цикла автоматики
if (currentMillis - previousRabotaCheckMillis >= atoi(rabotaCheckInterval)*1000) {
previousRabotaCheckMillis = currentMillis;
rabotaKotla();
}
// Проверка WiFi соединения
if (currentMillis - previousWiFiCheckMillis >= wifiCheckInterval) {
previousWiFiCheckMillis = currentMillis;
checkWiFiConnection();
}
//Если ошибка подключения к прогнозу - сокращаю период опроса до 1 мин
if (errorWeather != 0) updateTime = 60000; else updateTime = atoi(timerDelayWeather)*60000;
//загрузка прогноза погоды
if ((currentMillis - lastTime) >= updateTime && WiFi.status() == WL_CONNECTED && String(mapApiKey).length() != 0 ) {
lastTime = currentMillis;
loadJson();
}
// старт сервер для обновления
server.handleClient();
//Опрос телеграм бота и отсылка ошибок
if (WiFi.status() == WL_CONNECTED && String(BOTtoken).length() != 0 ) {
read_telega();
}
// Проверяем, есть ли новые данные в Serial
if (Serial.available()) {
char inChar = (char)Serial.read(); // считываем символ из Serial
inputString += inChar; // добавляем символ в строку
// Если получена команда полностью (завершается символом новой строки)
if (inChar == '\n') {
handleCommand(inputString); // вызываем функцию обработки команды
inputString = ""; // очищаем строку
}
}
if (currentMillis - intervall220 >= 2000 && atoi(pin220Use) == 1){//проверяем подключение 220
intervall220 = currentMillis;
check_220v();
}
if (currentMillis - intervallDisp >= 3000 && display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)){//обновляем дисплей
intervallDisp = currentMillis;
displayRenew();
}
}
/********************************************************** connectToWiFi *********************************************************/
//подкл вайфай
void connectToWiFi() {
Serial.println("Підключення до WiFi...");
WiFi.begin(ssid, password);
if (WiFi.status() == WL_CONNECTED) {
Serial.println("WiFi підключено.\nIP адреса: " + WiFi.localIP().toString());
} else {
Serial.println("Не вдалося підключитися до WiFi.");
}
}
/********************************************************** checkWiFiConnection *********************************************************/
//проверка состояния вайфай
void checkWiFiConnection() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi відтключено, спроба перепідключення...");
connectToWiFi();
}
}
/********************************************************** rabotaKotla *********************************************************/
void rabotaKotla() {
// Чтение температур с датчиков
sensors1.requestTemperatures();
sensors2.requestTemperatures();
sensors3.requestTemperatures();
sensors4.requestTemperatures();
sensors5.requestTemperatures();
sensors6.requestTemperatures();
temp1 = sensors1.getTempCByIndex(0);
temp2 = sensors2.getTempCByIndex(0);
temp3 = sensors3.getTempCByIndex(0);
temp4 = sensors4.getTempCByIndex(0);
temp5 = sensors5.getTempCByIndex(0);
temp6 = sensors6.getTempCByIndex(0);
//проверяю датчики на ошибки
ErrorDt = "";
if (temp1 == -127 || isnan(temp1)) ErrorDt += "1";
if (temp2 == -127 || isnan(temp2)) ErrorDt += "2";
if (temp3 == -127 || isnan(temp3)) ErrorDt += "3";
if (temp4 == -127 || isnan(temp4)) ErrorDt += "4";
if (temp5 == -127 || isnan(temp5)) ErrorDt += "5";
if (temp6 == -127 || isnan(temp6)) ErrorDt += "6";
thermostatSignal = digitalRead(PIN_THERMOSTAT);
powerSignal = digitalRead(PIN_POWER);
if (temp1 >= 90 || temp2 >= 90) {//если перегрев
tone(buzzerPin, 500, 2000);//бип ошибки
}
if (temp5 <= 5 && ErrorDt == "") {//если переохлаждение
heating50 = 1;//запуск принудительного нагрева 50%
tone(buzzerPin, 500, 2000);//бип ошибки
}
// Вывод данные в сериал порт
Serial.print("Temp1: ");
Serial.print(temp1);
Serial.print(", Temp2: ");
Serial.print(temp2);
Serial.print(", Temp3: ");
Serial.print(temp3);
Serial.print(", Temp4: ");
Serial.print(temp4);
Serial.print(", Temp5: ");
Serial.print(temp5);
Serial.print(", Temp6: ");
Serial.print(temp6);
Serial.print(", Temp_c: ");
Serial.print(temp_c);
Serial.print(", ErrorDt: ");
Serial.print(ErrorDt);
Serial.print(", powerSignal: ");
Serial.print(powerSignal);
Serial.print(", thermostatSignal: ");
Serial.print(thermostatSignal);
Serial.print(", errorWeather: ");
Serial.println(errorWeather);
//Проверка на ошибки. Если есть ошибки - переходим на резервную автоматику или если ее нет - просто выкл индикация
if (ErrorDt != "") {//если ошибка датчика
if (errorWeather == 0 && ErrorDt == "6") {//если шибка датчика 6, но прогноз есть - продолжаем
neopixelWrite(RGB_PIN,165,255,0); // светодиод orange
rabotaError = 2;
goto bailout; //переходим к работе
}
else
{
// отключаем всё оборудование кроме насоса системы
digitalWrite(PIN_HEATER_LOW, LOW); // мин мощность котла
digitalWrite(PIN_HEATER_HIGH, LOW); // мах мощность котла
//digitalWrite(PIN_PUMP, LOW); //циркуляц насос котла
digitalWrite(PIN_PUMP2, LOW); //циркуляц насос системы (отключен когда реле включено, для дублируюющей автоматики)
digitalWrite(PIN_VALVE, LOW); //вентиль к теплоаккумулятору открыт
digitalWrite(PIN_RABOTA, LOW);// индикация работы системы без ошибок
tone(buzzerPin, 1000, 200);// бип бузер
neopixelWrite(RGB_PIN,0,255,0); // светодиод красный
if (!pumpShouldStop) {
pumpStopTime = millis();
pumpShouldStop = true;
}
pumpStop();
rabotaError = 1;
}
}
else bailout: { //если критических ошибок нет - работа
rabotaError = 0;
if (manualStop == 1) { //если ручная остановка через бот
// отключаем всё оборудование кроме насоса системы
digitalWrite(PIN_HEATER_LOW, LOW); // мин мощность котла
digitalWrite(PIN_HEATER_HIGH, LOW); // мах мощность котла
//digitalWrite(PIN_PUMP, LOW); //циркуляц насос котла
digitalWrite(PIN_PUMP2, LOW); //циркуляц насос системы (отключен когда реле включено, для дублируюющей автоматики)
digitalWrite(PIN_VALVE, LOW); //вентиль к теплоаккумулятору открыт
digitalWrite(PIN_RABOTA, HIGH);// индикация работы системы без ошибок
//tone(buzzerPin, 1000, 200);// бип бузер
neopixelWrite(RGB_PIN,0,255,0); // светодиод красный
if (!pumpShouldStop) {
pumpStopTime = millis();
pumpShouldStop = true;
}
pumpStop();
return;
}
if (errorWeather == 0 && ErrorDt == "6") { //если есть прогноз, но нет датчика 6
//tone(buzzerPin, 500, 200);//бип ошибки
neopixelWrite(RGB_PIN,165,255,0); // светодиод orange
}
if (errorWeather != 0 && ErrorDt == "") { //если не удалось получить прогноз- берем температуру улицы с temp6
temp_c = temp6;
//tone(buzzerPin, 500, 200);//бип ошибки
neopixelWrite(RGB_PIN,0,0,255); // Blue
}
if (errorWeather == 0 && ErrorDt == "") { //если всё ок
neopixelWrite(RGB_PIN,255,0,0); // Green
}
digitalWrite(PIN_RABOTA, HIGH); // работа возможна- включаем индикацию РАБОТА
//нагреваем часть бака в зависимости от прогноза среднесуточной температуры
//чем выше температура по прогнозу - тем меньше воды греем (1/4, 1/2, 3/4, 4/4 бака)
//определяем предельный датчик для нагрева
float BottomTemp = temp5; //думаем греть максимум воды
if (heating50 == 0 && heating100 == 0){//без принудительного нагрева
if (temp_c < atof(tempStop100)) BottomTemp = temp5; //если среднесуточная температура или температура внешнего датчика улица меньше чем в настройках EEPROM для 100%
else
if (temp_c < atof(tempStop75)) BottomTemp = temp4; //если среднесуточная температура или температура внешнего датчика улица меньше чем в настройках EEPROM для 75%
else
if (temp_c < atof(tempStop50)) BottomTemp = temp3; //если среднесуточная температура или температура внешнего датчика улица меньше чем в настройках EEPROM для 50%
else
if (temp_c < atof(tempStop25)) BottomTemp = temp2; //если среднесуточная температура или температура внешнего датчика улица меньше чем в настройках EEPROM для 25%
else
if (temp_c >= atof(tempStop25)) powerSignal = false; //если среднесуточная температура или температура внешнего датчика улица выше чем в настройках EEPROM для 25% -отключаем нагрев
} else if (heating50 == 1 && rabotaError != 1) { //принудительный нагрев 50%
BottomTemp = temp3;
powerSignal = true;
} else if (heating100 == 1 && rabotaError != 1) { //принудительный нагрев 100%
BottomTemp = temp5;
powerSignal = true;
}
// Логика управления отоплением
if (BottomTemp < atof(maxT) && powerSignal) {
// Котел на полной мощности, насос включен, вентиль открыт
if (error220v == 0) {//если нет 220 - откл реле и насос
digitalWrite(PIN_HEATER_LOW, HIGH);
digitalWrite(PIN_HEATER_HIGH, HIGH);
digitalWrite(PIN_PUMP, HIGH);
pumpShouldStop = false;
} else {
digitalWrite(PIN_HEATER_LOW, LOW);
digitalWrite(PIN_HEATER_HIGH, LOW);
if (!pumpShouldStop) {
pumpStopTime = millis();
pumpShouldStop = true;
}
}
digitalWrite(PIN_VALVE, LOW);
} else if (BottomTemp >= atof(maxT)) {
// Котел выключен, насос отключается с задержкой pumpStopDelay, вентиль открыт
digitalWrite(PIN_HEATER_LOW, LOW);
digitalWrite(PIN_HEATER_HIGH, LOW);
if (!pumpShouldStop) {
pumpStopTime = millis();
pumpShouldStop = true;
}
digitalWrite(PIN_VALVE, LOW);
heating50 = 0;
heating100 = 0;
} else if (!thermostatSignal && !powerSignal) {
// Котел выключен, насос выключен, вентиль открыт
digitalWrite(PIN_HEATER_LOW, LOW);
digitalWrite(PIN_HEATER_HIGH, LOW);
//digitalWrite(PIN_PUMP, LOW);
digitalWrite(PIN_VALVE, LOW);
//pumpShouldStop = false;
if (!pumpShouldStop) {
pumpStopTime = millis();
pumpShouldStop = true;
}
} else if (temp1 < atof(minT) && thermostatSignal) {
// Котел на первой ступени мощности, насос включен, вентиль закрыт
if (error220v == 0) {//если нет 220 - откл реле и насос
digitalWrite(PIN_HEATER_LOW, HIGH);
digitalWrite(PIN_PUMP, HIGH);
digitalWrite(PIN_VALVE, HIGH);
pumpShouldStop = false;
} else {
digitalWrite(PIN_HEATER_LOW, LOW);
digitalWrite(PIN_VALVE, LOW);
if (!pumpShouldStop) {
pumpStopTime = millis();
pumpShouldStop = true;
}
}
digitalWrite(PIN_HEATER_HIGH, LOW);
} else if (temp1 >= (atof(minT)+1)) {
// Котел выключен, насос отключается с задержкой pumpStopDelay, вентиль открыт
digitalWrite(PIN_HEATER_LOW, LOW);
digitalWrite(PIN_HEATER_HIGH, LOW);
if (!pumpShouldStop) {
pumpStopTime = millis();
pumpShouldStop = true;
}
digitalWrite(PIN_VALVE, LOW);
}
// Отключение насоса с задержкой pumpStopDelay после полного выключения котла
pumpStop();
//индикация нагрева & управление насосом системы
bool powerHighSignal = digitalRead(PIN_HEATER_HIGH);//читаем состояние пинов
bool powerLowSignal = digitalRead(PIN_HEATER_LOW);
if (powerHighSignal || powerLowSignal)
digitalWrite(PIN_PUMP2, LOW);//вкл насос системы
else {
if (temp1 >= atof(minT)){
digitalWrite(PIN_PUMP2, LOW);//вкл насос системы
pump2Power = false;}
else{
digitalWrite(PIN_PUMP2, HIGH);//откл насос системы
pump2Power = true;}
}
}
}
void pumpStop() {//остановка насоса котла с задержкой pumpStopDelay
if (pumpShouldStop) {
if (millis() - pumpStopTime >= atoi(pumpStopDelay)*1000) {
digitalWrite(PIN_PUMP, LOW);
pumpShouldStop = false;
}
}
}
/********************************************************** loadJson погода *********************************************************/
void loadJson() { //--------запрашиваем погоду
client.setInsecure();
String serverPath = "http://api.openweathermap.org/data/2.5/forecast?lat=" + String(lat) + "&lon=" + String(lon) + "&APPID=" + mapApiKey + "&units=metric";
// Подключаемся к серверу OpenWeatherMap
if (client.connect("api.openweathermap.org", 443)) {
Serial.println("Connected to OpenWeatherMap");
// Отправляем GET запрос
client.println("GET " + serverPath + " HTTP/1.1");
client.println("Host: api.openweathermap.org");
client.println("User-Agent: ESP32");
client.println("Connection: close");
client.println();
// Читаем ответ от сервера
String response = "";
while (client.connected() || client.available()) {
if (client.available()) {
response += client.readString();
}
}
client.stop();
// Находим начало JSON данных
int jsonStart = response.indexOf("\r\n\r\n");
if (jsonStart == -1) {
Serial.println("JSON not found");
errorWeather = 1;
temp_c = temp6;
return;
}
// Выделяем JSON часть из ответа
String jsonResponse = response.substring(jsonStart + 4);
// Парсим JSON
DynamicJsonDocument doc(8192);
DeserializationError error = deserializeJson(doc, jsonResponse);
if (error) {
Serial.print("deserializeJson() failed: ");
Serial.println(error.c_str());
errorWeather = 2;
temp_c = temp6;
return;
}
// Получаем данные о погоде
//String temperature = doc.as<String>();
//String weatherDescription = doc["weather"][5]["description"].as<String>();
temp_c = 0;//обнуляю среднесуточную темп
for (int i=0; i<8; i++) { //плюсум темпер из погоды за 8 периодов по 3 часа (24/3=8)
float temp_c1 = doc["list"][i]["main"]["temp"];
temp_c = temp_c + temp_c1;
}
temp_c = temp_c/8; //получаю среднее температуры на след сутки
errorWeather = 0;//ошибок нет
int Error_cod = doc["cod"];
if (Error_cod == 401 && errorWeather == 0) {//если сервер вернул ошибку 401
errorWeather = 4;
Serial.println("Невірні дані з сервера. Код: " + String(Error_cod) + "");
temp_c = temp6;//среднесуточная тем берем с улицы
return; //прерываем функцию
}
// Выводим данные в Serial монитор
Serial.print("Temperature: ");
Serial.print(temp_c);
Serial.println(" °C");
} else {
Serial.println("Connection to OpenWeatherMap failed");
errorWeather = 3;
temp_c = temp6;//среднесуточная тем берем с улицы
}
}
/********************************************************** handleRoot html главная *********************************************************/
void handleRoot() {
if (server.method() == HTTP_POST) {//если была нажата кнопка
server.send(200, "text/html", "<html><meta charset='UTF-8' http-equiv='refresh' content='3;URL='/'><body><h1>Інформація оновлюється</h1></body></html>");
String stop = server.arg("stop");
String pusk = server.arg("pusk");
String heating50s = server.arg("heating50");
String heating100s= server.arg("heating100");
if (stop == "1") {//если нажили кнопку "Стоп"
manualStop = 1;
heating50 = 0;
heating100 = 0;
}
if (pusk == "1") {//если нажили кнопку "Пуск"
manualStop = 0;
heating50 = 0;
heating100 = 0;
}
if (heating50s == "1" && rabotaError != 1) {//если нажили кнопку "Нагрев 50%"
manualStop = 0;
heating50 = 1;
heating100 = 0;
}
if (heating100s == "1" && rabotaError != 1) {//если нажили кнопку "Нагрев 100%"
manualStop = 0;
heating50 = 0;
heating100 = 1;
}
} else {
bool powerHighSignal = digitalRead(PIN_HEATER_HIGH);//читаем состояние пинов
bool powerLowSignal = digitalRead(PIN_HEATER_LOW);
bool keyHighSignal = digitalRead(PIN_POWER);
int refresh_interval = atoi(rabotaCheckInterval); //частота обновления страницы сек
String html = "<html><meta charset='UTF-8'><meta http-equiv='Refresh' content='"+ String(refresh_interval) +"' />";
html += "<style>";
html += "body { font-family: Arial, sans-serif; margin: 20px; }";
html += "h1 { color: #2c3e50; }";
html += "p { margin-bottom: 10px; }";
html += "span { font-weight: bold; }";
html += "a { color: #2980b9; text-decoration: none; }";
html += "a:hover { text-decoration: underline; }";
html += ".warning { color: red; font-weight: bold; }";
html += ".info { color: green; font-weight: bold; }";
html += ".table { margin-bottom: 20px; table-layout: auto;}";
html += "td, th { padding: 5px; border: 1px solid #ddd; text-align: left; }";
html += "input[type=submit] { background-color: #3498db; color: white; padding: 10px 20px; border: none; cursor: pointer; }";
html += "input[type=submit]:hover { background-color: #2980b9; }";
html += "</style>";
html += "<body><h1>Електрокотел телеметрія</h1>";
html += "<p><a href='/setup'>Налаштування</a> | <a href='/update'>Оновлення</a></p>";
if (powerHighSignal || powerLowSignal) {//проверка нагрева
html += "<p class='info'>ІДЕ НАГРІВ " + String(powerHighSignal ? "MAX потужність" : "MIN потужність") + "</p>";
}
if (temp1 >= 90 || temp2 >= 90) {//проверка перегрева
html += "<p class='warning'>Перегрів!!! Температура: "+ String(temp1) +" °С! Перевірте.</p>";
}
if (temp5 <= 5 && ErrorDt == "") {//проверка переохлаждения
html += "<p class='warning'>Дуже низька температура: "+ String(temp5) +" °С! Перевірте.</p>";
}
if (manualStop == 1) {//если остановлено вручную кнопкой html или через бот
html += "<p class='warning'>Систему зупинено! Перевірте і перезапустіть вручну.</p>";
}
if (powerLowSignal && temp_c >= atof(tempStop25)) {//если идет нагрев и температура прогноз/улица высока (термостат, принудительный нагрев)
if (errorWeather !=0) {//с датчика улица
html += "<p class='warning'>Іде нагрів. Температура датчика вулиця "+ String(temp_c) +" °С! Перевірте.</p>";
} else {//с прогноза
html += "<p class='warning'>Іде нагрів. Середньодобова температура "+ String(temp_c) +" °С! Перевірте.</p>";
}
}
if (ErrorDt != "") {//если ошибки датчиков температуры
html += "<p class='warning'>Помилка роботи датчиків температури "+ String(ErrorDt) +"! Перевірте.</p>";
}
if (WiFi.status() == WL_CONNECTED && errorWeather !=0) {//если есть вайфай но нет прогноза
html += "<p class='warning'>Помилка доступу до OpenWeatherMap! Перевірте.</p>";
}
if (heating50 == 1 && rabotaError != 1) {//если принудит накрев 50%
html += "<p class='warning'>Примусовий нагрів 50%</p>";
} else if (heating100 == 1 && rabotaError != 1) {//если принудит накрев 100%
html += "<p class='warning'>Примусовий нагрів 100%</p>";
}
html += "<table class='table'>";
html += "<tr><th>Параметр</th><th>Значення</th></tr>";
html += "<tr><td>Wi-Fi</td><td>" + String(WiFi.status() == WL_CONNECTED ? "Підключено до " + String(ssid) : "НЕ підключено") + "</td></tr>";
html += "<tr><td>IP адреса</td><td>" + WiFi.localIP().toString() + "</td></tr>";
if (atoi(pin220Use) == 1) {
html += "<tr><td>Мережа 220В</td><td>" + String(digitalRead(PIN_220V) ? "<span style='color:red;'>Немає!</span>" : "<span style='color:green;'>Подано</span>") + "</td></tr>";
}
if (errorWeather !=0) {
html += "<tr><td>Температура датчика вулиця</td><td>" + String(temp_c) + " °С</td></tr>";
} else {
html += "<tr><td>Середньодобова температура</td><td>" + String(temp_c) + " °С</td></tr>";
}
html += "<tr><td>Датчик 1 | (верхій 0%)</td><td>" + String(temp1) + " °С</td></tr>";
html += "<tr><td>Датчик 2 | (25%)</td><td>" + String(temp2) + " °С</td></tr>";
html += "<tr><td>Датчик 3 | (середній 50%)</td><td>" + String(temp3) + " °С</td></tr>";
html += "<tr><td>Датчик 4 | (75%)</td><td>" + String(temp4) + " °С</td></tr>";
html += "<tr><td>Датчик 5 °С | (нижній 100%)</td><td>" + String(temp5) + " °С</td></tr>";
html += "<tr><td>Датчик 6 | (повітря вулиця)</td><td>" + String(temp6) + " °С</td></tr>";
html += "<tr><td>Сигнал нічний тариф (HP)</td><td>" + String(keyHighSignal ? "ВКЛ" : "ВИКЛ") + "</td></tr>";
html += "<tr><td>Нагрів High Power (HP)</td><td>" + String(powerHighSignal ? "ВКЛ" : "ВИКЛ") + "</td></tr>";
html += "<tr><td>Сигнал з термостата (LP)</td><td>" + String(thermostatSignal ? "ВКЛ" : "ВИКЛ") + "</td></tr>";
html += "<tr><td>Нагрів Low Power (LP)</td><td>" + String(powerLowSignal ? "ВКЛ" : "ВИКЛ") + "</td></tr>";
html += "<tr><td>Насос котла</td><td>" + String(digitalRead(PIN_PUMP) ? "ВКЛ" : "ВИКЛ") + "</td></tr>";
html += "<tr><td>Насос системи</td><td>" + String(digitalRead(PIN_PUMP2) ? "ВИКЛ" : "ВКЛ") + "</td></tr>";
html += "<tr><td>Стан вентиля</td><td>" + String(digitalRead(PIN_VALVE) ? "Закрито" : "Відкрито") + "</td></tr>";
html += "<tr><td>Працездатність</td><td>" + String(digitalRead(PIN_RABOTA) ? "ТАК" : "НІ") + "</td></tr>";
html += "</table>";
if (manualStop == 0) {//если ручная остановка не активна показываю кнопку Стоп
html += "<form action='/' method='POST'>";
html += "<p hidden><input type='text' name='stop' value='1'></p>";
html += "<p><input type='submit' value='Зупинити'></p>";
html += "</form>";
} else {//если в ручной остановке - показываю кнопку Пуск
html += "<form action='/' method='POST'>";
html += "<p hidden><input type='text' name='pusk' value='1'></p>";
html += "<p><input type='submit' value='Запуск'></p>";
html += "</form>";
}
if (heating50 == 0 && !powerHighSignal) { // если Нагрів 50% не активен - показываю кнопку
if (rabotaError != 1 && error220v == 0) {//нет критических ошибок в работе
html += "<form action='/' method='POST'>";
html += "<p hidden><input type='text' name='heating50' value='1'></p>";
html += "<p><input type='submit' value='Нагрів 50%'></p>";
html += "</form>";
}
}
if (heating100 == 0 && !powerHighSignal) { // если Нагрів 100% не активен - показываю кнопку
if (rabotaError != 1 && error220v == 0) {//нет критических ошибок в работе
html += "<form action='/' method='POST'>";
html += "<p hidden><input type='text' name='heating100' value='1'></p>";
html += "<p><input type='submit' value='Нагрів 100%'></p>";
html += "</form>";
}
}
html += "</body></html>";
server.send(200, "text/html", html);
}
}
/********************************************************** handleReboot html перезапуск *********************************************************/
void handleReboot() {
if (server.method() == HTTP_POST) {//если нажата кнопка "Перезавантажити\"
server.send(200, "text/html", "<html><meta charset='UTF-8' http-equiv='refresh' content='8;URL='/'><body><h1>Перезавантажується!</h1></body></html>");
delay(2000);
ESP.restart();
} else {
String html = "<html><meta charset='UTF-8'><body><h1>Перезавантаження!</h1>";
html += "<form action='/restart' method='POST'>";
html += "<p><a href='/'>Головна</a></p>";
html += "<p><input type='submit' value='Перезавантажити'></p>";
html += "</form></body></html>";
server.send(200, "text/html", html);
}
}
/********************************************************** handleSetup html настройки *********************************************************/
void handleSetup() {
if (server.method() == HTTP_POST) {//если нажата кнопка "Сохранить" - пишем настройки в память
server.send(200, "text/html", "<html><meta charset='UTF-8' http-equiv='refresh' content='8;URL='/setup'><body><h1>Налаштування оновлено</h1></body></html>");
String new_ssid = server.arg("ssid");
String new_password = server.arg("password");
String new_mapApiKey = server.arg("mapApiKey");
String new_lat = server.arg("lat");
String new_lon = server.arg("lon");
String new_minT = server.arg("minT");
String new_maxT = server.arg("maxT");
String new_pumpStopDelay = server.arg("pumpStopDelay");
String new_tempStop100 = server.arg("tempStop100");
String new_tempStop75 = server.arg("tempStop75");
String new_tempStop50 = server.arg("tempStop50");
String new_tempStop25 = server.arg("tempStop25");
String new_rabotaCheckInterval = server.arg("rabotaCheckInterval");
String new_timerDelayWeather = server.arg("timerDelayWeather");
String new_telega_errors_interval = server.arg("telega_errors_interval");
String new_pin220Use = server.arg("pin220Use");
String new_CHAT_ID = server.arg("CHAT_ID");
String new_BOTtoken = server.arg("BOTtoken");
writeEEPROM(
new_ssid.c_str()
, new_password.c_str()
, new_mapApiKey.c_str()
, new_lat.c_str()
, new_lon.c_str()
, new_minT.c_str()
, new_maxT.c_str()
, new_pumpStopDelay.c_str()
, new_tempStop100.c_str()
, new_tempStop75.c_str()
, new_tempStop50.c_str()
, new_tempStop25.c_str()
, new_rabotaCheckInterval.c_str()
, new_timerDelayWeather.c_str()
, new_telega_errors_interval.c_str()
, new_pin220Use.c_str()
, new_CHAT_ID.c_str()
, new_BOTtoken.c_str()
);
ESP.restart();
} else {
String html = "<html><meta charset='UTF-8'>";
html += "<style>";
html += "body { font-family: Arial, sans-serif; margin: 20px; }";
html += "h1 { color: #2c3e50; }";
html += "p { margin-bottom: 10px; }";
html += "a { color: #2980b9; text-decoration: none; }";
html += "a:hover { text-decoration: underline; }";
html += "input { padding: 5px; border: 1px solid #ddd; text-align: left; }";
html += "input[type=submit] { background-color: #3498db; color: white; padding: 10px 20px; border: none; cursor: pointer; }";
html += "input[type=submit]:hover { background-color: #2980b9; }";
html += "</style>";
html += "<body><h1>Налаштування параметрів</h1>";
html += "<p><a href='/'>Головна</a> | <a href='/update'>Оновлення</a></p>";
html += "<form action='/setup' method='POST'>";
html += "WiFi SSID: <input type='text' name='ssid' value='" + String(ssid) + "'><br>";
html += "WiFi Password: <input type='text' name='password' value='" + String(password) + "'><br>";
html += "OpenWeatherMap ApiKey: <input type='text' name='mapApiKey' value='" + String(mapApiKey) + "'><br>";
html += "BOTtoken: <input type='text' name='BOTtoken' value='" + String(BOTtoken) + "'><br>";
html += "CHAT_ID: <input type='text' name='CHAT_ID' value='" + String(CHAT_ID) + "'><br>";
html += "Широта Виду xx.xx: <input type='text' name='lat' value='" + String(lat) + "'><br>";
html += "Довгота Виду xx.xx: <input type='text' name='lon' value='" + String(lon) + "'><br>";
html += "Мінімальна температура °С вмикання насосу системи: <input type='text' name='minT' value='" + String(minT) + "'><br>";
html += "Максимальна температура °С нагріву бака: <input type='text' name='maxT' value='" + String(maxT) + "'><br>";
html += "Затримка вимкнення насосу котла сек: <input type='text' name='pumpStopDelay' value='" + String(pumpStopDelay) + "'><br>";
html += "Період читання датчиків сек: <input type='text' name='rabotaCheckInterval' value='" + String(rabotaCheckInterval) + "'><br>";
html += "Середньодобова Т°С для нагріву 100% об'єму бака: <input type='text' name='tempStop100' value='" + String(tempStop100) + "'><br>";
html += "Середньодобова Т°С для нагріву 75% об'єму бака: <input type='text' name='tempStop75' value='" + String(tempStop75) + "'><br>";
html += "Середньодобова Т°С для нагріву 50% об'єму бака: <input type='text' name='tempStop50' value='" + String(tempStop50) + "'><br>";
html += "Середньодобова Т°С для нагріву 25% об'єму бака: <input type='text' name='tempStop25' value='" + String(tempStop25) + "'><br>";
html += "Частота оновлення прогнозу погоди хвилин: <input type='text' name='timerDelayWeather' value='" + String(timerDelayWeather) + "'><br>";
html += "Частота надсилання повідомлень (помилок) хвилин: <input type='text' name='telega_errors_interval' value='" + String(telega_errors_interval) + "'><br>";
html += "Використовувати датчик напруги 220В (1-так, 0 - ні): <input type='text' name='pin220Use' value='" + String(pin220Use) + "'><br>";
html += "<p><input type='submit' value='Зберегти'></p>";
html += "</form></body></html>";
server.send(200, "text/html", html);
}
}
/********************************************************** readEEPROM *********************************************************/
void readEEPROM() {
EEPROM.get(SSID_ADDR, ssid);
EEPROM.get(PASS_ADDR, password);
EEPROM.get(KEY_ADDR, mapApiKey);
EEPROM.get(LAT_ADDR, lat);
EEPROM.get(LON_ADDR, lon);
EEPROM.get(MIN_T_ADDR, minT);
EEPROM.get(MAX_T_ADDR, maxT);
EEPROM.get(PUMP_STOP_ADDR, pumpStopDelay);
EEPROM.get(TEMP_100_ADDR, tempStop100);
EEPROM.get(TEMP_75_ADDR, tempStop75);
EEPROM.get(TEMP_50_ADDR, tempStop50);
EEPROM.get(TEMP_25_ADDR, tempStop25);
EEPROM.get(RABOTA_INT_ADDR, rabotaCheckInterval);
EEPROM.get(DELAY_WEATHER_ADDR, timerDelayWeather);
EEPROM.get(TELEGA_ERRORS_ADDR, telega_errors_interval);
EEPROM.get(PIN_220V_USE_ADDR, pin220Use);
EEPROM.get(CHAT_ID_ADDRESS, CHAT_ID);
EEPROM.get(TOKEN_ADDRESS, BOTtoken);
}
/********************************************************** writeEEPROM *********************************************************/
void writeEEPROM(const char* new_ssid
, const char* new_password
, const char* new_mapApiKey
, const char* new_lat
, const char* new_lon
, const char* new_minT
, const char* new_maxT
, const char* new_pumpStopDelay
, const char* new_tempStop100
, const char* new_tempStop75
, const char* new_tempStop50
, const char* new_tempStop25
, const char* new_rabotaCheckInterval
, const char* new_timerDelayWeather
, const char* new_telega_errors_interval
, const char* new_pin220Use
, const char* new_CHAT_ID
, const char* new_BOTtoken
) {
for (int i = 0; i < 33; ++i) {
EEPROM.write(SSID_ADDR + i, new_ssid[i]);
EEPROM.write(PASS_ADDR + i, new_password[i]);
EEPROM.write(KEY_ADDR + i, new_mapApiKey[i]);
EEPROM.write(LAT_ADDR + i, new_lat[i]);
EEPROM.write(LON_ADDR + i, new_lon[i]);
EEPROM.write(MIN_T_ADDR + i, new_minT[i]);
EEPROM.write(MAX_T_ADDR + i, new_maxT[i]);
EEPROM.write(PUMP_STOP_ADDR + i, new_pumpStopDelay[i]);
EEPROM.write(TEMP_100_ADDR + i, new_tempStop100[i]);
EEPROM.write(TEMP_75_ADDR + i, new_tempStop75[i]);
EEPROM.write(TEMP_50_ADDR + i, new_tempStop50[i]);
EEPROM.write(TEMP_25_ADDR + i, new_tempStop25[i]);
EEPROM.write(RABOTA_INT_ADDR + i, new_rabotaCheckInterval[i]);
EEPROM.write(DELAY_WEATHER_ADDR + i, new_timerDelayWeather[i]);
EEPROM.write(TELEGA_ERRORS_ADDR + i, new_telega_errors_interval[i]);
EEPROM.write(PIN_220V_USE_ADDR + i, new_pin220Use[i]);
EEPROM.write(CHAT_ID_ADDRESS + i, new_CHAT_ID[i]);
}
for (int i = 0; i < 55; ++i) { //Токен пишу отдельно потому как он длиннее
EEPROM.write(TOKEN_ADDRESS + i, new_BOTtoken[i]);
}
EEPROM.commit();
}
/********************************************************** check_220v *********************************************************/
void check_220v(){
if (digitalRead(PIN_220V) && atoi(pin220Use) == 1) {//пропала 220
String error22 = "";
if (error220v == 0) {
error22 += "Відсутня мережа 220В!\nПеревірте!\n\n";
error220v = 1;
bot.sendMessage(CHAT_ID, error22, "");
}
}
if (!digitalRead(PIN_220V) && atoi(pin220Use) == 1) {//появилась 220
String error22 = "";
if (error220v == 1) {
error22 += "З'явилась мережа 220В!\n\n";
error220v = 0;
bot.sendMessage(CHAT_ID, error22, "");
}
}
}
/********************************************************** read_telega *********************************************************/
void read_telega(){
if (millis() > telega_millis + telega_interval){
int numberMess = bot.getUpdates(bot.last_message_received + 1);
while(numberMess) {
Serial.println("got response");
handleNewMessages(numberMess);
numberMess = bot.getUpdates(bot.last_message_received + 1);
}
}
if (helloMesage == 0) {// приветственное сообщение
bot.sendMessage(CHAT_ID, "Систему запущено.\n/status Подивитисть статус.\n/help Подивитисть довідку.\n", "");
helloMesage = 1;
goto metka_start; //проверка ошибок на старте без задержки telega_errors_interval
}
//Обработка ошибок
telega_millis = millis();
if (millis() > telega_errors_millis + atoi(telega_errors_interval)*60000){
metka_start: //проверка ошибок на старте
bool powerLowSignal = digitalRead(PIN_HEATER_LOW);
String error = "";
if (temp1 >= 90 || temp2 >= 90) {
error += "Перегрів!!!\nТемпература: "+ String(temp1) +" °С!\nПеревірте.\n\n";
}
if (temp5 <= 5 && ErrorDt == "") {
error += "Дуже низька температура: "+ String(temp5) +" °С!\nПеревірте.\n\n";
}
if (manualStop == 1) {
error += "Систему зупинено!\nПеревірте і перезапустіть вручну.\n\n";
}
if (powerLowSignal && temp_c >= atof(tempStop25)) {
if (errorWeather !=0) {
error += "Іде нагрів!!!\nТемпература датчика вулиця: "+ String(temp_c) +" °С!\nПеревірте!\n";
error += "Можете зупинити командою\n/stop\n\n";
} else {
error += "Іде нагрів!!!\nСередньодобова температура: "+ String(temp_c) +" °С!\nПеревірте!\n";
if (heating50 == 1) {
error += "Примусовий нагрів 50%\n";
} else if (heating100 == 1) {
error += "Примусовий нагрів 100%\n";
}
error += "Можете зупинити командою\n/stop\n\n";
}
}
if (ErrorDt != "") {
error += "Помилка роботи датчиків температури "+ String(ErrorDt) +"!\nПеревірте.\n\n";
}
if (WiFi.status() == WL_CONNECTED && errorWeather !=0) {
error += "Помилка доступу до OpenWeatherMap!\nПеревірте.\n\n";
}
if (telega_errors_stop != 1 && error != "") {
error += "Отримувати повідомлення рідше надішліть\n/ok\n\n";
}
if (error != "") {
bot.sendMessage(CHAT_ID, error, "");
telega_errors = 1;
}
telega_errors_millis = millis();
}
if (telega_errors == 1 && telega_errors_stop == 1) {//увеличиваем интервал между сообщениями до 30 мин
String sec30 = "30";
sec30.toCharArray(telega_errors_interval, 33);
}
}
/********************************************************** handleNewMessages *********************************************************/
// Обрабатываем полученные сообщения
void handleNewMessages(int numNewMessages) {
bool powerHighSignal = digitalRead(PIN_HEATER_HIGH);
bool powerLowSignal = digitalRead(PIN_HEATER_LOW);
bool keyHighSignal = digitalRead(PIN_POWER);
for (int i=0; i<numNewMessages; i++) {
// Получаем Chat ID
String chat_id = String(bot.messages[i].chat_id);
if (chat_id != CHAT_ID){
bot.sendMessage(chat_id, "Unauthorized user", "");
continue;
}
// Получаемсообщения из Telegram
String text = bot.messages[i].text;
Serial.println(text);
String from_name = bot.messages[i].from_name;
if (text == "/start") {
String welcome = "Привіт, " + from_name + ".\n";
welcome += "Це автоматика котла. Тут можна побачити стан роботи та помилки\n\n";
welcome += "/status Подивитисть статус.\n";
welcome += "/help Подивитисть довідку.\n";
bot.sendMessage(chat_id, welcome, "");
} else if (text == "/stop") {
String stopText = "Систему зупинено!\nПеревірте і перезапустіть вручну або командою /pusk\n";
bot.sendMessage(chat_id, stopText, "");
manualStop = 1;
heating50 = 0;
heating100 = 0;
} else if (text == "/ok") {
String окText = "Повідомлення надходитимуть рідше.";
bot.sendMessage(chat_id, окText, "");
telega_errors_stop = 1;
} else if (text == "/help") {
String helpText = "/status Подивитисть статус.\n";
helpText += "/help Подивитисть довідку.\n";
helpText += "/stop Зупинити систему.\n";
helpText += "/pusk Запуск системи.\n";
helpText += "/heating50 Нагрів 50% об'єму.\n";
helpText += "/heating100 Нагрів 100% об'єму.\n";
helpText += "/ok Надсилати повідомлення рідше.\n";
bot.sendMessage(chat_id, helpText, "");
} else if (text == "/pusk" && manualStop == 1) {
String puskText = "Запускаю!";
bot.sendMessage(chat_id, puskText, "");
manualStop = 0;
heating50 = 0;
heating100 = 0;
//ESP.restart();
} else if (text == "/pusk" && manualStop == 0) {
String puskText = "Система працює.";
bot.sendMessage(chat_id, puskText, "");
} else if (text == "/heating50" && rabotaError != 1) {
String heating50Text = "Запускаю нагрів 50% об'єму!";
bot.sendMessage(chat_id, heating50Text, "");
manualStop = 0;
heating50 = 1;
heating100 = 0;
} else if (text == "/heating100" && rabotaError != 1) {
String heating100Text = "Запускаю нагрів 100% об'єму!";
bot.sendMessage(chat_id, heating100Text, "");
manualStop = 0;
heating100 = 1;
heating50 = 0;
} else if (text == "/status") {
String status = "";
if (powerHighSignal || powerLowSignal) {
status += "ІДЕ НАГРІВ " + String(powerHighSignal ? "MAX потужність.\n" : "MIN потужність.\n") + "";
}
if (heating50 == 1 && rabotaError != 1) {
status += " Примусовий нагрів 50%\n";
} else if (heating100 == 1 && rabotaError != 1) {
status += " Примусовий нагрів 100%\n";
}
if (manualStop == 1) {
status += "Систему зупинено! Перевірте і перезапустіть вручну.\n";
}
if (ErrorDt != "") {
status += "Помилка роботи датчиків температури "+ String(ErrorDt) +"! Перевірте.\n";
}
if (WiFi.status() == WL_CONNECTED && errorWeather !=0) {
status += "Помилка доступу до прогнозу OpenWeatherMap! Перевірте.\n";
}
status += "Датчик 1: "+ String(temp1) +" °С | (верхій 0%)\n";
status += "Датчик 2: "+ String(temp2) +" °С | (25%)\n";
status += "Датчик 3: "+ String(temp3) +" °С | (середній 50%)\n";
status += "Датчик 4: "+ String(temp4) +" °С | (75%)\n";
status += "Датчик 5: "+ String(temp5) +" °С | (нижній 100%)\n";
status += "Датчик 6: "+ String(temp6) +" °С | (повітря вулиця)\n";
if (WiFi.status() != WL_CONNECTED) {
status += "WiFi НЕ підключено! Налаштуйте.\n";
} else {
status += "WiFi підключено до: "+ String(ssid) + "\n";
status += "IP адреса: " + WiFi.localIP().toString() + "\n";
}
if (atoi(pin220Use) == 1) {
status += "Мережа 220В: " + String(digitalRead(PIN_220V) ? "Немає!" : "Подано.") + "\n";
}
if (errorWeather !=0) {
status += "Температура датчика вулиця: "+ String(temp_c) +" °С\n";
} else {
status += "Середньодобова температура: "+ String(temp_c) +" °С\n";
}
status += "Сигнал нічний тариф (HP): " + String(keyHighSignal ? "ВКЛ" : "ВИКЛ") + "\n";
status += "Нагрів High Power (HP): " + String(powerHighSignal ? "ВКЛ" : "ВИКЛ") + "\n";
status += "Сигнал термостата (LP): " + String(thermostatSignal ? "ВКЛ" : "ВИКЛ") + "\n";
status += "Нагрів Low Power (LP): " + String(powerLowSignal ? "ВКЛ" : "ВИКЛ") + "\n";
status += "Насос котла: " + String(digitalRead(PIN_PUMP) ? "ВКЛ" : "ВИКЛ") + "\n";
status += "Насос системи: " + String(digitalRead(PIN_PUMP2) ? "ВИКЛ" : "ВКЛ") + "\n";
status += "Стан вентиля: " + String(digitalRead(PIN_VALVE) ? "Закрито" : "Відкрито") + "\n";
status += "Працездатність: " + String(digitalRead(PIN_RABOTA) ? "ТАК" : "НІ") + "\n";
bot.sendMessage(chat_id, status, "");
} else {
String mesage = "Невідома команда.\nНадішліть /help для довідки.";
bot.sendMessage(chat_id, mesage, "");
}
}
}
/********************************************************** handleCommand *********************************************************/
// Функция для обработки команды из сериал
void handleCommand(String command) {
command.trim(); // удаляем пробелы и символы новой строки с концов строки
// Ищем позицию первого пробела
int spaceIndex = command.indexOf(' ');
// Если пробел найден, разбиваем строку на ключ и значение
if (spaceIndex > 0) {
String key = command.substring(0, spaceIndex); // извлекаем ключ
String value = command.substring(spaceIndex + 1); // извлекаем значение
// Проверяем, что за ключ был передан, и присваиваем значение соответствующей переменной
if (key == "wifissid") {
wifissid = value;
wifissid.toCharArray(ssid, 33); // Копируем строку в массив
Serial.print("WiFi SSID: ");
Serial.println(wifissid);
} else if (key == "wifipass") {
wifipass = value;
wifipass.toCharArray(password, 33); // Копируем строку в массив
Serial.print("WiFi Password: ");
Serial.println(wifipass);
if (wifissid != "" && wifipass !="") {
WiFi.reconnect();
connectToWiFi();
Serial.println("Перепідключення до WiFi");
Serial.println("Не забудьте зберегти налаштування через вебсторінку!");
}
} else {
Serial.print("Невідома команда: ");
Serial.println(key);
}
} else {
Serial.println("Невірний формат команди.");
Serial.println("Введіть SSID в форматі: wifissid <значення>");
Serial.println("Введіть WiFi pass в форматі: wifissid <значення>");
}
}
/********************************************************** writeStringToEEPROM *********************************************************/
// Функция для записи дефолтных значений в EEPROM
void writeStringToEEPROM(int startAddress, const char* str) {
int len = strlen(str);
for (int i = 0; i < len; i++) {
EEPROM.write(startAddress + i, str[i]);
}
EEPROM.write(startAddress + len, '\0'); // Добавляем нулевой символ в конце строки
EEPROM.commit();
}
/* Recode russian fonts from UTF-8 to Windows-1251 */
String utf8rus(String source)
{
int i,k;
String target;
unsigned char n;
char m[2] = { '0', '\0' };
k = source.length(); i = 0;
while (i < k) {
n = source[i]; i++;
if (n >= 0xC0) {
switch (n) {
case 0xD0: {
n = source[i]; i++;
if (n == 0x81) { n = 0xA8; break; }
if (n >= 0x90 && n <= 0xBF) n = n + 0x30;
break;
}
case 0xD1: {
n = source[i]; i++;
if (n == 0x91) { n = 0xB8; break; }
if (n >= 0x80 && n <= 0x8F) n = n + 0x70;
break;
}
}
}
m[0] = n; target = target + String(m);
}
return target;
}
/********************************************************** displayRenew *********************************************************/
void displayRenew() {//вывод данных на дисплей циклически
bool powerHighSignal = digitalRead(PIN_HEATER_HIGH);
bool powerLowSignal = digitalRead(PIN_HEATER_LOW);
display.clearDisplay();
display.setTextSize(1); // Normal 1:1 pixel scale
display.setTextColor(SSD1306_WHITE); // Draw white text
display.setCursor(0,0);
if (displayCount == 0) {
if (WiFi.status() == WL_CONNECTED) {
display.print("IP: ");
display.println(WiFi.localIP().toString());
}else{
display.println(utf8rus("WiFi не пiдключено"));
}
if (atoi(pin220Use) == 1) {
display.print(utf8rus("Мережа 220V: "));
display.println(utf8rus(String(digitalRead(PIN_220V) ? "Нема!" : "Подано")));
}
if (temp1 >= 90 || temp2 >= 90) {//проверка перегрева
display.print(utf8rus("Перегрiв! Temp: "));
display.println(utf8rus(String(temp1) +" С!"));
}
if (temp5 <= 5 && ErrorDt == "") {//проверка переохлаждения
display.print(utf8rus("Низька Temp: "));
display.println(utf8rus(String(temp5) +" С!"));
}
if (manualStop == 1) {//если остановлено вручную кнопкой html или через бот
display.println(utf8rus("Систему зупинено!"));
}
if (powerLowSignal && temp_c >= atof(tempStop25)) {//если идет нагрев и температура прогноз/улица высока (термостат, принудительный нагрев)
display.print(utf8rus("Нагрiв! Temp: "));
display.println(utf8rus(String(temp_c)));
}
if (ErrorDt != "") {//если ошибки датчиков температуры
display.print(utf8rus("Помилка DT: "));
display.println(utf8rus(String(ErrorDt)));
}
if (WiFi.status() == WL_CONNECTED && errorWeather !=0) {//если есть вайфай но нет прогноза
display.println(utf8rus("Нема прогонозу!"));
}
if (heating50 == 1 && rabotaError != 1) {//если принудит накрев 50%
display.print(utf8rus("Примусовий 50%"));
} else if (heating100 == 1 && rabotaError != 1) {//если принудит накрев 100%
display.print(utf8rus("Примусовий 100%"));
}
}
if (displayCount == 1) {
if (powerHighSignal || powerLowSignal) {//проверка нагрева
display.print(utf8rus("НАГРIВ "));
display.println(String(powerHighSignal ? "MAX Power" : "MIN Power"));
}
if (errorWeather !=0) {
display.println(utf8rus("Вулиця: " + String(temp_c) + " C"));
} else {
display.println(utf8rus("Прогноз: " + String(temp_c) + " C"));
}
display.println(utf8rus("Датчик 1: " + String(temp1) + " C"));
display.println(utf8rus("Датчик 2: " + String(temp2) + " C"));
display.println(utf8rus("Датчик 3: " + String(temp3) + " C"));
display.println(utf8rus("Датчик 4: " + String(temp4) + " C"));
display.println(utf8rus("Датчик 5: " + String(temp5) + " C"));
display.println(utf8rus("Датчик 6: " + String(temp6) + " C"));
}
if (displayCount == 2) {
display.print(utf8rus("Нiч: "));
display.println(utf8rus(String(digitalRead(PIN_POWER) ? "ВКЛ" : "ВИКЛ")));
display.print(utf8rus("Нагрiв HP: "));
display.println(utf8rus(String(powerHighSignal ? "ВКЛ" : "ВИКЛ")));
display.print(utf8rus("Tермостат: "));
display.println(utf8rus(String(thermostatSignal ? "ВКЛ" : "ВИКЛ")));
display.print(utf8rus("Нагрiв LP: "));
display.println(utf8rus(String(powerLowSignal ? "ВКЛ" : "ВИКЛ")));
display.print(utf8rus("Насос котла: "));
display.println(utf8rus(String(digitalRead(PIN_PUMP) ? "ВКЛ" : "ВИКЛ")));
display.print(utf8rus("Насос системи: "));
display.println(utf8rus(String(digitalRead(PIN_PUMP2) ? "ВИКЛ" : "ВКЛ")));
display.print(utf8rus("Вентиль: "));
display.println(utf8rus(String(digitalRead(PIN_VALVE) ? "Закрито" : "Вiдкрито")));
display.print(utf8rus("Працездатнiсть: "));
display.println(utf8rus(String(digitalRead(PIN_RABOTA) ? "YES" : "NO")));
}
displayCount ++;
if (displayCount == 3) displayCount = 0;
display.display();
}