ГлавнаяАкадемияСценарии умного дома: режимы, состояния, приоритеты → Гистерезис для аналоговых датчиков (температура, влажность, освещенность)

Гистерезис для аналоговых датчиков (температура, влажность, освещенность)

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

Введение в гистерезис: концепция 'мертвой зоны'

В предыдущих уроках мы рассмотрели проблему дребезга (flapping) и методы борьбы с ней, основанные на времени, такие как задержки и узел `trigger`. Однако существует целый класс проблем, где временные задержки неэффективны или недостаточны. Это касается, в первую очередь, работы с аналоговыми датчиками: температуры, влажности, освещенности, давления и т.д.

Представьте датчик освещенности, установленный на улице. Его задача — включать освещение, когда стемнеет. Вы устанавливаете порог в 100 люкс. Как только освещенность падает до 99.9 люкс, свет включается. Но что произойдет, если значение начнет колебаться прямо на границе порога: 99, 101, 99.5, 100.5? Система начнет хаотично включать и выключать реле, создавая светомузыку и приводя к преждевременному износу оборудования. Это и есть дребезг аналогового датчика.

Для решения этой проблемы применяется мощный и элегантный метод — гистерезис.

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

> * Гистерезис (от греч. «запаздывание») — это свойство системы, при котором её текущее состояние зависит не только от текущих входных параметров, но и от её предыдущего состояния. В автоматизации это техника управления, использующая два разных порога для изменения состояния.

> * Верхний порог (High Threshold): Значение, при превышении которого система переходит в одно состояние (например, выключается).

> * Нижний порог (Low Threshold): Значение, при падении ниже которого система переходит в другое состояние (например, включается).

> * Мертвая зона (Dead Zone / Dead Band): Диапазон значений между верхним и нижним порогами. Находясь в этой зоне, система не меняет своего состояния, что обеспечивает стабильность.

