ГлавнаяАкадемияСценарии умного дома: режимы, состояния, приоритеты → SCN-CLIMATE-001: Поддержание температуры (термостат с гистерезисом)

SCN-CLIMATE-001: Поддержание температуры (термостат с гистерезисом)

Урок · Сценарии умного дома: режимы, состояния, приоритеты · 30 мин · theory

Введение в гистерезис: Ключ к стабильному климат-контролю

ведение в гистерезис: Ключ к стабильному климат-контролю

В предыдущем уроке мы детально разобрали проблему «дребезга» (flapping), возникающую при использовании простой пороговой логики. Напомним, что это приводит к ускоренному износу оборудования, избыточной нагрузке на систему и снижению комфорта.

Решением этой проблемы является гистерезис.

> 💡 Подсказка: Использование гистерезиса значительно продлевает срок службы реле и контакторов, управляющих нагревательными или охлаждающими элементами, за счет сокращения количества циклов включения/выключения.

📋 Ключевые понятия:

Вместо одного порога, гистерезис вводит два: верхний и нижний. Логика работы меняется:

  • Нагреватель включается, когда температура опускается ниже нижнего порога.
  • Нагреватель выключается, когда температура поднимается выше верхнего порога.
  • Между этими двумя порогами образуется "мертвая зона", в которой контроллер не предпринимает никаких действий, позволяя температуре естественно колебаться.

    | Критерий | Простая пороговая логика (НЕПРАВИЛЬНО) | Логика с гистерезисом (ПРАВИЛЬНО) |

    | :--- | :--- | :--- |

    | Уставка | 22.0°C | 22.0°C |

    | Гистерезис | 0°C | 1.0°C |

    | Порог включения | <= 22.0°C | <= 21.5°C (Уставка - Гистерезис / 2) |

    | Порог выключения | > 22.0°C | >= 22.5°C (Уставка + Гистерезис / 2) |

    | Поведение при 21.8°C | Включить -> Выключить -> Включить... (дребезг) | Ничего не делать (если был выключен) |

    | Износ реле | Очень высокий | Низкий |

    Лучшая аналогия для понимания — работа обычного бытового холодильника. Вы задаете желаемую температуру, например, +5°C. Компрессор включается, только когда температура внутри поднимется до +7°C (верхний порог), и работает до тех пор, пока не охладит камеру до +3°C (нижний порог).

    Варианты расчета порогов: Симметричный и Асимметричный гистерезис

    В зависимости от задачи, "мертвая зона" может распределяться относительно целевой температуры по-разному. Различают два основных подхода программирования логики:

  • Симметричный гистерезис (как в таблице выше). Целевая температура находится строго в центре.
  • Формула включения:* `Setpoint - (Hysteresis / 2)`

    Формула выключения:* `Setpoint + (Hysteresis / 2)`

  • Асимметричный гистерезис. Пороги отклонения вниз (допуск на охлаждение) и вверх (допуск на перегрев) задаются раздельно. В системах вроде Home Assistant (компонент Generic Thermostat) это параметры `cold_tolerance` и `hot_tolerance`.
  • Формула включения:* `Setpoint - cold_tolerance`

    Формула выключения:* `Setpoint + hot_tolerance`

    > ⚠️ Зачем нужна асимметрия? Если 22°C — это абсолютный минимум комфорта для пользователя, вы ставите уставку 22°C, `cold_tolerance` = 0.0 (не допускаем падения ниже), а `hot_tolerance` = 1.0 (греем до 23°C и выключаем). Это гарантирует, что температура никогда не упадет до дискомфортных значений.

    Применение в освещении: Решение проблемы заката

    Хотя мы рассматриваем климат, принцип гистерезиса универсален. В уроке M04 мы упоминали "дребезг" уличного света при проплывающих облаках на закате.

    Выбор величины гистерезиса в зависимости от типа оборудования

    Ширина мертвой зоны и допуски должны соотноситься с тепловой инерцией вашей системы:

    Рекомендуемый гистерезис:* 1.0°C – 1.5°C (иначе реле будет щелкать каждые 5 минут). Рекомендуемый гистерезис:* 0.5°C – 1.0°C. Рекомендуемый гистерезис для воздуха:* 0.2°C – 0.5°C. (В идеале используется ПИД-регулирование, но узкий асимметричный гистерезис также решает задачу).

    🛠 Практическое мини-задание

    Давайте наложим теорию на практические цифры до того, как перейдем к написанию кода.

    Сценарий: Вы настраиваете обогреватель типа "конвектор" в спальне. Идеальная температура для сна клиента: 21.5°C. Клиент сильно чувствует холод, если температура падает даже незначительно, но спокойно переносит легкий перегрев.

    Вы применяете асимметричный гистерезис: `cold_tolerance` = 0.2°C, а `hot_tolerance` = 0.8°C.

    Вычислите ожидаемое поведение системы (ответы раскройте ниже):
  • При какой температуре на датчике система пошлет сигнал «ВКЛ»?
  • При какой температуре на датчике система пошлет сигнал «ВЫКЛ»?
  • Какова общая ширина гистерезиса (в градусах) в этом сценарии?
  • > 💡 Решение и проверка себя:

    > 1. Температура включения: `21.5°C - 0.2°C = ` 21.3°C

    > 2. Температура выключения: `21.5°C + 0.8°C = ` 22.3°C

    > 3. Общая ширина рабочей зоны: `0.2°C + 0.8°C = ` 1.0°C

    >

    > Итог: Комната всегда будет находиться в диапазоне 21.3°C - 22.3°C, реле не будет нагружено частыми стартами, а клиент не замерзнет.

    Проектирование логики термостата в Node-RED

    роектирование логики термостата в Node-RED

    Перед тем как собирать поток, необходимо четко спроектировать его логику. Наш термостат будет оперировать тремя основными переменными, которые должны быть доступны внутри потока, а также обрабатывать входящие данные от датчика.

    Входные данные и состояние

  • Текущая температура (`current_temp`): Значение, поступающее от физического датчика. В нашей системе оно будет приходить по MQTT в виде числового значения (например, `21.5`) в `msg.payload`.
  • Целевая температура (уставка, `setpoint`): Желаемое значение, которое мы хотим поддерживать. Для начала мы зададим его как константу (например, `22.0`), но в будущем его можно будет динамически изменять через интерфейс дашборда или по расписанию.
  • Значение гистерезиса (`hysteresis`): "Мертвая зона" (дельта) вокруг целевой температуры, предотвращающая "дребезг" реле. Оптимальное значение зависит от инертности системы отопления и помещения:
  • Водяные теплые полы (высокая инертность):* 0.5 – 1.0 °C.

    Электрические конвекторы/радиаторы (средняя инертность):* 1.0 – 2.0 °C.

  • Состояние нагревателя (`is_heating`): Булевый флаг (`true`/`false`), который показывает, включен ли нагреватель программно в данный момент. Это критически важный параметр. Без отслеживания текущего (предыдущего) состояния логика не сможет принять верное решение. Например, если температура находится внутри "мертвой зоны" (гистерезиса), нам нужно знать, что делал нагреватель до этого — работал или простаивал, чтобы поддержать статус-кво.
  • > ⚠️ Внимание: Сохранение состояния в контексте, который очищается при перезапуске (memory-only context), приведет к некорректной работе логики (потере знания о том, включено ли реле) после перезагрузки контроллера или деплоя Node-RED. В рабочих проектах используйте файловый контекст (file-backed context) для критичных сценариев, что настроено по умолчанию на платформе HI. Обращение в коде выглядит так: `flow.get("is_heating", "file")`.

    Алгоритм принятия решения

    Алгоритм будет реализован в одном узле `function`. На каждом его запуске (при получении нового MQTT-сообщения с температурой) логика пройдет через следующие этапы:

  • Инициализация контекста: При чтении переменных из контекста потока (`flow context`) необходимо задать значения по умолчанию (fallback values), используя логическое ИЛИ (`||`). Это гарантирует, что сценарий не выдаст ошибку `undefined` при самом первом запуске после развертывания.
  • Получение и валидация данных: Извлечь `current_temp` из входящего `msg.payload`. Обязательно проверить, является ли полученное значение числом (защита от "мусорных" сообщений вроде "offline" или пустых строк).
  • Расчет порогов срабатывания: Вычислить верхний и нижний пороги по формулам:
  • * `upper_threshold = setpoint + (hysteresis / 2)`

    * `lower_threshold = setpoint - (hysteresis / 2)`

  • Основная логика (Стейт-машина):
  • * ЕСЛИ `current_temp` упала ниже `lower_threshold` И нагреватель сейчас выключен (`is_heating === false`):

    * Решение: Включить нагреватель.

    * Обновить состояние: `is_heating = true`.

    * ИНАЧЕ ЕСЛИ `current_temp` поднялась выше `upper_threshold` И нагреватель сейчас включен (`is_heating === true`):

    * Решение: Выключить нагреватель.

    * Обновить состояние: `is_heating = false`.

    * ИНАЧЕ:

    * Температура находится в норме или внутри гистерезиса. Команду отправлять не нужно.

  • Формирование команды и сохранение: Если состояние `is_heating` изменилось, сформировать управляющее сообщение для реле (обычно `1` для включения, `0` для выключения) и сохранить новый флаг `is_heating` обратно в контекст. Если изменений нет — остановить поток (`return null`), предотвращая спам командами в шину MQTT.
  • Визуализация в редакторе: Использовать API `node.status()` для обновления текста под узлом, чтобы администратор видел текущие показатели без включения Debug-узла (например: `🟢 Вкл | T: 21.0 | Уст: 22.0`).
  • Пример реализации в коде (Узел Function)

    На основе спроектированного алгоритма мы напишем следующий JavaScript код для узла Function (подробно разберем его применение в следующем разделе):

    // Извлекаем температуру с датчика, преобразуем в число (float)
    

    let current_temp = parseFloat(msg.payload);

    // 1. Валидация: если пришло не число, прерываем выполнение

    if (isNaN(current_temp)) {

    node.warn("Получена некорректная температура: " + msg.payload);

    return null;

    }

    // 2. Инициализация переменных из контекста с дефолтными значениями

    let setpoint = flow.get("setpoint") || 22.0;

    let hysteresis = flow.get("hysteresis") || 1.0;

    // Состояние реле: если переменной нет, считаем, что выключено

    let is_heating = flow.get("is_heating") || false;

    // 3. Расчет порогов

    let half_hyst = hysteresis / 2;

    let upper_threshold = setpoint + half_hyst;

    let lower_threshold = setpoint - half_hyst;

    // 4. Флаг изменения состояния

    let state_changed = false;

    // Основная логика термостата

    if (current_temp <= lower_threshold && !is_heating) {

    is_heating = true;

    state_changed = true;

    } else if (current_temp >= upper_threshold && is_heating) {

    is_heating = false;

    state_changed = true;

    }

    // 5. Визуализация статуса узла в Node-RED

    let status_text = `T: ${current_temp.toFixed(1)}°C | Уст: ${setpoint.toFixed(1)}°C`;

    node.status({

    fill: is_heating ? "red" : "grey",

    shape: "dot",

    text: (is_heating ? "НАГРЕВ | " : "ОЖИДАНИЕ | ") + status_text

    });

    // 6. Формирование исходящего сообщения или блокировка потока

    if (state_changed) {

    // Сохраняем новое состояние в контекст потока

    flow.set("is_heating", is_heating);

    // Для нашего реле 1 - включить, 0 - выключить

    msg.payload = is_heating ? 1 : 0;

    return msg;

    } else {

    // Если состояние не изменилось, останавливаем поток (ничего не отправляем)

    return null;

    }

    Практическое мини-задание (Mental Check)

    > 💡 Проверьте себя перед сборкой:

    > Представьте, что вы задали уставку (`setpoint`) = 23.0°C, а гистерезис (`hysteresis`) = 2.0°C. Сейчас в помещении тепло, нагреватель выключен. Вы открываете окно, и температура начинает падать.

    >

    > Вопрос 1: При какой конкретно температуре термостат подаст команду на включение?

    > Вопрос 2: После включения вы закрыли окно, комната греется. При какой температуре термостат подаст команду на выключение?

    > Вопрос 3: Что произойдет при получении температуры 22.5°C?

    >

    > Ожидаемые ответы:

    > 1. Включение произойдет при `<= 22.0°C` (23.0 - (2.0/2)).

    > 2. Выключение произойдет при `>= 24.0°C` (23.0 + (2.0/2)).

    > 3. При 22.5°C система находится внутри гистерезиса. Скрипт проверит состояние (если греем — продолжаем греть, если выключено — останется выключенным), обновит статус под узлом и вернет `null` (команда на реле не отправится). Состояние не изменится.

    Такая логика гарантирует, что команда на переключение отправится строго в момент пересечения порогов, аппаратура реле будет защищена от частого щелканья, а радиоэфир (или шина платформы) не будет засоряться дублирующими сообщениями каждую секунду поступления данных с датчика.

    Практическая реализация: Сборка потока термостата

    рактическая реализация: Сборка потока термостата

    Теперь, имея четкий алгоритм, мы можем собрать рабочий поток `FLOW-CLIMATE-THERMOSTAT-001` в среде Node-RED на контроллере HI (или совместимом, например, Wiren Board).

    Структура потока

    Поток будет состоять из следующих основных узлов:

               ┌────────────────┐      ┌───────────────────────────┐      ┌──────────┐
    

    (MQTT In)──┤ MQTT In ├─►───-│ Function: Термостат ├─►───-┤ MQTT Out │───(To Relay)

    │ hi/temp/room1 │ │ (Логика с гистерезисом) │ │ hi/relay/1/set │

    └────────────────┘ └───────────┬───────────────┘ └──────────┘

    │ (Визуальный статус)

    ┌───────────┐

    │ node.status() │

    └───────────┘

    Дополнительно, для отладки, мы обязательно подключим узел `Debug` к выходу узла `Function`, а также несколько узлов `Inject` для симуляции датчика.

    Конфигурация узлов

  • Узел `mqtt in` (Получение температуры):
  • * Сервер: Выберите MQTT-брокер вашего контроллера (обычно `localhost:1883`).

    * Топик: Укажите топик, из которого приходят данные с датчика температуры. Например, для стандартного датчика WAGO, 1-Wire или Zigbee это может быть `telemetry/office/room1/temperature`.

    * Вывод: В поле `Output` выберите параметр `a parsed JSON object` (Разобранный объект JSON). Мы предполагаем, что датчик отправляет данные в стандартизированном формате (паттерн "Контракт сообщения"):

            {

    "value": 21.8,

    "source": "temp-sensor-office-1",

    "ts": 1678886400000,

    "unit": "°C"

    }

    Примечание:* Если ваш брокер отдает только "чистое" значение (payload вида `21.8`), вам потребуется скорректировать логику в узле `function` (убрать `.value`).

  • Узел `function` (Логика термостата):
  • Это сердце нашего сценария. Скопируйте следующий расширенный код в редактор узла.

    > 💡 Подсказка: Используйте `node.warn()` внутри узла `function` для вывода отладочных сообщений в боковую панель Debug. Это позволяет отслеживать переходные состояния без необходимости ставить лишние узлы.

        // SCN-CLIMATE-001: Логика термостата с гистерезисом

    // --- НАСТРОЙКИ ---

    // Целевая температура (Уставка / Setpoint).

    // Забираем из контекста (если задано через UI/Дашборд), иначе дефолт 22.0.

    const SETPOINT = flow.get('target_temp') || 22.0;

    // Гистерезис - "мертвая зона". Например, 1.0°C означает зону в ±0.5°C от уставки.

    const HYSTERESIS = flow.get('target_hysteresis') || 1.0;

    // -----------------

    // 1. ИНИЦИАЛИЗАЦИЯ

    // Получаем текущее состояние нагревателя из контекста потока.

    // Если его нет (первый запуск), считаем, что он выключен (false).

    let isHeating = flow.get('thermostat_is_heating') || false;

    // Сохраняем настройки в контекст для отладки.

    flow.set('thermostat_setpoint', SETPOINT);

    flow.set('thermostat_hysteresis', HYSTERESIS);

    // 2. ПОЛУЧЕНИЕ И ВАЛИДАЦИЯ ДАННЫХ

    // Ожидаем, что температура приходит в msg.payload.value (JSON формат)

    const currentTemp = parseFloat(msg.payload.value);

    // Проверяем, что получили корректное число.

    if (isNaN(currentTemp)) {

    node.status({ fill: "red", shape: "dot", text: "Ошибка: неверные данные" });

    node.error("Получено нечисловое значение температуры.", msg);

    return null; // Прерываем выполнение

    }

    // 3. РАСЧЕТ ПОРОГОВ

    const lowerThreshold = SETPOINT - (HYSTERESIS / 2); // Порог включения (21.5°C)

    const upperThreshold = SETPOINT + (HYSTERESIS / 2); // Порог выключения (22.5°C)

    // 4. ОСНОВНАЯ ЛОГИКА

    let command = null; // Команда для реле (обычно строка "1" или "0")

    let stateChanged = false; // Флаг изменения состояния

    if (currentTemp < lowerThreshold && isHeating === false) {

    // Температура упала ниже нижнего порога, а нагреватель выключен. ВКЛЮЧАЕМ.

    command = "1"; // Строковый формат надежнее для большинства MQTT-драйверов ПЛК

    isHeating = true;

    stateChanged = true;

    node.warn(`ВКЛЮЧЕНИЕ НАГРЕВА: Temp (${currentTemp}°C) < Lower (${lowerThreshold}°C)`);

    } else if (currentTemp >= upperThreshold && isHeating === true) {

    // Температура поднялась выше верхнего порога, а нагреватель включен. ВЫКЛЮЧАЕМ.

    command = "0";

    isHeating = false;

    stateChanged = true;

    node.warn(`ВЫКЛЮЧЕНИЕ НАГРЕВА: Temp (${currentTemp}°C) >= Upper (${upperThreshold}°C)`);

    }

    // 5. СОХРАНЕНИЕ СОСТОЯНИЯ И ВИЗУАЛИЗАЦИЯ

    // Обновляем состояние в персистентном контексте

    flow.set('thermostat_is_heating', isHeating);

    // Обновляем визуальный статус узла для быстрой диагностики

    let statusText = `Temp: ${currentTemp.toFixed(1)}°C | Set: ${SETPOINT.toFixed(1)}°C`;

    node.status({

    fill: isHeating ? "red" : "blue",

    shape: "dot",

    text: (isHeating ? "[ON] " : "[OFF] ") + statusText

    });

    // 6. ФОРМИРОВАНИЕ КОМАНДЫ

    // Отправляем сообщение дальше, только если состояние изменилось.

    // Это предотвращает флуд в MQTT-брокере и бережет ресурс реле (особенно электромагнитного).

    if (stateChanged) {

    msg.payload = command;

    return msg;

    } else {

    // Состояние в зоне гистерезиса или не пересекло пороги, ничего не отправляем в реле.

    return null;

    }

  • Узел `mqtt out` (Управление реле):
  • * Сервер: Тот же брокер `localhost:1883`.

    * Топик: Укажите командный топик для реле (нагревателя). На платформе Wiren Board для релейного модуля это выглядит как: `/devices/wb-mr6c_25/controls/K1/on`. Убедитесь, что реле, используемое для нагревателя, рассчитано на соответствующую нагрузку (ток) или подключено через контактор/твердотельное реле (SSR).

    * QoS: `1` (Обязательная доставка).

    * Retain: `false` (Командные топики обычно не должны быть Retained, кроме специфических настроек драйвера).

    Отладка и тестирование (Мини-задание)

    Загружать логику сразу на реальное "железо", не проверив её, — плохая практика. Проведем стендовое тестирование.

  • Подключите узел `Debug` к выходу узла `Function`.
  • Создайте три узла `Inject` и подключите их ко входу узла `Function`.
  • Настройте полезную нагрузку (Payload) каждого узла `Inject` как JSON:
  • * Inject 1 (Холодно): `{"value": 20.5}`

    * Inject 2 (Нормально): `{"value": 21.8}`

    * Inject 3 (Жарко): `{"value": 22.8}`

  • Нажмите Deploy.
  • План тестирования (Ожидаемый результат): Ожидание:* Статус узла `Function` меняется на красный `[ON] Temp: 20.5°C`. В боковой панели Debug появляется сообщение об ошибке/предупреждении `ВКЛЮЧЕНИЕ НАГРЕВА...` и команда `"1"`. Ожидание: Температура выросла, но мы в зоне гистерезиса (порог отключения — 22.5°C). Контекст не меняется. Статус узла остается красным: `[ON] Temp: 21.8°C`. В панель Debug новые команды не отправляются*. Ожидание:* Статус меняется на синий `[OFF] Temp: 22.8°C`. В Debug выводится `"0"` (Команда выключения контура).

    После успешного выполнения этого теста поток термостата готов к интеграции с реальными MQTT-устройствами. Заданная температура будет поддерживаться автоматически с бережным отношением к ресурсу оборудования.

    Резюме и лучшие практики

    езюме и лучшие практики

    В этом уроке мы разработали один из самых фундаментальных и востребованных сценариев автоматизации — термостат с гистерезисом. Мы разобрали, почему простая пороговая логика неприменима на практике и как «мертвая зона», создаваемая гистерезисом, решает проблему дребезга оборудования (частого переключения реле).

    Ключевые концепции, которые мы освоили:

    > ⚠️ Важно: Чтобы контекст сохранялся после перезагрузки контроллера или сбоя питания, в файле `settings.js` сервера Node-RED должно быть настроено хранение в файловой системе (`localfilesystem`), а не только в оперативной памяти (`memory`). Оптимизация команд (паттерн RBE): Наш код отправляет управляющую команду только* в момент фактического изменения состояния (Report By Exception). Если реле уже включено, повторная отправка `ON` блокируется, что избегает ненужного трафика в сети умного дома и лишних записей в логах.

    Чек-лист проверки надёжности термостата

    Перед тем как внедрять подобный узел в реальную (продуктовую) среду умного дома, убедитесь, что он соответствует следующим критериям:

    Мини-задание: Симуляция работы термостата

    Чтобы убедиться в правильности работы логики, проведите ручное тестирование потока. Используйте узлы `Inject` или программу MQTT Explorer для отправки тестовых значений.

    Условия: `SETPOINT = 22.0`, `HYSTERESIS = 0.5`.

    Верхний порог отключения: `22.5°C`. Нижний порог включения: `21.5°C`.

    План тестирования (отправляйте значения по очереди):
  • Отправляем `21.0`. Ожидаемый результат: На выходе узла формируется команда `ON` (температура ниже нормы, включаем нагрев).
  • Отправляем `21.8`. Ожидаемый результат: На выходе пусто (ничего не отправляется, мы внутри мертвой зоны, продолжаем нагрев).
  • Отправляем `22.6`. Ожидаемый результат: Формируется команда `OFF` (пересекли верхний порог, отключаем нагрев).
  • Отправляем `22.1`. Ожидаемый результат: На выходе пусто (мы внутри мертвой зоны, остываем, ждем достижения 21.5°C).
  • Перезагрузите Node-RED (или переразверните поток кнопкой Deploy). Отправляем `22.0`. Ожидаемый результат: Система должна «вспомнить», что до перезагрузки она была в состоянии охлаждения (`state: "OFF"`), и не отправлять команду на включение реле вплоть до падения температуры ниже 21.5°C.
  • Векторы развития сценария

    Этот сценарий является надежным фундаментом для более сложных систем климат-контроля и логичным продолжением темы практических сценариев после изучения освещения (урок COURSE-07-M06-L01). Его можно абстрагировать в Subflow и развивать дальше:

    > 💡 Информация: В следующем уроке мы расширим этот базовый сценарий, добавив возможность работы по недельному расписанию и удобное ручное управление через веб-интерфейс, используя узлы `node-red-dashboard`.

    Что дальше?

    Убедитесь, что вы успешно выполнили мини-задание и полностью поняли логику сохранения состояний (`flow.get` / `flow.set`) внутри узла `function`. Поэкспериментируйте с граничными значениями: отправьте текст вместо числа или экстремально высокие значения, чтобы продумать, как узел должен реагировать на аномалии. Подготовьтесь к следующей лабораторной работе, где мы будем объединять этот логический блок с интерфейсом пользователя.