ГлавнаяАкадемияNode-RED: установка, flows, msg/JSON, отладка → Паттерн 'Gate': блокировка/разрешение прохождения сообщений

Паттерн 'Gate': блокировка/разрешение прохождения сообщений

Урок · Node-RED: установка, flows, msg/JSON, отладка · 30 мин · theory

Введение в паттерн 'Gate': фильтрация и контроль потоков

Паттерн 'Gate' (в переводе — «Ворота» или «Шлюз») представляет собой один из наиболее важных и часто используемых механизмов в проектировании сложных потоков Node-RED. Его основная задача — динамически управлять прохождением сообщений (`msg`) через определенный участок логической цепи.

> 💡 Подсказка: Паттерн 'Gate' — это программный аналог шлагбаума на пути данных. В «открытом» состоянии он беспрепятственно пропускает все проходящие через него сообщения. В «закрытом» — полностью блокирует их, не давая потоку выполняться дальше. Это позволяет в любой момент взять управление автоматизацией на себя, игнорируя стандартную логику без необходимости переразвертывания (re-deploy) потоков.

Ключевая ценность этого паттерна заключается в его способности отделить основную логику автоматизации от логики управления ее состоянием.

📋 Ключевые сценарии использования:

Важно понимать фундаментальное отличие паттерна 'Gate' от стандартного узла `switch`. Узел `switch` является маршрутизатором: он анализирует сообщение и, в зависимости от его содержимого, направляет его по одному из нескольких возможных путей. 'Gate' же является фильтром: он не меняет направление потока, а принимает бинарное решение — пропустить сообщение дальше по единственному пути или полностью остановить его обработку.

| Критерий | Узел `switch` | Паттерн `Gate` |

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

| Назначение | Маршрутизация сообщений | Блокировка/разрешение прохождения сообщений |

| Количество выходов | Множество (по одному на каждое правило) | Один (для пропущенных сообщений) |

| Логика работы | "Если `msg.payload` равно X, отправить сюда" | "Если 'ворота' открыты, пропустить `msg` дальше" |

| Состояние | Не имеет собственного состояния (stateless) | Имеет состояние (открыто/закрыто), хранимое в контексте |

| Основной вопрос| "Куда должно пойти это сообщение?" | "Должно ли это сообщение вообще идти дальше?" |

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

---

Реализация 'Gate' с помощью узла `function` и контекста

Наиболее гибкий и мощный способ реализации паттерна 'Gate' — это использование стандартного узла `function` в связке с контекстом потока (`flow context`). Такой подход дает полный контроль над логикой и позволяет легко адаптировать 'Gate' под любую специфическую задачу.

