Автоматическая система сброса теплой воды для фильтров обратного осмоса

Друзья, в одной из моих квартир недавно возникла проблема с температурой холодной воды, и с тех пор она не была решена. Возможно, в системе центрального водоснабжения моего дома горячая вода где-то подмешивается в трубу с холодной водой, и из-за этого из крана холодной воды поступает вода с температурой 36–39 градусов.
Текущие показания температуры (данные обновляются раз в 5 секунд):

Такая повышенная температура из трубы холодной воды негативно сказывается на работе моей системы очистки воды методом обратного осмоса (ОС), а также на работу моего устройства ионизации воды, так как оно подключено после трех префильтров ОС. По паспорту, в систему ОС можно подавать воду до 38 градусов, а в устройство ионизации воды — до 30 градусов Цельсия. На практике рекомендуется использовать воду с температурой 4–30 °C для оптимальной работы системы ОС. Также температура воды в 25-35 °C может способствовать активному росту бактерий в накопительном баке системы обратного осмоса, но это решается установкой UV-фильтра.
После слива такой воды из крана в течение 5 минут вода становится около 27 градусов Цельсия. Было решено сделать автоматическую систему сброса теплой воды на базе ESP32 и Home Assistant.
Что вам понадобится
- Бокс для блока управления – 1 шт.
- Бокс для блока манифольда – 1 шт.
- Микроконтроллер ESP32 – 1 шт.
- Блок питания 220VAC / 5VDC, 10Вт – 1 шт.
- Блок питания 220VAC / 24VDC, 72Вт – 1 шт.
- Электрический шаровый кран (1/2” 24VDC Normally Opened) – 1 шт.
- Электрический шаровый кран (1/2” 24VDC Normally Closed) – 1 шт.
- Фитинги для шарового крана (1/4” Hose — 1/2” BSP) – 4 шт.
- Трубки 1/4”
- Расходомер, 5VDC – 1 шт.
- Цифровой температурный сенсор DS18B20 – 1 шт.
- Стальной патрубок 50 мм 1/4” BSP – 1 шт.
- Термоусадка для стального патрубка d18 мм — 50 мм
- Термоизоляция для стального патрубка внутр. диам. 16 мм — 50 мм
- Фиттинг для стального патрубка (1/4” Hose — 1/4” BSP Female) – 2 шт.
- Угловой фиттинг (1/4” quick jack) – 6 шт.
- Т-фиттинг (1/4” quick jack) – 2 шт.
- Соединительный фиттинг (1/4” quick jack) – 3 шт.
- Коннектор GX12 2P под корпус (вилка + розетка) – 8 шт.
- Коннектор GX12 2P для провода (вилка + розетка) – 1 шт.
- Коннектор GX12 3P под корпус (вилка + розетка) – 4 шт.
- Резистор 4.7 кОм, 1/4 Вт – 1 шт.
- Реле SRD-05VDC-SL-C – 3 шт.
- Диод 1N4001 – 3 шт.
- Транзистор 2N3904 – 3 шт.
- Резистор 1 кОм, 1/4 Вт – 3 шт.
- Красный LED 5VDC – 2 шт.
- Синий LED 5VDC – 1 шт.
- Резистор 240 Ом, 1/4 Вт – 3 шт.
- Красный LED 24VDC – 1 шт.
- Синий LED 24VDC – 2 шт.
- Резистор 2.4 кОм, 1/4 Вт – 3 шт.
Шаг 1 : Изготавливаем температурный датчик

Температурный сенсор DS18B20
Возьмите температурный сенсор и припаяйте к нему трехжильный сигнальный провод. Не забудьте про маркировку контактов, чтобы знать, где Data, Ground и VDC. Закрепите сенсор на металлическом патрубке с помощью термоусадки. Сам патрубок соедините с двумя фитингами 1/4BSP – 1/4 Hose. Затем поместите патрубок в термоизоляцию.

Температурный сенсор прикрепленный к патрубку 1/4 BSP с помощью термоусадки
А вот и готовые термодатчики (дополнительный термодатчик я сделал для другого проекта):

Термодатчики
Шаг 2 : Собираем блок манифольда

