Практика: Сценарий управления отоплением с гистерезисом
Введение: Постановка задачи для сценария отопления
ведение: Постановка задачи для сценария отопления
Целью данного урока является разработка полнофункционального и, что наиболее важно, стабильного сценария управления системой отопления на базе контроллера HI. Мы перейдем от теоретических концепций, изученных в предыдущих уроках этого модуля, к созданию реального инженерного решения, готового к внедрению на объекте.
Как мы детально разбирали в первом уроке данного модуля, основная проблема при автоматизации климата — это флаппинг (дребезг) исполнительных устройств. Примитивная логика «включить, если ниже уставки, и выключить, если выше» неизбежно приводит к постоянным переключениям реле из-за микроколебаний температуры возле целевого значения. Последствия вам уже знакомы: быстрый износ оборудования, неэффективный расход энергии и постоянные раздражающие щелчки в электрощите.
Решением проблемы дребезга является внедрение гистерезиса. Мы откажемся от одной жесткой точки уставки и создадим программируемую «мертвую зону». Вводя два разных порога (нижний для включения обогревателя и верхний для выключения), мы гарантируем, что система после включения отработает полный цикл подогрева помещения и не станет дергать контактор из-за падения температуры на доли градуса после выключения.
В данном уроке мы спроектируем и соберем такой сценарий в Node-RED. Основной фокус будет направлен на то, чтобы сделать логику надежной, отказоустойчивой и готовой к повседневной эксплуатации.
📋 Ключевые понятия:
- Флаппинг (дребезг): Нежелательные, частые и быстрые переключения состояния системы оборудования из-за малой разницы между текущим измеряемым значением и уставкой.
- Гистерезис: Свойство системы, при котором реакция на входной сигнал зависит от ее предыдущего состояния. В программной логике — внедрение промежуточной «мертвой зоны» с разными порогами на включение и выключение.
- Уставка (Setpoint): Базовое целевое значение регулируемого параметра, относительно которого будут рассчитываться пороги гистерезиса.
Проектирование логики: Уставка, Гистерезис и Пороги
роектирование логики: Уставка, Гистерезис и Пороги
Перед тем как приступить к сборке потока в Node-RED, необходимо четко спроектировать его логику. Правильное проектирование — залог простоты реализации и дальнейшей поддержки сценария.
Определение ключевых переменных
Для нашего сценария потребуются три основные переменные:
* Пример: `22.0` (°C)
* Пример: `1.0` (°C)
Расчет порогов срабатывания
На основе уставки и гистерезиса мы вычисляем два порога, которые и будут управлять логикой:
- Нижний порог (Lower_Threshold): Температура, при падении ниже которой система должна включить нагрев.
* Пример: `22.0 - 1.0 = 21.0` °C
- Верхний порог (Upper_Threshold): Температура, при достижении которой система должна выключить нагрев. В классической схеме для отопления верхний порог равен уставке.
* Пример: `22.0` °C
> 💡 Подсказка: Для удобства в реальных проектах уставку (Setpoint) и гистерезис (Hysteresis) лучше выносить в глобальные переменные, персистентный контекст или в отдельные MQTT-топики. Это позволит менять их "на лету" через панель управления (Dashboard) или мобильное приложение, не редактируя и не перезапуская сам сценарий Node-RED.
Описание логики работы и управление состоянием
Теперь опишем полный алгоритм работы системы. Ключевой элемент здесь — состояние (State), которое показывает, включен (`true`) или выключен (`false`) нагреватель в данный момент. Мы должны хранить это состояние, чтобы избежать отправки лишних дублирующих команд на реле.
Логика работы при поступлении нового значения температуры:
* ЕСЛИ `Temperature < Lower_Threshold` (например, 20.9 < 21.0)
* И ЕСЛИ состояние системы сейчас `ВЫКЛЮЧЕНО` (`state == false`)
* ТО изменить состояние на `ВКЛЮЧЕНО` (`state = true`) и отправить команду на включение нагревателя.
* ЕСЛИ `Temperature >= Upper_Threshold` (например, 22.0 >= 22.0)
* И ЕСЛИ состояние системы сейчас `ВКЛЮЧЕНО` (`state == true`)
* ТО изменить состояние на `ВЫКЛЮЧЕНО` (`state = false`) и отправить команду на выключение нагревателя.
* ЕСЛИ `Lower_Threshold <= Temperature < Upper_Threshold` (например, температура находится в диапазоне `21.0, 22.0)`)
* ТО ничего не делать. Система переходит в режим ожидания и сохраняет свое текущее состояние.
> 💡 Напоминание: Детальную физику процесса и теорию о том, почему возникает «флаппинг» (дребезг реле) без использования мертвых зон, мы подробно разбирали в [базовом уроке по теории состояний. В текущем уроке мы опускаем теоретическую часть и фокусируемся исключительно на программной реализации этого поведения в Node-RED.
Практический подход к хранению состояния
Для хранения переменной `state` мы будем использовать контекст узла (node context). В Node-RED существует три уровня контекста (Node, Flow, Global), но поскольку текущее состояние нагревателя (включен/выключен) необходимо отслеживать только внутри конкретного узла `function`, вычисляющего пороги, уровня Node будет достаточно. Это позволяет скрипту "помнить" свое состояние между поступлениями новых данных от датчика температуры, не делясь этой информацией с остальными частями потока и не расходуя глобальную память.
Практика: Сборка потока в Node-RED
рактика: Сборка потока в Node-RED
Перейдем к непосредственной реализации спроектированной логики на контроллере HI.
Обзор необходимых узлов
Для сборки нашего потока мы будем использовать стандартный набор узлов, доступных в Node-RED.
Реализация гистерезиса в узле Function
Теперь напишем JavaScript-код, который будет выполнять всю основную работу. Этот код будет размещен внутри узла `function`, который мы создали ранее.
Инициализация переменных и работа с контекстом
Первым делом, внутри функции нам нужно определить наши константы (уставку и гистерезис) и получить текущее значение температуры из входящего сообщения. Также мы будем активно использовать контекст узла (node context) для хранения состояния нагревателя между вызовами функции.
javascript
// 1. Определение параметров сценария из контекста
// Считываем уставку и гистерезис из контекста потока (flow context).
// Это позволяет менять их "на лету", не редактируя код.
// Если переменные не заданы, используются значения по умолчанию.
const setpoint = flow.get('heating_setpoint') || 22.0;
const hysteresis = flow.get('heating_hysteresis') || 1.0;
// Рассчитываем пороги
const lowerThreshold = setpoint - hysteresis;
const upperThreshold = setpoint;
// 2. Получение и валидация входящих данных
// Датчик присылает температуру в виде строки, преобразуем ее в число
const currentTemp = parseFloat(msg.payload);
// Проверяем, что получили корректное число.
// Если нет - выводим ошибку в лог и останавливаем выполнение.
if (isNaN(currentTemp)) {
node.error("Получено нечисловое значение температуры: " + msg.payload, msg);
node.status({fill:"red", shape:"dot", text:"Ошибка: неверные данные"});
return null; // Прекратить обработку
}
// 3. Работа с состоянием
// Получаем ТЕКУЩЕЕ состояние нагревателя из контекста узла.
// Если оно не задано (первый запуск), считаем, что нагреватель выключен (false).
let isHeating = context.get('heatingState') || false;
// Переменная для новой команды. По умолчанию - не меняем состояние.
let command = null;
// 4. Основная логика гистерезиса
if (currentTemp < lowerThreshold) {
// Температура упала ниже нижнего порога
if (isHeating === false) {
// И нагреватель был выключен...
command = 1; // ...формируем команду на ВКЛЮЧЕНИЕ
isHeating = true; // и обновляем наше внутреннее состояние
}
// Если нагреватель уже был включен, ничего не делаем
} else if (currentTemp >= upperThreshold) {
// Температура достигла или превысила верхний порог
if (isHeating === true) {
// И нагреватель был включен...
command = 0; // ...формируем команду на ВЫКЛЮЧЕНИЕ
isHeating = false; // и обновляем наше внутреннее состояние
}
// Если нагреватель уже был выключен, ничего не делаем
}
// Если температура находится в "мертвой зоне", блок if/else if не сработает,
// и переменная 'command' останется 'null'.
// 5. Отправка команды и обновление статуса
if (command !== null) {
// Если была сформирована новая команда (0 или 1)...
// Сохраняем новое состояние в контексте узла для следующего вызова
context.set('heatingState', isHeating);
// Формируем исходящее сообщение для отправки на реле
msg.payload = command;
// Обновляем визуальный статус узла для удобства отладки
if(isHeating) {
node.status({fill:"red", shape:"dot", text:"ВКЛ: " + currentTemp + "°C"});
} else {
node.status({fill:"green", shape:"dot", text:"ВЫКЛ: " + currentTemp + "°C"});
}
// Отправляем сообщение дальше по потоку (на узел 'rbe' и затем 'mqtt out')
return msg;
} else {
// Если команда не изменилась, просто обновляем статус для информации
const statusText = isHeating ? "ВКЛ: " : "ВЫКЛ: ";
node.status({shape:"dot", fill: isHeating ? "red" : "green", text: statusText + currentTemp + "°C"});
// И ничего не отправляем дальше (возвращаем null)
return null;
}
```
Разбор кода:- Секция 1: Определяем рабочие параметры. Вместо жестко заданных констант мы считываем уставку (`setpoint`) и гистерезис (`hysteresis`) из контекста потока (flow context), используя `flow.get()`. Это профессиональный подход, отделяющий конфигурацию от логики. Теперь для изменения уставки достаточно установить переменную `flow.heating_setpoint`, например, через узел `inject` или панель `node-red-dashboard`. Если переменные в контексте не заданы, используются значения по умолчанию (`22.0` и `1.0`).
- Секция 2: Получаем и валидируем текущую температуру. Важнейший шаг здесь — валидация `isNaN(currentTemp)`, чтобы убедиться, что от датчика не пришло что-то вроде `"unavailable"` или пустой строки.
- Секция 3: `context.get('heatingState') || false` — это идиома для получения значения из контекста узла. Если `heatingState` еще не был установлен, `context.get()` вернет `undefined`, и оператор `||` присвоит `isHeating` значение `false`. Внутреннее состояние хранится именно в контексте узла, а не потока.
- Секция 4: Реализует логику, описанную на этапе проектирования. Команда (`command`) формируется только в том случае, если текущее состояние отличается от требуемого.
- Секция 5: Ключевой момент. Сообщение отправляется дальше по потоку (`return msg;`) только если `command` изменился. В противном случае `return null;` останавливает поток. Это экономит ресурсы и делает работу связки с узлом `rbe` еще более надежной. Состояние `heatingState` сохраняется в контекст с помощью `context.set()`. Визуальный статус `node.status()` помогает мгновенно оценить работу сценария.
Теперь скопируйте этот код в ваш узел `function`. Сценарий готов к работе.
---
Отказоустойчивость: Обработка нештатных ситуаций
тказоустойчивость: Обработка нештатных ситуаций
Хотя использование программного гистерезиса уже защитило нас от частого переключения состояний (флаппинга) на границе целевой температуры (подробнее о физике этого процесса мы говорили в базовом уроке по устранению дребезга или реализации watchdog-таймера (как мы сделали для отслеживания `safe-