Основной принцип:

  • Состояние 'Gate' (например, строка `'open'` или `'closed'`) хранится в переменной контекста потока. Мы используем `flow.context`, чтобы состояние было доступно всем узлам в рамках одной вкладки (flow).
  • Узел `function`, реализующий 'Gate', при получении сообщения сначала проверяет, является ли оно управляющим (например, команда "открыть" или "закрыть" 'Gate').
  • Если сообщение управляющее, узел изменяет значение переменной в контексте.
  • Если сообщение информационное (то, которое нужно пропустить или заблокировать), узел считывает текущее состояние 'Gate' из контекста и принимает решение о дальнейшей судьбе сообщения.
  • Начальная настройка и инициализация

    Критически важный шаг — это инициализация состояния 'Gate' при первом запуске потока или после перезагрузки контроллера. Без этого 'Gate' окажется в неопределенном состоянии.

    В коде узла `function` необходимо предусмотреть блок, который проверит наличие состояния в контексте и, если его нет, установит значение по умолчанию (например, `'closed'`).

    Вот базовый код для узла `function`, который реализует сам 'Gate' без логики управления:

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

    // Если переменная 'gateState' не существует (например, при первом запуске),

    // устанавливаем ей значение 'closed' по умолчанию.

    let gateState = flow.get('gateState') || 'closed';

    // Теперь, когда у нас есть состояние, мы просто проверяем его.

    // Если 'Gate' открыт, мы возвращаем исходное сообщение,

    // отправляя его на выход узла.

    if (gateState === 'open') {

    // Для отладки полезно отображать текущее состояние

    node.status({fill:"green", shape:"dot", text:"Open: msg passed"});

    return msg;

    } else {

    // Если 'Gate' закрыт, мы возвращаем null.

    // Это эффективно останавливает поток, и сообщение никуда дальше не идет.

    node.status({fill:"red", shape:"ring", text:"Closed: msg blocked"});

    return null;

    }

    Этот код является ядром паттерна. При поступлении любого `msg` он проверяет флаг `flow.gateState` и либо пропускает сообщение, либо отбрасывает его. Теперь нам нужно добавить способ изменять этот флаг.

    ---

    Управление состоянием 'Gate': команды 'open', 'close', 'toggle'

    Чтобы наш 'Gate' был полезен, нам нужен механизм для изменения его состояния. Стандартный подход — отправлять на тот же узел `function` специальные управляющие сообщения. Чтобы отличить их от обычных, информационных сообщений, мы будем использовать `msg.topic`.

    > ⚠️ Внимание: Критически важно чётко разделять 'управляющие' и 'информационные' (проходящие через 'Gate') сообщения. Используйте для этого выделенные топики (`msg.topic`) – например, `control` для команд и `data` для данных, которые должны быть отфильтрованы. Смешение логики, когда полезная нагрузка может быть и данными, и командой, часто приводит к непредсказуемому поведению потока и сложным для отладки ошибкам.

    Расширенный код узла `function`

    Дополним наш код, чтобы он мог обрабатывать команды `'open'`, `'close'` и `'toggle'`, передаваемые через `msg.payload` при `msg.topic === 'control'`.

    // 1. Инициализация состояния (как и раньше)
    

    let gateState = flow.get('gateState') || 'closed';

    // 2. Обработка управляющих сообщений

    // Проверяем, является ли это сообщение командой для 'Gate'

    if (msg.topic === 'control') {

    let command = msg.payload;

    switch (command) {

    case 'open':

    gateState = 'open';

    break;

    case 'close':

    gateState = 'closed';

    break;

    case 'toggle':

    // 'toggle' инвертирует текущее состояние

    gateState = (gateState === 'open') ? 'closed' : 'open';

    break;

    default:

    // Если пришла неизвестная команда, логируем ошибку

    node.warn("Unknown control command received: " + command);

    // Не меняем состояние

    break;

    }

    // Сохраняем новое (или старое) состояние в контекст

    flow.set('gateState', gateState);

    // Управляющее сообщение не должно проходить дальше. Останавливаем поток.

    return null;

    }

    // 3. Обработка информационных (проходящих) сообщений

    // Этот блок выполняется только если msg.topic !== 'control'

    if (gateState === 'open') {

    node.status({fill:"green", shape:"dot", text:"Open: msg passed at " + new Date().toLocaleTimeString()});

    // Сообщение проходит

    return msg;

    } else {

    node.status({fill:"red", shape:"ring", text:"Closed: msg blocked at " + new Date().toLocaleTimeString()});

    // Сообщение блокируется

    return null;

    }

    Пример потока для управления и тестирования

    Теперь соберем простой поток, чтобы увидеть 'Gate' в действии.

    ASCII-схема потока:
    // Управляющие инжекты
    

    [Inject: "open"] --+

    |

    [Inject: "close"]--+--> [Function: Set topic='control'] --+

    | |

    [Inject: "toggle"]-+ |

    v

    //------------ Главный узел "Gate" ----------------------------------

    [Function: Gate Logic (код выше)] --> [Debug: "Прошло через Gate"]

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

    ^

    |

    // Информационный инжект для теста

    [Inject: timestamp] --> [Function: Set topic='data'] -----+

    Настройка узлов:
  • Узлы `Inject` для управления:
  • * Три узла `Inject`. В каждом `msg.payload` устанавливается в строковое значение: `'open'`, `'close'`, `'toggle'`.

  • Узел `Function` "Set topic='control'":
  • * Этот узел нужен, чтобы назначить правильный топик управляющим сообщениям. Код:

            msg.topic = 'control';

    return msg;

  • Узел `Inject` для данных:
  • * Обычный `Inject`, отправляющий метку времени (`timestamp`).

  • Узел `Function` "Set topic='data'":
  • * Аналогично управляющему, но устанавливает топик для данных, чтобы они не были ошибочно приняты за команды. Код:

            msg.topic = 'data';

    return msg;

  • Главный узел `function` "Gate Logic":
  • * Вставьте сюда расширенный код, приведенный выше.

  • Узел `Debug`:
  • * Будет отображать только те сообщения, которые успешно прошли через 'Gate'.

    Теперь, если вы нажмете `Inject` "open", 'Gate' перейдет в открытое состояние (статус узла станет зеленым). Последующие нажатия на `Inject` с данными будут приводить к появлению сообщений в панели отладки. Если нажать `Inject` "close", 'Gate' закроется (статус станет красным), и сообщения с данными перестанут проходить. Команда "toggle" будет переключать его состояние.

    ---

    Практический кейс: режим 'Не беспокоить' для датчиков движения

    Рассмотрим реальную задачу на объекте автоматизации, например, в умной квартире.

    Задача: В гостиной установлен датчик движения, который включает основное освещение при обнаружении активности. Пользователь хочет иметь возможность временно отключать эту автоматизацию, например, когда смотрит кино, чтобы свет не включался от каждого движения. После окончания просмотра автоматизация должна легко восстанавливаться. Решение: Применить паттерн 'Gate' для потока, обрабатывающего данные с датчика движения.

    > 🔗 Связанный материал: Для корректной работы с MQTT и KNX в данном примере убедитесь, что вы изучили материалы из модулей COURSE-06-M03 (Основы MQTT) и COURSE-08-M02 (Интеграция с KNX). Понимание принципов работы этих протоколов является обязательным.

    Схема потока

    ASCII-схема:
    // Вход данных с датчика движения
    

    [mqtt in: "hi/living_room/motion/state"] --+

    |

    v

    //----------------- GATE ---------------------------------------------

    [Function: "Do Not Disturb Gate"] --> [knx-out: "Включить свет"]

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

    ^

    |

    // Управление режимом "Не беспокоить"

    [mqtt in: "hi/living_room/dnd/set"] --> [Function: "Control Gate"] --+

    Детали реализации

  • Датчик движения (Источник данных):
  • * Датчик движения (например, Zigbee-датчик, подключенный через шлюз) публикует свое состояние в MQTT-топик `hi/living_room/motion/state`.

    * Сообщение при обнаружении движения: `{"motion": true}`.

  • Управление режимом "Не беспокоить" (DND):
  • * В пользовательском интерфейсе (например, в мобильном приложении) есть переключатель "Не беспокоить".

    * При его включении приложение публикует в MQTT-топик `hi/living_room/dnd/set` сообщение с `payload` = `'ON'`.

    * При выключении — `payload` = `'OFF'`.

  • Логика в Node-RED:
  • * Узел `mqtt in` "DND Control": Подписан на топик `hi/living_room/dnd/set`.

    * Узел `function` "Control Gate": Преобразует команды `'ON'`/`'OFF'` в команды для 'Gate'.

            // Преобразуем ON/OFF в наши стандартные команды open/close

    if (msg.payload === 'ON') {

    msg.payload = 'close'; // Режим DND включен -> Gate закрыт

    } else if (msg.payload === 'OFF'){

    msg.payload = 'open'; // Режим DND выключен -> Gate открыт

    }

    msg.topic = 'control';

    return msg;

    * Узел `function` "Do Not Disturb Gate": На вход этого узла приходят два типа сообщений: управляющие от переключателя DND и информационные от датчика движения. Код внутри узла — это наш стандартный код 'Gate' из предыдущего раздела. Когда приходит `msg.topic === 'control'`, он меняет состояние. Когда приходит сообщение от датчика движения, он его либо пропускает, либо блокирует.

    * Узел `knx-out` "Включить свет": Если сообщение от датчика движения успешно прошло через открытый 'Gate', оно попадает на этот узел, который отправляет команду на включение света в шину KNX на соответствующий групповой адрес.

    Преимущества такого подхода:

    ---

    Итоги и лучшие практики использования 'Gate'

    Паттерн 'Gate' — это мощный, но простой в своей основе программный переключатель, который позволяет вносить в потоки автоматизации управляемость и гибкость. Он является незаменимым инструментом для создания систем, которые адаптируются к поведению пользователя и внешним условиям.

    📋 Ключевые выводы:

    Лучшие практики:

  • Всегда инициализируйте состояние: Чтобы избежать неопределенности после развертывания или перезагрузки контроллера HI, всегда предусматривайте в коде 'Gate' установку начального состояния по умолчанию (например, `let gateState = flow.get('gateState') || 'closed';`). Это гарантирует предсказуемое поведение системы.
  • Используйте глобальный контекст для глобальных 'Gate': Если один 'Gate' должен управлять поведением сценариев на нескольких разных вкладках (flows), храните его состояние в глобальном контексте (`global.context`). Например, глобальный режим «Отпуск» должен блокировать потоки и в гостиной, и на кухне, и в системе полива.
  • * Чтение: `let vacationMode = global.get('vacationMode') || 'off';`

    * Запись: `global.set('vacationMode', 'on');`

  • Используйте персистентный контекст: Для критически важных 'Gate', состояние которых должно сохраняться даже после полного отключения питания контроллера, настройте персистентное хранилище контекста. На платформе HI это можно сделать, указав в файле `settings.js` сохранение контекста в файловой системе или в базе данных MySQL.
  • Рассмотрите готовые альтернативы для простых задач: Для самых простых сценариев, не требующих сложной логики, можно использовать готовые узлы из палитры Node-RED. Например, `node-red-contrib-simple-gate`. Он предоставляет базовый функционал 'Gate' без необходимости писать JavaScript-код. Однако он менее гибок и не позволяет реализовать сложные управляющие команды или кастомную логику, поэтому для профессиональных инсталляций рекомендуется использовать собственную реализацию на базе узла `function`.
  • Что дальше

    В этом уроке мы детально разобрали паттерн 'Gate' и научились применять его для создания управляемых и гибких систем автоматизации. В следующем уроке мы перейдем к изучению не менее важного паттерна 'Конечный автомат' (Finite State Machine), который позволяет элегантно управлять объектами со сложным поведением и множеством состояний.