Рассмотрим на простом примере комнатного термостата, управляющего обогревателем:

  • Целевая температура: 22°C.
  • Простой подход (без гистерезиса): Если температура < 22°C, включить обогрев. Если температура >= 22°C, выключить. Как только температура достигнет 22.01°C, обогреватель выключится. Через несколько секунд она упадет до 21.99°C, и он снова включится. Это приведет к постоянным щелчкам реле (тактование).
  • Подход с гистерезисом:
  • * Устанавливаем нижний порог на включение: 21.5°C.

    * Устанавливаем верхний порог на выключение: 22.5°C.

    Теперь логика работает иначе:

    Диапазон от 21.5°C до 22.5°C является той самой мертвой зоной. Если температура колеблется внутри этого градуса (например, 21.8°C, 22.1°C, 22.4°C), система не будет реагировать. Обогреватель будет либо стабильно работать, либо стабильно оставаться выключенным. Это и есть ключевое преимущество гистерезиса: он вносит "память" и инерцию в систему, делая ее устойчивой к мелким колебаниям входного сигнала.

    ---

    Реализация гистерезиса в Node-RED с помощью узла Function

    Для реализации stateful-логики, то есть логики, зависящей от предыдущего состояния, нам необходимо где-то это состояние хранить. В Node-RED идеальным инструментом для этого является контекст потока (flow context). В отличие от глобального контекста, он изолирован в пределах одной вкладки (flow), что делает логику инкапсулированной и предсказуемой.

    Алгоритм реализации гистерезиса в узле `Function` выглядит следующим образом:

  • Получить текущее значение от датчика из `msg.payload`.
  • Получить текущее состояние системы (например, 'ON' или 'OFF') из контекста потока с помощью `context.get()`. Если состояние еще не установлено, задать ему начальное значение (например, 'OFF').
  • Сравнить текущее значение с порогами, учитывая текущее состояние.
  • * Если система ВЫКЛЮЧЕНА ('OFF'): Проверять только условие на включение (например, `значение < нижний_порог`). Если оно выполнилось — изменить состояние на 'ON', сохранить его в контекст через `context.set()` и передать дальше сообщение о необходимости включения.

    * Если система ВКЛЮЧЕНА ('ON'): Проверять только условие на выключение (например, `значение > верхний_порог`). Если оно выполнилось — изменить состояние на 'OFF', сохранить его в контекст и передать дальше сообщение о выключении.

    * Во всех остальных случаях (когда значение находится в "мертвой зоне" или движется не в ту сторону), ничего не делать — просто остановить поток, вернув `null`.

    Это предотвращает отправку лишних команд и обеспечивает стабильность.

    Пример кода для узла `Function`

    Давайте напишем универсальный JavaScript-код, который реализует эту логику. Мы определим пороги в начале кода для удобства настройки.

    /*
    

    * Узел для реализации логики гистерезиса.

    * Ожидает на входе msg.payload с числовым значением от датчика.

    * Хранит текущее состояние (state) в контексте потока.

    * Возвращает сообщение только при необходимости изменить состояние.

    */

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

    const LOW_THRESHOLD = 21.5; // Нижний порог. Если значение упадет НИЖЕ, система включится.

    const HIGH_THRESHOLD = 22.5; // Верхний порог. Если значение поднимется ВЫШЕ, система выключится.

    const ON_PAYLOAD = "ON"; // Сообщение, отправляемое при включении.

    const OFF_PAYLOAD = "OFF"; // Сообщение, отправляемое при выключении.

    const CONTEXT_VAR_NAME = "hysteresis_state"; // Имя переменной в контексте потока.

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

    // 1. Получаем значение от датчика. Убедимся, что это число.

    const currentValue = parseFloat(msg.payload);

    if (isNaN(currentValue)) {

    node.warn("Некорректное входящее значение: " + msg.payload);

    return null; // Прекращаем обработку, если данные не являются числом.

    }

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

    // Если его нет, устанавливаем начальное состояние "OFF".

    let currentState = context.get(CONTEXT_VAR_NAME) || "OFF";

    // 3. Основная логика гистерезиса

    let newState = null;

    if (currentState === "OFF") {

    // Если система сейчас выключена, мы ждем только события для ВКЛЮЧЕНИЯ.

    if (currentValue < LOW_THRESHOLD) {

    node.status({ fill: "green", shape: "dot", text: `ВКЛ: ${currentValue} < ${LOW_THRESHOLD}` });

    newState = "ON";

    }

    } else { // currentState === "ON"

    // Если система сейчас включена, мы ждем только события для ВЫКЛЮЧЕНИЯ.

    if (currentValue > HIGH_THRESHOLD) {

    node.status({ fill: "red", shape: "dot", text: `ВЫКЛ: ${currentValue} > ${HIGH_THRESHOLD}` });

    newState = "OFF";

    }

    }

    // 4. Если состояние должно измениться

    if (newState !== null) {

    // Сохраняем новое состояние в контекст для следующего запуска

    context.set(CONTEXT_VAR_NAME, newState);

    // Формируем исходящее сообщение

    if (newState === "ON") {

    msg.payload = ON_PAYLOAD;

    } else {

    msg.payload = OFF_PAYLOAD;

    }

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

    return msg;

    }

    // Если состояние не меняется (значение в "мертвой зоне"),

    // ничего не возвращаем (null), чтобы остановить поток.

    node.status({ fill: "blue", shape: "ring", text: `IDLE (${currentState}): ${currentValue}` });

    return null;

    Этот код является мощным и переиспользуемым шаблоном. Вы можете скопировать его, изменить пороги и `payload` под свои задачи, и он будет надежно работать. Обратите внимание на использование `node.status` — это критически важный паттерн для визуальной отладки.

    ---

    Практический пример: управление освещением по датчику освещенности

    Рассмотрим реальный сценарий на базе нашей платформы.

    Задача: Автоматически управлять уличным освещением на фасаде коттеджа. * Включить свет, когда освещенность упадет ниже 100 лк.

    * Выключить свет, когда освещенность поднимется выше 150 лк.

    * Игнорировать кратковременные изменения (например, тень от проезжающей машины, закрывшей датчик).

    > 💡 Подсказка: Для повышения надежности, комбинируйте гистерезис с техниками из предыдущих уроков. Например, добавьте небольшую "Задержку на включение" (см. урок COURSE-07-M04-L02), чтобы отфильтровать тень от пролетающей птицы или проезжающего автомобиля. Задержка в 1-2 минуты будет уместна.

    Структура потока в Node-RED

    [MQTT In] ---> [Function: Гистерезис] ---> [RBE] ---> [Switch] ---> [MQTT Out: Команда реле]
    

    |

    +--------> [Debug]

  • `MQTT In`:
  • * Topic: `telemetry/sensors/light_street_1/illuminance`

    * Подписывается на топик с данными от датчика освещенности.

  • `Function` ("Гистерезис"):
  • * Содержит код, аналогичный приведенному выше, но с адаптированными порогами:

    * `LOW_THRESHOLD = 100`

    * `HIGH_THRESHOLD = 150`

    * `ON_PAYLOAD = { "command": "ON" }`

    * `OFF_PAYLOAD = { "command": "OFF" }` (Используем JSON-объект для следования контракту сообщений).

  • `RBE` (Report by Exception):
  • * Mode: `block unless value changes`.

    * Этот узел является дополнительной линией защиты. Он пропустит сообщение дальше только в том случае, если `msg.payload` изменился по сравнению с предыдущим. Если по какой-то причине узел `Function` начнет отправлять одинаковые команды (например, 'ON', 'ON', 'ON'), `RBE` пропустит только первую.

  • `Switch`:
  • * Маршрутизирует сообщение для отправки и для отладки, если требуется.

  • `MQTT Out`:
  • * Topic: `commands/relays/relay_08/set`

    * Публикует команду на управление реле.

    Анализ `msg` объекта

    Давайте проследим путь сообщения.

    Предположим, наш датчик освещенности — это Modbus-устройство, а отдельный поток (как мы рассматривали в модуле по протоколам) считывает его данные и публикует в MQTT.

  • Сообщение на выходе `MQTT In`:
  •     {

    "topic": "telemetry/sensors/light_street_1/illuminance",

    "payload": "98.5",

    "_msgid": "a1b2c3d4.e5f6g7"

    }

  • Обработка в `Function`:
  • * Код получает `98.5`.

    * Предположим, `context.get("hysteresis_state")` вернул `OFF`.

    * Условие `98.5 < 100` истинно.

    * `context.set("hysteresis_state", "ON")`.

    * Узел возвращает новое сообщение.

  • Сообщение на выходе `Function`:
  •     {

    "topic": "telemetry/sensors/light_street_1/illuminance",

    "payload": {

    "command": "ON"

    },

    "_msgid": "a1b2c3d4.e5f6g7"

    }

  • На входе `RBE`: узел видит, что предыдущее значение было, например, `{ "command": "OFF" }`. Новое значение `{ "command": "ON" }` отличается, поэтому он пропускает сообщение дальше.
  • `MQTT Out`: публикует `{"command": "ON"}` в топик `commands/relays/relay_08/set`. Реле включается.
  • Теперь освещенность растет до `120 лк`. `Function` получает `120`. Состояние `ON`. Условие `120 > 150` ложно. `Function` возвращает `null`. Поток прерывается. Свет продолжает гореть. Так будет происходить, пока значение не превысит 150 лк, обеспечивая стабильную работу.

    ---

    Применение гистерезиса для климат-контроля (отопление и кондиционирование)

    Сценарий управления климатом (HVAC) является классическим примером, где гистерезис критически важен. Частые запуски и остановки компрессора кондиционера или циркуляционного насоса котла приводят к их быстрому износу и значительному перерасходу электроэнергии.

    Здесь логика усложняется, так как у нас есть три основных состояния системы: ОТОПЛЕНИЕ (HEATING), ОХЛАЖДЕНИЕ (COOLING) и ОЖИДАНИЕ (IDLE). Соответственно, нам потребуется уже четыре порога.

    * `HEAT_ON_THRESHOLD`: 22.0°C (включить котел, если температура упала ниже)

    * `HEAT_OFF_THRESHOLD`: 23.0°C (выключить котел, если температура достигла уставки)

    * `COOL_ON_THRESHOLD`: 24.0°C (включить кондиционер, если температура поднялась выше)

    * `COOL_OFF_THRESHOLD`: 23.0°C (выключить кондиционер, если температура достигла уставки)

    > ⚠️ Внимание: Неправильно настроенные пороги гистерезиса для климатических систем могут привести как к некомфортному микроклимату, так и к повышенному расходу энергии. Всегда оставляйте "мертвую зону" (dead band) как минимум в 1-2°C между порогом выключения отопления (`HEAT_OFF_THRESHOLD`) и порогом включения кондиционирования (`COOL_ON_THRESHOLD`). В нашем примере это `24.0°C - 23.0°C = 1°C`. Эта зона предотвращает "борьбу" систем друг с другом, когда отопление только выключилось, а кондиционер уже пытается включиться.

    Логика состояний в узле `Function`

    Код для такой системы будет представлять собой небольшой конечный автомат (FSM), где мы проверяем условия перехода в зависимости от текущего состояния.

    /*
    

    * Конечный автомат для управления климат-контролем (HVAC)

    * с тремя состояниями: IDLE, HEATING, COOLING.

    */

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

    const HEAT_ON_THRESHOLD = 22.0;

    const HEAT_OFF_THRESHOLD = 23.0;

    const COOL_ON_THRESHOLD = 24.0;

    const COOL_OFF_THRESHOLD = 23.0;

    const CONTEXT_VAR_NAME = "hvac_state";

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

    const currentValue = parseFloat(msg.payload);

    if (isNaN(currentValue)) {

    node.warn("Некорректное значение температуры: " + msg.payload);

    return null;

    }

    let currentState = context.get(CONTEXT_VAR_NAME) || "IDLE";

    let command = null;

    switch (currentState) {

    case "IDLE":

    // В режиме ожидания мы можем либо начать греть, либо начать охлаждать

    if (currentValue < HEAT_ON_THRESHOLD) {

    command = { hvac: "HEAT", reason: `Temp ${currentValue} < ${HEAT_ON_THRESHOLD}` };

    context.set(CONTEXT_VAR_NAME, "HEATING");

    } else if (currentValue > COOL_ON_THRESHOLD) {

    command = { hvac: "COOL", reason: `Temp ${currentValue} > ${COOL_ON_THRESHOLD}` };

    context.set(CONTEXT_VAR_NAME, "COOLING");

    }

    break;

    case "HEATING":

    // В режиме отопления мы ждем только события на выключение отопления

    if (currentValue > HEAT_OFF_THRESHOLD) {

    command = { hvac: "OFF", reason: `Temp ${currentValue} > ${HEAT_OFF_THRESHOLD}` };

    context.set(CONTEXT_VAR_NAME, "IDLE");

    }

    break;

    case "COOLING":

    // В режиме охлаждения мы ждем только события на выключение кондиционера

    if (currentValue < COOL_OFF_THRESHOLD) {

    command = { hvac: "OFF", reason: `Temp ${currentValue} < ${COOL_OFF_THRESHOLD}` };

    context.set(CONTEXT_VAR_NAME, "IDLE");

    }

    break;

    }

    if (command) {

    node.status({ fill: "green", shape: "dot", text: `${command.hvac}: ${command.reason}` });

    msg.payload = command;

    return msg;

    }

    node.status({ fill: "blue", shape: "ring", text: `${currentState} @ ${currentValue}°C` });

    return null;

    Этот код отправляет структурированный объект, например `{"hvac": "HEAT"}`, который затем можно обработать в узле `Switch` и направить на соответствующие MQTT-топики для управления котлом и кондиционером. Такая реализация не только стабильна, но и энергоэффективна.

    ---

    Итоги: гистерезис в сравнении с задержками и узлом Trigger

    Теперь, когда мы освоили гистерезис, важно понять его место среди других инструментов для борьбы с дребезгом и создания стабильных сценариев.

    > 🔗 Связанный материал: Для глубокого понимания временных задержек, вернитесь к уроку COURSE-07-M04-L02 'Техника 'Задержка на включение/выключение' (Delay On/Off)' и COURSE-07-M04-L03 про узел 'trigger'.

    Гистерезис — это stateful-техника, основанная на пороговых значениях. Она идеальна для аналоговых сигналов, где значение плавно меняется. Её главная задача — создать "мертвую зону", чтобы предотвратить частые переключения при колебаниях значения около одного порога.

    Давайте сравним три основных подхода:

    | Метод | Принцип работы | Идеально подходит для | Не подходит для |

    | --------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |

    | Гистерезис | Два порога (верхний и нижний) создают 'мертвую зону' | Управление по аналоговым датчикам (температура, влажность, освещенность) | Быстрой реакции на дискретные события (нажатие кнопки) |

    | Задержка (Delay) | Выжидает заданное время перед изменением состояния | Фильтрация коротких импульсов ("тень птицы"), защита от дребезга контактов | Плавного управления по аналоговому сигналу (не решает проблему колебаний) |

    | Узел `trigger` | Ограничивает частоту или создает последовательности | Ограничение частоты сообщений, сценарии "включить и выключить через N сек" (свет в коридоре) | Реализации сложной логики состояний, как в HVAC |

    Ключевой вывод: эти методы не взаимоисключающие, а взаимодополняющие. Гистерезис vs. Задержка: Гистерезис реагирует на величину изменения сигнала, а задержка — на его длительность*. В сценарии с уличным освещением можно сначала применить гистерезис для определения базовой логики "темно/светло", а затем добавить узел `Delay` на 1 минуту перед командой на включение, чтобы игнорировать кратковременное затемнение от проезжающей фуры.

    Освоив гистерезис, вы получили в свой арсенал один из самых важных паттернов для создания по-настоящему надежных, предсказуемых и эффективных систем автоматизации.

    Что дальше?

    В следующем уроке мы перейдем к более сложной концепции — приоритетам сценариев. Мы научимся разрешать конфликты, когда несколько автоматизаций пытаются управлять одним и тем же устройством одновременно (например, ручное управление, недельное расписание и сценарий "Никого нет дома").