Блок манифольда
Боксы для электрических шаровых кранов у всех будут свои, поэтому количество соединительных фитингов будет разным. В моем случае я купил контейнер для хранения с крышкой, просверлил в нем три отверстия под трубки 1/4”, а также четыре отверстия для вилки разъема GX12 на корпус. В этот бокс я поместил температурный датчик, два электрических шаровых крана, расходомер и соединил все фитингами для систем обратного осмоса. Провода электрических шаровых кранов, термодатчика и расходомера припаял к соответствующим выходам вилок разъемов GX12.

Шаг 3 : Пайка блока управления

Блок управления
Здесь также все предельно просто. В боксе просверлите соответствующие отверстия под GX12, светодиоды и кабель питания 220VAC. Соберите компоненты согласно схеме. Полноразмерную схему скачайте отсюда.


Задняя сторона блока управления
Шаг 4 : Заливаем программу в ESP32
Подсоедините ESP32 к ноутбуку, откройте Arduino IDE и загрузите следующий скетч:
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <mqtt_client.h>
#include "esp_heap_caps.h"
#if __has_include(<esp_arduino_version.h>)
#include <esp_arduino_version.h>
#endif
// ================== КОНФИГУРАЦИЯ ==================
// Параметры WiFi
#define WIFI_SSID "HomeNetwork"
#define WIFI_PASSWORD "SecurePass2024"
IPAddress staticIP(192, 168, 1, 100);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
// Параметры MQTT брокера
#define MQTT_HOST "192.168.1.50"
#define MQTT_PORT 1883
#define MQTT_USER "device_user"
#define MQTT_PASSWORD "mqtt_secure_pass"
// MQTT топики
#define MQTT_TOPIC_WATER_TEMPERATURE "homeassistant/ro_system/water_temperature"
#define MQTT_TOPIC_RO_SOLENOID "homeassistant/ro_system/ro_solenoid"
#define MQTT_TOPIC_DRAINAGE_SOLENOID "homeassistant/ro_system/drainage_solenoid"
#define MQTT_TOPIC_RO_PUMP "homeassistant/ro_system/ro_pump"
#define MQTT_TOPIC_DUMPED_WATER_INTERVAL_VOLUME "homeassistant/ro_system/dumped_water_interval_volume"
#define MQTT_TOPIC_SYSTEM_STATE "homeassistant/ro_system/system_state"
#define MQTT_TOPIC_DUMP_TIME "homeassistant/ro_system/dump_time"
#define MQTT_TOPIC_DUMP_TEMP_THRESHOLD "homeassistant/ro_system/dump_temp_threshold"
// ================== GPIO ==================
#define RO_SOLENOID_PIN 26 // Клапан подачи
#define DRAINAGE_SOLENOID_PIN 25 // Клапан слива
#define ONE_WIRE_BUS 27 // Датчик температуры DS18B20
#define FLOW_METER_PIN 33 // Счётчик расхода
#define RO_BLUE_LED 15 // Синий LED
#define DRAINAGE_RED_LED 2 // Красный LED
#define DUMP_TIMEOUT_RED_LED 4 // LED ошибки
#define RO_PUMP_PIN 18 // Насос подкачки
// ================== ПЕРЕЧИСЛЕНИЯ СОСТОЯНИЙ ==================
// Состояние команды насоса (дебаунс)
enum PumpCommandState {
PUMP_CMD_IDLE=0,
PUMP_CMD_PENDING=1,
PUMP_CMD_CONFIRMED=2
};
// Состояние системы
enum SystemState {
STATE_NORMAL=0, // Нормальная работа
STATE_DUMP_IN_PROGRESS=1, // Идёт слив тёплой воды
STATE_PUMP_DELAYED_START=2,// Задержка перед перезапуском насоса
STATE_DUMP_TIMEOUT=3 // Ошибка: превышено время слива
};
// ================== ДАТЧИКИ И MQTT ==================
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
static esp_mqtt_client_handle_t mqtt_client = NULL;
static char mqtt_publish_buf[128];
static char mqtt_topic_buf[128];
static char mqtt_data_buf[64];
// ================== СОСТОЯНИЕ СИСТЕМЫ ==================
SystemState system_state = STATE_NORMAL;
PumpCommandState pump_cmd_state = PUMP_CMD_IDLE;
bool ro_solenoid_open = true;
bool drainage_solenoid_open = false;
bool ro_pump_enabled = true;
bool mqtt_control_allowed = true;
// ================== ПАРАМЕТРЫ ==================
float water_temperature = 0.0;
long dump_time_ms = 600000; // Время слива (мс)
float dump_temp_threshold = 29.0; // Порог температуры для слива (°C)
float dump_temp_hysteresis = 2.0; // Гистерезис (°C)
const long pump_restart_delay = 15000; // Задержка перезапуска насоса (мс)
bool pump_cmd_pending_value = false;
unsigned long pump_cmd_timestamp = 0;
// ================== ТАЙМЕРЫ ==================
unsigned long last_sensor_update = 0;
unsigned long last_status_update = 0;
unsigned long dump_start_time = 0;
unsigned long pump_restart_time = 0;
unsigned long last_heap_check = 0;
const long sensor_interval = 5000;
const long STATUS_UPDATE_INTERVAL = 5000;
const long led_update_interval = 100;
// ================== СЧЁТЧИК РАСХОДА ==================
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
volatile uint32_t pulseCount = 0;
volatile bool wifiReconnectNeeded = false;
volatile bool mqttReconnectNeeded = false;
const float FLOW_COEFFICIENT = 0.1622168797; // Импульсы -> литры
// ================== СТРОКИ СТАТУСА ==================
const char* solenoid_open_name = "Открыто";
const char* solenoid_closed_name = "Закрыто";
const char* status_normal = "Подача воды в обратный осмос";
const char* status_dump = "Сброс тёплой воды";
const char* status_dump_timeout = "Превышение времени сброса";
const char* deviceName = "ESP32RO";
// ================== ЛОГИРОВАНИЕ И MQTT ==================
// Логирование с меткой времени и категорией
void logInfoF(const char* category, const char* format, ...) {
unsigned long timestamp = millis();
char buf[128];
va_list args;
va_start(args, format);
vsnprintf(buf, sizeof(buf), format, args);
va_end(args);
Serial.printf("[%lu] [%s] %sn", timestamp, category, buf);
}
// Публикация в MQTT (QoS=1, без retain)
void publishMqtt(const char* topic, const char* payload) {
if (mqtt_client) {
esp_mqtt_client_publish(mqtt_client, topic, payload, 0, 1, 0);
}
}
// ================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ПУБЛИКАЦИИ ==================
void publishRoSolenoidStatus() {
publishMqtt(MQTT_TOPIC_RO_SOLENOID, ro_solenoid_open ? solenoid_open_name : solenoid_closed_name);
}
void publishDrainageSolenoidStatus() {
publishMqtt(MQTT_TOPIC_DRAINAGE_SOLENOID, drainage_solenoid_open ? solenoid_open_name : solenoid_closed_name);
}
void publishPumpStatus() {
publishMqtt(MQTT_TOPIC_RO_PUMP, ro_pump_enabled ? "1" : "0");
}
void publishSystemState() {
const char* state_str;
switch (system_state) {
case STATE_DUMP_IN_PROGRESS:
state_str = status_dump;
break;
case STATE_DUMP_TIMEOUT:
state_str = status_dump_timeout;
break;
case STATE_PUMP_DELAYED_START:
case STATE_NORMAL:
default:
state_str = status_normal;
break;
}
publishMqtt(MQTT_TOPIC_SYSTEM_STATE, state_str);
}
// ================== УПРАВЛЕНИЕ КОМПОНЕНТАМИ ==================
// Управление клапаном подачи (LOW открывает, HIGH закрывает)
void setRoSolenoid(bool open) {
if (ro_solenoid_open != open) {
ro_solenoid_open = open;
digitalWrite(RO_SOLENOID_PIN, open ? LOW : HIGH);
logInfoF("RO_SOL", "Changed to: %s", open ? "OPEN" : "CLOSED");
publishRoSolenoidStatus();
}
}
// Управление клапаном слива (HIGH открывает, LOW закрывает)
void setDrainageSolenoid(bool open) {
if (drainage_solenoid_open != open) {
drainage_solenoid_open = open;
digitalWrite(DRAINAGE_SOLENOID_PIN, open ? HIGH : LOW);
logInfoF("DRAIN_SOL", "Changed to: %s", open ? "OPEN" : "CLOSED");
publishDrainageSolenoidStatus();
}
}
// Управление насосом (LOW включает, HIGH выключает)
// Блокирует включение во время слива и охлаждения
void setPumpEnabled(bool enabled) {
if (enabled && (system_state == STATE_DUMP_IN_PROGRESS || system_state == STATE_PUMP_DELAYED_START)) {
logInfoF("PUMP", "Block: System State %d", system_state);
return;
}
if (ro_pump_enabled != enabled) {
ro_pump_enabled = enabled;
digitalWrite(RO_PUMP_PIN, enabled ? LOW : HIGH);
logInfoF("PUMP", "Changed to: %s", enabled ? "ON" : "OFF");
publishPumpStatus();
}
}
// ================== ЛОГИКА СИСТЕМЫ ==================
// Основная логика управления системой:
// 1. Обнаружение перегрева -> запуск слива
// 2. Обнаружение охлаждения -> задержка перед перезапуском
// 3. Таймаут слива -> ошибка
// 4. Завершение задержки -> возврат в норму
void handleSystemLogic() {
unsigned long now = millis();
// ЭТАП 1: ПЕРЕГРЕВ - ЗАПУСК СЛИВА
if (system_state == STATE_NORMAL && water_temperature > dump_temp_threshold) {
logInfoF("SYSTEM", "Temp too high (%.1f), starting DUMP", water_temperature);
system_state = STATE_DUMP_IN_PROGRESS;
dump_start_time = now;
setRoSolenoid(false); // Закрыть подачу
setDrainageSolenoid(true); // Открыть слив
setPumpEnabled(false); // Отключить насос
mqtt_control_allowed = false;
publishSystemState();
}
// ЭТАП 2: ОХЛАЖДЕНИЕ - ЗАДЕРЖКА ПЕРЕД ПЕРЕЗАПУСКОМ
if (system_state == STATE_DUMP_IN_PROGRESS && water_temperature < (dump_temp_threshold - dump_temp_hysteresis)) {
logInfoF("SYSTEM", "Temp normal (%.1f), waiting...", water_temperature);
system_state = STATE_PUMP_DELAYED_START;
pump_restart_time = now;
setRoSolenoid(true); // Открыть подачу
setDrainageSolenoid(false); // Закрыть слив
mqtt_control_allowed = false;
publishSystemState();
}
// ЭТАП 3: ТАЙМАУТ СЛИВА - ОШИБКА
if (system_state == STATE_DUMP_IN_PROGRESS && (now - dump_start_time) >= dump_time_ms) {
logInfoF("SYSTEM", "DUMP TIMEOUT! Error!");
system_state = STATE_DUMP_TIMEOUT;
setRoSolenoid(false);
setDrainageSolenoid(false);
setPumpEnabled(false);
mqtt_control_allowed = false;
publishSystemState();
}
// ЭТАП 4: ЗАДЕРЖКА ЗАВЕРШЕНА - ВОЗВРАТ К НОРМЕ
if (system_state == STATE_PUMP_DELAYED_START && (now - pump_restart_time) >= pump_restart_delay) {
logInfoF("SYSTEM", "Back to Normal.");
system_state = STATE_NORMAL;
setPumpEnabled(true);
mqtt_control_allowed = true;
publishSystemState();
}
}
// Обработка команд управления насосом с дебаунсом (300мс)
void handlePumpCommand() {
unsigned long now = millis();
switch(pump_cmd_state) {
case PUMP_CMD_IDLE:
break;
case PUMP_CMD_PENDING:
// Ждём дебаунс
if (now - pump_cmd_timestamp >= 300)
pump_cmd_state = PUMP_CMD_CONFIRMED;
break;
case PUMP_CMD_CONFIRMED:
// Выполняем команду если позволяет состояние
if (mqtt_control_allowed && system_state != STATE_DUMP_TIMEOUT) {
setPumpEnabled(pump_cmd_pending_value);
}
pump_cmd_state = PUMP_CMD_IDLE;
break;
}
}
// ================== ДАТЧИКИ И HEARTBEAT ==================
// Прерывание от счётчика расхода
void IRAM_ATTR pulseCounter() {
portENTER_CRITICAL_ISR(&mux);
pulseCount++;
portEXIT_CRITICAL_ISR(&mux);
}
// Опрос датчиков (температура и расход)
void handleSensorUpdate() {
if (millis() - last_sensor_update < sensor_interval) return;
last_sensor_update = millis();
sensors.requestTemperatures();
float t = sensors.getTempCByIndex(0);
if (t > -50 && t < 100) water_temperature = t;
// Считываем счётчик расхода
uint32_t pulses;
portENTER_CRITICAL(&mux);
pulses = pulseCount;
pulseCount = 0;
portEXIT_CRITICAL(&mux);
float intervalVolume = pulses * FLOW_COEFFICIENT;
snprintf(mqtt_publish_buf, sizeof(mqtt_publish_buf), "%.2f", water_temperature);
publishMqtt(MQTT_TOPIC_WATER_TEMPERATURE, mqtt_publish_buf);
snprintf(mqtt_publish_buf, sizeof(mqtt_publish_buf), "%.2f", intervalVolume);
publishMqtt(MQTT_TOPIC_DUMPED_WATER_INTERVAL_VOLUME, mqtt_publish_buf);
}
// Периодическое обновление статуса в Home Assistant
void publishStatusHeartbeat() {
if (millis() - last_status_update < STATUS_UPDATE_INTERVAL) return;
last_status_update = millis();
if (mqtt_client) {
publishRoSolenoidStatus();
publishDrainageSolenoidStatus();
publishPumpStatus();
publishSystemState();
snprintf(mqtt_publish_buf, sizeof(mqtt_publish_buf), "%ld", dump_time_ms / 60000);
publishMqtt(MQTT_TOPIC_DUMP_TIME, mqtt_publish_buf);
snprintf(mqtt_publish_buf, sizeof(mqtt_publish_buf), "%.1f", dump_temp_threshold);
publishMqtt(MQTT_TOPIC_DUMP_TEMP_THRESHOLD, mqtt_publish_buf);
}
}
// ================== MQTT ОБРАБОТЧИК ==================
// Обработчик MQTT событий
static void mqtt_event_handler(void* handler_args, esp_event_base_t base, int32_t event_id, void* event_data) {
esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t) event_data;
switch((esp_mqtt_event_id_t)event_id) {
case MQTT_EVENT_CONNECTED:
logInfoF("MQTT","Connected");
// Подписываемся на управляющие топики
esp_mqtt_client_subscribe(mqtt_client, MQTT_TOPIC_RO_PUMP, 0);
esp_mqtt_client_subscribe(mqtt_client, MQTT_TOPIC_DUMP_TIME, 0);
esp_mqtt_client_subscribe(mqtt_client, MQTT_TOPIC_DUMP_TEMP_THRESHOLD, 0);
// Отправляем полный статус при подключении
publishRoSolenoidStatus();
publishDrainageSolenoidStatus();
publishPumpStatus();
publishSystemState();
snprintf(mqtt_publish_buf, sizeof(mqtt_publish_buf), "%.2f", water_temperature);
publishMqtt(MQTT_TOPIC_WATER_TEMPERATURE, mqtt_publish_buf);
break;
case MQTT_EVENT_DATA: {
if (event->topic_len >= sizeof(mqtt_topic_buf) || event->data_len >= sizeof(mqtt_data_buf)) break;
memcpy(mqtt_topic_buf, event->topic, event->topic_len);
mqtt_topic_buf[event->topic_len] = 0;
memcpy(mqtt_data_buf, event->data, event->data_len);
mqtt_data_buf[event->data_len] = 0;
// Команда включения/выключения насоса
if (strncmp(mqtt_topic_buf, MQTT_TOPIC_RO_PUMP, event->topic_len) == 0) {
bool new_state = (atoi(mqtt_data_buf) != 0);
pump_cmd_pending_value = new_state;
pump_cmd_state = PUMP_CMD_PENDING;
pump_cmd_timestamp = millis();
}
// Изменение порога температуры слива
else if (strncmp(mqtt_topic_buf, MQTT_TOPIC_DUMP_TEMP_THRESHOLD, event->topic_len) == 0) {
dump_temp_threshold = atof(mqtt_data_buf);
logInfoF("CONF", "New Temp: %.1f", dump_temp_threshold);
}
// Изменение времени слива (в минутах)
else if (strncmp(mqtt_topic_buf, MQTT_TOPIC_DUMP_TIME, event->topic_len) == 0) {
dump_time_ms = atol(mqtt_data_buf) * 60000;
logInfoF("CONF", "New Time: %ld ms", dump_time_ms);
}
} break;
default:
break;
}
}
// Инициализация MQTT клиента
void mqtt_setup() {
if (mqtt_client) {
esp_mqtt_client_stop(mqtt_client);
esp_mqtt_client_destroy(mqtt_client);
mqtt_client = NULL;
}
esp_mqtt_client_config_t mqtt_cfg = {};
#if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR >= 3
mqtt_cfg.broker.address.hostname = MQTT_HOST;
mqtt_cfg.broker.address.port = MQTT_PORT;
mqtt_cfg.credentials.username = MQTT_USER;
mqtt_cfg.credentials.authentication.password = MQTT_PASSWORD;
#else
mqtt_cfg.host = MQTT_HOST;
mqtt_cfg.port = MQTT_PORT;
mqtt_cfg.username = MQTT_USER;
mqtt_cfg.password = MQTT_PASSWORD;
#endif
mqtt_client = esp_mqtt_client_init(&mqtt_cfg);
if (mqtt_client) {
esp_mqtt_client_register_event(mqtt_client, MQTT_EVENT_ANY, mqtt_event_handler, NULL);
esp_mqtt_client_start(mqtt_client);
}
}
// ================== ОСНОВНАЯ ПРОГРАММА ==================
void setup() {
Serial.begin(115200);
// Инициализация GPIO
pinMode(FLOW_METER_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(FLOW_METER_PIN), pulseCounter, FALLING);
pinMode(RO_SOLENOID_PIN, OUTPUT);
pinMode(DRAINAGE_SOLENOID_PIN, OUTPUT);
pinMode(RO_PUMP_PIN, OUTPUT);
pinMode(RO_BLUE_LED, OUTPUT);
pinMode(DRAINAGE_RED_LED, OUTPUT);
pinMode(DUMP_TIMEOUT_RED_LED, OUTPUT);
// Инициализация в начальное состояние
setRoSolenoid(true);
setDrainageSolenoid(false);
setPumpEnabled(true);
// Инициализация датчика температуры
sensors.begin();
logInfoF("SETUP", "Reading initial temp...");
sensors.requestTemperatures();
float t = sensors.getTempCByIndex(0);
if (t > -50 && t < 100) {
water_temperature = t;
logInfoF("SETUP", "Initial Temp: %.2f", water_temperature);
}
// Инициализация WiFi
WiFi.onEvent([](WiFiEvent_t e){
if(e==SYSTEM_EVENT_STA_GOT_IP) mqttReconnectNeeded=true;
if(e==SYSTEM_EVENT_STA_DISCONNECTED) wifiReconnectNeeded=true;
});
WiFi.mode(WIFI_STA);
WiFi.hostname(deviceName);
WiFi.config(staticIP, gateway, subnet);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}
// Управление светодиодами
digitalWrite(RO_BLUE_LED, (system_state==STATE_NORMAL || system_state==STATE_PUMP_DELAYED_START));
digitalWrite(DRAINAGE_RED_LED, (system_state==STATE_NORMAL || system_state==STATE_PUMP_DELAYED_START || system_state==STATE_DUMP_TIMEOUT));
digitalWrite(DUMP_TIMEOUT_RED_LED, (system_state==STATE_DUMP_TIMEOUT));
}
// Главный цикл
void loop() {
// Переподключение к WiFi
if (wifiReconnectNeeded) {
wifiReconnectNeeded=false;
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
}
// Переподключение к MQTT
if (mqttReconnectNeeded && WiFi.isConnected()) {
mqttReconnectNeeded=false;
mqtt_setup();
}
// Основная логика
handleSensorUpdate();
handleSystemLogic();
handlePumpCommand();
updateLEDs();
publishStatusHeartbeat();
// Диагностика каждые 30 секунд
if (millis() - last_heap_check > 30000) {
last_heap_check = millis();
Serial.printf("[HEAP] Free: %u KB | Temp: %.2f | State: %dn",
esp_get_free_heap_size()/1024, water_temperature, system_state);
}
}
Все указанные библиотеки в скетче должны быть установлены заранее. Обратите внимание, что в формуле intervalVolume = pulseCount * 0.1622168797, коэффициент 0.1622168797 означает количество миллилитров за каждый оборот турбинки расходомера.
Шаг 5 : Настраиваем Home Assistant

