Паттерн 'Gate': блокировка/разрешение прохождения сообщений
Введение в паттерн 'Gate': фильтрация и контроль потоков
Паттерн 'Gate' (в переводе — «Ворота» или «Шлюз») представляет собой один из наиболее важных и часто используемых механизмов в проектировании сложных потоков Node-RED. Его основная задача — динамически управлять прохождением сообщений (`msg`) через определенный участок логической цепи.
> 💡 Подсказка: Паттерн 'Gate' — это программный аналог шлагбаума на пути данных. В «открытом» состоянии он беспрепятственно пропускает все проходящие через него сообщения. В «закрытом» — полностью блокирует их, не давая потоку выполняться дальше. Это позволяет в любой момент взять управление автоматизацией на себя, игнорируя стандартную логику без необходимости переразвертывания (re-deploy) потоков.
Ключевая ценность этого паттерна заключается в его способности отделить основную логику автоматизации от логики управления ее состоянием.
📋 Ключевые сценарии использования:
- Режим обслуживания (Maintenance Mode): Глобальное отключение всех автоматических сценариев на время проведения пусконаладочных или ремонтных работ. Например, чтобы случайное срабатывание датчика не включило насос, пока инженер работает с ним.
- Активация сложных сцен: Включение режима «Кино» в умном доме, который блокирует реакцию света на движение, но пропускает команды с пульта.
- Предотвращение конфликтов автоматизации: В системе климат-контроля 'Gate' может блокировать работу кондиционера на охлаждение, если в данный момент активен сценарий отопления.
- Ручное управление (Manual Override): Предоставление пользователю возможности временно отключить автоматику (например, автополив газона перед приходом гостей) через кнопку в интерфейсе.
- Режим "Отпуск": Перевод дома в экономичный режим, при котором большинство фоновых автоматизаций (кроме безопасности и имитации присутствия) блокируются одним 'Gate'.
Важно понимать фундаментальное отличие паттерна 'Gate' от стандартного узла `switch`. Узел `switch` является маршрутизатором: он анализирует сообщение и, в зависимости от его содержимого, направляет его по одному из нескольких возможных путей. 'Gate' же является фильтром: он не меняет направление потока, а принимает бинарное решение — пропустить сообщение дальше по единственному пути или полностью остановить его обработку.
| Критерий | Узел `switch` | Паттерн `Gate` |
| ------------------ | ------------------------------------------------- | -------------------------------------------------------- |
| Назначение | Маршрутизация сообщений | Блокировка/разрешение прохождения сообщений |
| Количество выходов | Множество (по одному на каждое правило) | Один (для пропущенных сообщений) |
| Логика работы | "Если `msg.payload` равно X, отправить сюда" | "Если 'ворота' открыты, пропустить `msg` дальше" |
| Состояние | Не имеет собственного состояния (stateless) | Имеет состояние (открыто/закрыто), хранимое в контексте |
| Основной вопрос| "Куда должно пойти это сообщение?" | "Должно ли это сообщение вообще идти дальше?" |
Освоение этого паттерна позволяет создавать гибкие и легко управляемые системы, которые адекватно реагируют на изменение внешних условий и требований пользователя.
---
Реализация 'Gate' с помощью узла `function` и контекста
Наиболее гибкий и мощный способ реализации паттерна 'Gate' — это использование стандартного узла `function` в связке с контекстом потока (`flow context`). Такой подход дает полный контроль над логикой и позволяет легко адаптировать '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`. В каждом `msg.payload` устанавливается в строковое значение: `'open'`, `'close'`, `'toggle'`.
* Этот узел нужен, чтобы назначить правильный топик управляющим сообщениям. Код:
msg.topic = 'control';
return msg;
* Обычный `Inject`, отправляющий метку времени (`timestamp`).
* Аналогично управляющему, но устанавливает топик для данных, чтобы они не были ошибочно приняты за команды. Код:
msg.topic = 'data';
return msg;
* Вставьте сюда расширенный код, приведенный выше.
* Будет отображать только те сообщения, которые успешно прошли через '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}`.
* В пользовательском интерфейсе (например, в мобильном приложении) есть переключатель "Не беспокоить".
* При его включении приложение публикует в MQTT-топик `hi/living_room/dnd/set` сообщение с `payload` = `'ON'`.
* При выключении — `payload` = `'OFF'`.
* Узел `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 на соответствующий групповой адрес.
Преимущества такого подхода:- Централизация управления: Мы можем управлять режимом DND из любого места системы (мобильное приложение, настенная панель, голосовой ассистент), просто отправив сообщение в один MQTT-топик.
- Инкапсуляция логики: Логика датчика движения и логика управления светом остаются неизменными. Мы просто вставили между ними "кран", которым можем перекрывать поток данных.
- Масштабируемость: Если у нас появится еще один датчик в этой же комнате, мы можем направить его сообщения через тот же 'Gate', и он также будет подчиняться режиму DND.
---
Итоги и лучшие практики использования 'Gate'
Паттерн 'Gate' — это мощный, но простой в своей основе программный переключатель, который позволяет вносить в потоки автоматизации управляемость и гибкость. Он является незаменимым инструментом для создания систем, которые адаптируются к поведению пользователя и внешним условиям.
📋 Ключевые выводы:
- Назначение: 'Gate' не маршрутизирует, а фильтрует поток сообщений, либо пропуская, либо блокируя их.
- Реализация: Самый гибкий способ — узел `function` с хранением состояния (`'open'`/`'closed'`) в контексте.
- Управление: Реализуется через специальные управляющие сообщения, которые отличаются от информационных по `msg.topic`.
- Визуализация: Обязательно используйте API `node.status()` для отображения текущего состояния 'Gate' прямо в редакторе — это многократно упрощает отладку.
Лучшие практики:
* Чтение: `let vacationMode = global.get('vacationMode') || 'off';`
* Запись: `global.set('vacationMode', 'on');`
Что дальше
В этом уроке мы детально разобрали паттерн 'Gate' и научились применять его для создания управляемых и гибких систем автоматизации. В следующем уроке мы перейдем к изучению не менее важного паттерна 'Конечный автомат' (Finite State Machine), который позволяет элегантно управлять объектами со сложным поведением и множеством состояний.