Связь между ESP32 и Home Assistant (HA) осуществляется через MQTT-брокер. В HA создайте следующую карточку:
type: entities
entities:
- entity: sensor.water_temperature
secondary_info: last-changed
name: Температура воды
icon: mdi:water
- entity: sensor.ro_system_state
secondary_info: last-changed
name: Статус системы
icon: mdi:water-alert
- entity: sensor.ro_solenoid_status
secondary_info: last-changed
name: Соленоид осмоса
icon: mdi:pipe-valve
- entity: sensor.drainage_solenoid_status
secondary_info: last-changed
name: Соленоид сброса
icon: mdi:pipe-valve
- entity: switch.ro_pump
secondary_info: last-changed
name: Насос
icon: mdi:pump
- entity: input_number.dump_temp_threshold
secondary_info: last-changed
name: Порог температуры
- entity: input_number.dump_time
name: Макс. время сброса
secondary_info: last-changed
- entity: sensor.water_dump_counts_for_today
secondary_info: last-changed
name: Количество сбросов за сегодня
- entity: sensor.average_dump_time_today
secondary_info: last-changed
name: Среднее время сброса
icon: mdi:timer
show_header_toggle: false
title: Обратный осмос
state_color: true
В configuration.yaml добавьте следующие строки:
sensor:
- platform: template
sensors:
average_dump_time_today:
unit_of_measurement: 'мин.'
friendly_name: "Average dump time today"
value_template: "{{ ((states('sensor.water_dumps_total_time_for_today')|float / states('sensor.water_dump_counts_for_today')|float) * 60)|round(1) }}"
- platform: history_stats
name: Water dump counts for today
entity_id: sensor.ro_system_state
state: "Сброс тёплой воды"
type: count
start: "{{ now().replace(hour=0, minute=0, second=0) }}"
end: "{{ now() }}"
- platform: history_stats
name: Water dumps total time for today
entity_id: sensor.ro_system_state
state: "Сброс тёплой воды"
type: time
start: "{{ now().replace(hour=0, minute=0, second=0) }}"
end: "{{ now() }}"
input_number:
dump_time:
name: Dump time
mode: slider
unit_of_measurement: "мин."
icon: mdi:timer
min: 10
max: 20
step: 1
dump_temp_threshold:
name: Dump temp threshold
mode: slider
unit_of_measurement: "°C"
icon: mdi:thermometer
min: 22
max: 31
step: 0.1
В automations.yaml добавьте следующее:
- id: 'XXXXXXXXXXXX1'
alias: Отправить значение максимального времени сброса воды через MQTT
description: ''
trigger:
- platform: state
entity_id: input_number.dump_time
condition: []
action:
- data:
topic: homeassistant/ro_system/dump_time
payload: '{{ states(''input_number.dump_time'') }}'
retain: true
action: mqtt.publish
mode: single
- id: 'XXXXXXXXXXXX2'
alias: Отправить значение порога температуры воды через MQTT
description: ''
trigger:
- platform: state
entity_id: input_number.dump_temp_threshold
condition: []
action:
- data:
topic: homeassistant/ro_system/dump_temp_threshold
payload: '{{ states(''input_number.dump_temp_threshold'') }}'
retain: true
action: mqtt.publish
mode: single
- id: 'XXXXXXXXXXXX3'
alias: Установить значение максимального времени сброса воды из MQTT на слайдере
description: ''
trigger:
- platform: mqtt
topic: homeassistant/ro_system/dump_time
condition: []
action:
service: input_number.set_value
data_template:
entity_id: input_number.dump_time
value: '{{ trigger.payload }}'
mode: single
- id: 'XXXXXXXXXXXX4'
alias: Установить значение порога температуры воды из MQTT на слайдере
description: ''
trigger:
- platform: mqtt
topic: homeassistant/ro_system/dump_temp_threshold
condition: []
action:
- service: input_number.set_value
data_template:
entity_id: input_number.dump_temp_threshold
value: '{{ trigger.payload }}'
mode: single
Шаг 6 : Подключаем систему обратного осмоса
Подключите трубку подачи воды от холодной воды к входу блока манифольда. Выход от электрического шарового крана (НО) подключите к входу обратного осмоса, а выход от электрического шарового крана (НЗ) подключите к трубке дренажа через Т-фиттинг. Затем соедините блок манифольда с блоком управления и протестируйте систему. В моем случае в системе обратного осмоса присутствует насос подкачки, и он также подключен к блоку управления. Если термодатчик зафиксирует превышение температуры поступающей воды, то подача воды на обратный осмос закроется, и параллельно начнется сброс воды в дренаж. Также блок управления отключит насос подкачки обратного осмоса. После падения температуры воды произойдет обратный процесс, и насос подкачки включится через 15 секунд после начала закрытия электрических шаровых кранов. Если по какой-то причине температура не снизится ниже 2 градусов от заданной пороговой температуры на слайдере и в течение заданного времени сброса в Home Assistant, то сброс автоматически закроется, и на корпусе блока управления загорится красный светодиод ошибки.
П.С.
Profit :)







