Контракт сообщения для управления
Введение в контракт сообщения: Зачем он нужен?
В предыдущих уроках мы рассмотрели базовые шаблоны управления исполнительными устройствами: «Включено/Выключено» (On/Off), «Импульс» (Pulse) и «На заданное время» (Timed). Каждый из них решал свою задачу, но как объединить их в единую, надежную и понятную систему? Как сделать так, чтобы любой инженер, взглянув на ваш проект, мог мгновенно понять, как управлять тем или иным устройством? Ответ кроется в стандартизации, и для систем на базе Node-RED таким стандартом является контракт сообщения (Message Contract).
> 💡 Подсказка: Думайте о контракте как о "паспорте" устройства: в нем четко прописано, какие команды оно понимает и в каком формате.
Контракт сообщения — это формальное соглашение о структуре и содержании объекта `msg`, который передается между узлами в Node-RED. Это набор правил, определяющих, как должна выглядеть команда для управления нагрузкой. Без такого контракта ваша система автоматизации рискует превратиться в хаос:- В одном потоке для включения света вы отправляете булево значение `true`.
- В другом — строку `"ON"`.
- В третьем — число `1`.
Каждый из этих подходов работает по отдельности, но вместе они создают путаницу, усложняют отладку и делают систему хрупкой. Любое изменение требует анализа всей цепочки, чтобы понять, какой именно формат данных "сломался".
Контракт сообщения решает эти проблемы, привнося в проект три ключевых преимущества:
По своей сути, контракт сообщения в Node-RED — это аналог API-контракта в веб-разработке. Когда вы используете API какого-либо сервиса, вы не гадаете, как отправить запрос. Вы открываете документацию и видите четкие правила: какой использовать URL (адрес), какой метод (GET/POST), какую структуру данных передать в теле запроса (JSON). Наш контракт сообщения выполняет ту же функцию, объединяя ранее изученные нами шаблоны в единую стройную систему. Он формализует способ, которым мы отдаем команды для реализации логики On/Off, Pulse и Timed, делая наши потоки профессиональными, надежными и готовыми к росту.
---
Структура базового контракта: topic и payload
Основа любого контракта сообщения в Node-RED — это грамотное разделение ответственности между двумя ключевыми свойствами объекта `msg`: `msg.topic` и `msg.payload`. Неправильное их использование является одной из самых частых ошибок начинающих инсталляторов.
Принцип разделения очень прост:
- `msg.topic` — Адрес. Он отвечает на вопрос «КУДА?» или «ЧТО?». Топик используется для маршрутизации сообщения, идентификации устройства или системы, к которой относится команда.
- `msg.payload` — Команда или Значение. Он отвечает на вопрос «ЧТО СДЕЛАТЬ?» или «КАКОЕ ЗНАЧЕНИЕ ПЕРЕДАТЬ?». Это непосредственно сама команда: включить, выключить, установить значение.
Адресация через `msg.topic`
Чтобы система была легко читаемой и масштабируемой, рекомендуется использовать иерархическую структуру топиков. Это особенно важно при работе с протоколом MQTT, но является хорошей практикой и внутри Node-RED для фильтрации потоков с помощью узла `Switch`.
Рекомендуемый формат: `место/система/устройство/действие`
- `office/lighting/main-group/set` — Установить состояние для основной группы освещения в офисе.
- `cottage/gates/main/command` — Отправить команду на главные ворота в коттедже.
- `hotel-room-101/climate/fan-speed/set` — Установить скорость вентилятора в номере 101.
Такая структура позволяет легко фильтровать сообщения. Например, в узле `Switch` вы можете настроить правило "starts with `office/lighting/`", чтобы направить все команды, связанные с освещением в офисе, в один управляющий поток.
Команда через `msg.payload`
Теперь, когда мы определились с адресом, нужно передать саму команду. Для базовых шаблонов управления, которые мы изучили, оптимально использовать простые типы данных. Это делает логику максимально прозрачной.
| Шаблон управления | Рекомендуемый тип `msg.payload` | Пример `msg.payload` | Описание |
| :---------------- | :------------------------------ | :------------------- | :------------------------------------------------------------------------------------------------------------------------- |
| On/Off | `boolean` | `true` / `false` | Самый прямой и надежный способ. `true` — включить, `false` — выключить. Однозначно и не требует дополнительных проверок. |
| Pulse | `string` | `"pulse"` | Специальная строковая команда, которая говорит логике: "сгенерируй импульс". Длительность импульса заранее задана в потоке. |
| Timed | `number` | `300` | Число, обозначающее длительность работы в секундах. Команда "включить на 300 секунд". 0 может означать "выключить". |
Вот как это выглядит на практике. Представим, что мы хотим управлять освещением на кухне (`kitchen/lighting/main/set`):
- Включить свет:
{
"topic": "kitchen/lighting/main/set",
"payload": true
}
- Выключить свет:
{
"topic": "kitchen/lighting/main/set",
"payload": false
}
- Включить свет на 5 минут (300 секунд):
{
"topic": "kitchen/lighting/main/set",
"payload": 300
}
- Кратковременно моргнуть светом (например, для индикации):
{
"topic": "kitchen/lighting/main/set",
"payload": "pulse"
}
Используя этот простой контракт, мы уже можем построить универсальный поток, который будет обрабатывать все четыре сценария для одной и той же группы света, просто анализируя тип и значение `msg.payload`. Это фундамент для создания по-настоящему гибких и переиспользуемых компонентов автоматизации.
---
Расширенный контракт: управление через структурированный payload
Базовый контракт с простыми типами данных в `msg.payload` отлично подходит для 80% задач. Однако по мере усложнения сценариев его возможностей становится недостаточно. Что если мы хотим передать не одну, а сразу несколько инструкций? Например, включить привод на определенное время, но только если он сейчас выключен? Или сгенерировать импульс не фиксированной, а динамической длительности?
Для таких задач используется расширенный контракт, где `msg.payload` представляет собой не простое значение, а структурированный JSON-объект. Этот подход открывает практически безграничные возможности для управления.
> ⚠️ Внимание: При использовании объектов в `payload` всегда проверяйте наличие обязательных полей (например, `state`), чтобы избежать ошибок в логике. Используйте узел `switch` или `function` для валидации.
Переход от простого `payload` к объекту
Основная идея — инкапсулировать команду и ее параметры внутри одного объекта. Это делает сообщение самодостаточным и легко расширяемым в будущем.
Рассмотрим усовершенствование наших базовых шаблонов.
Шаблон 'Timed' с явным указанием состояния
В базовом контракте мы отправляли в `msg.payload` число `300`, что означало "включить на 300 секунд". Но эта команда неявная. Что, если мы хотим иметь возможность принудительно выключить нагрузку до истечения таймера? Структурированный `payload` решает эту проблему элегантно.
Команда на включение на 5 минут (300 секунд):
{
"topic": "living-room/blinds/main/set",
"payload": {
"state": true,
"duration_sec": 300
}
}
- `"state": true` — Явно указывает, что действие — включить.
- `"duration_sec": 300` — Указывает длительность.
Команда на немедленное выключение, прерывающая любой таймер:
{
"topic": "living-room/blinds/main/set",
"payload": {
"state": false
}
}
Теперь наша логика может четко различать эти два случая. Если `payload` содержит `state: false`, мы немедленно выключаем нагрузку. Если `state: true` и есть `duration_sec`, мы запускаем таймер.
Шаблон 'Pulse' с динамической длительностью
В базовом контракте команда `"pulse"` запускала импульс заранее заданной длительности. Расширенный контракт позволяет задавать эту длительность прямо в команде.
Команда на генерацию импульса длительностью 500 миллисекунд (например, для короткого нажатия кнопки на приводе ворот):
{
"topic": "garage/gate-opener/command",
"payload": {
"pulse_ms": 500
}
}
А для "длинного нажатия" (например, для входа в режим программирования) мы можем отправить:
{
"topic": "garage/gate-opener/command",
"payload": {
"pulse_ms": 5000
}
}
Важность валидации
Работа со структурированным `payload` требует повышенного внимания к валидации. Перед тем как пытаться прочитать свойство `msg.payload.state` или `msg.payload.pulse_ms`, вы должны убедиться, что:
Простейшую валидацию можно выполнить в узле `Function`:
// Пример валидации для Timed шаблона
if (typeof msg.payload !== 'object' || typeof msg.payload.state !== 'boolean') {
node.error("Invalid message contract: payload is not an object or 'state' is missing", msg);
return null; // Прервать выполнение потока
}
// Теперь мы можем безопасно работать с msg.payload.state
let state = msg.payload.state;
let duration = msg.payload.duration_sec || 0; // Используем 0, если длительность не указана
// ... дальнейшая логика ...
Использование структурированных `payload` — это шаг от простого инсталлятора к архитектору систем автоматизации. Он позволяет создавать чрезвычайно гибкие, мощные и, что самое главное, понятные и поддерживаемые сценарии.
---
Практический пример: универсальный субпоток управления
Теория контрактов сообщений обретает реальную силу, когда мы применяем ее для создания переиспользуемых компонентов. Давайте объединим все наши знания и создадим универсальный субпоток (Subflow), который сможет управлять любой нагрузкой, понимая все рассмотренные нами варианты контрактов — от простого булева значения до сложного JSON-объекта.
> 🔗 Связанный материал: Подробно принципы создания и конфигурации субпотоков разбираются в модуле COURSE-08-M01.
Идея состоит в том, чтобы создать "черный ящик", на вход которого мы подаем сообщение, соответствующее нашему контракту, а он сам решает, какую логику применить: On/Off, Pulse или Timed.
Структура субпотока
Наш субпоток будет иметь один вход и один выход (для передачи команды на физический узел реле). Внутренняя логика будет выглядеть следующим образом:
+---------------------------------+
[Вход] --> | Switch: "Маршрутизатор команд" | -- (is boolean) ----> [Логика On/Off] -----+--> [Выход]
+---------------------------------+ |
| |
+-- (is number) ------> [Логика Timed] ------+--> [Выход] |
| | |
+-- (is string 'pulse') > [Логика Pulse] ----+ |
| |
+-- (is object) ------> [Function: "Парсер JSON"] -> [Разные логики] -> ...
* Правило 1: `is` `boolean` -> Выход 1 (для On/Off)
* Правило 2: `is` `number` -> Выход 2 (для Timed)
* Правило 3: `is` `string` и `==` `"pulse"` -> Выход 3 (для Pulse)
* Правило 4: `is` `object` -> Выход 4 (для расширенного контракта)
* `Trigger` настраивается на отправку `true`, затем ожидание (время берется из `msg.payload`) и отправку `false`.
* `msg.payload` будет динамически управлять задержкой узла `Trigger`.
Код для узла `Function` ("Парсер JSON")
Этот узел будет обрабатывать сообщения, отправленные на 4-й выход маршрутизатора.
const payload = msg.payload;
// Проверяем на наличие команды "pulse" с динамической длительностью
if (typeof payload.pulse_ms === 'number' && payload.pulse_ms > 0) {
// Если есть 'pulse_ms', переформатируем сообщение для узла Trigger
// и отправляем его на 2-й выход функции.
// Узел Trigger должен быть настроен на получение задержки из msg.delay
msg.payload = true; // Команда на включение
msg.reset = true; // Сбросить предыдущий таймер
msg.delay = payload.pulse_ms; // Динамическая задержка
// Мы можем использовать несколько выходов у Function node,
// чтобы направить сообщение на разные ветки Trigger'ов
return [null, msg]; // Отправляем на выход 2, предназначенный для Trigger'a
}
// Проверяем на наличие команды "timed" с явным состоянием
if (typeof payload.state === 'boolean') {
if (payload.state === true && typeof payload.duration_sec === 'number' && payload.duration_sec > 0) {
// Команда "включить на время"
msg.payload = true;
msg.reset = true;
msg.delay = payload.duration_sec * 1000; // Переводим секунды в мс
return [null, msg]; // Отправляем на выход 2 (для Trigger'a)
} else {
// Команда "включить/выключить" без таймера
msg.payload = payload.state;
return [msg, null]; // Отправляем на выход 1 (прямое управление)
}
}
// Если ни один контракт не подошел, логируем ошибку
node.warn("Unsupported object contract in payload: " + JSON.stringify(payload));
return null;
Обратите внимание, что для реализации такой логики узел `Function` может иметь несколько выходов, которые далее соединяются с нужными узлами `Trigger` или напрямую с выходом субпотока.
Гибкость в применении
Создав такой субпоток один раз, вы можете использовать его для управления десятками устройств на объекте.
- Настенный выключатель будет отправлять `true`/`false`.
- Кнопка в интерфейсе Home Assistant сможет отправлять `{ "pulse_ms": 500 }`.
- Сценарий "никого нет дома" отправит `false` на все субпотоки управления светом.
- Датчик протечки может отправить команду `{ "state": true, "duration_sec": 60 }` на привод закрытия клапана, который сам вернется в исходное состояние через минуту, если команда не будет послана повторно (принцип Watchdog).
Вы управляете одним и тем же компонентом из разных мест, используя наиболее удобный для каждого случая формат команды. Вся сложность логики инкапсулирована внутри субпотока, делая основной флоу чистым и читаемым.
---
Итоги и преимущества стандартизации
В этом уроке мы сделали важный шаг от написания отдельных потоков к проектированию целостной и надежной системы. Мы ввели ключевое понятие контракта сообщения — фундаментального соглашения о том, как компоненты вашей системы автоматизации общаются друг с другом.
Давайте подведем итоги:
- Контракт сообщения — это стандарт, определяющий структуру объекта `msg` для обеспечения предсказуемого и надежного взаимодействия между узлами. Он является аналогом API в мире Node-RED.
- Ключевыми компонентами контракта являются `msg.topic` и `msg.payload`. Мы усвоили принцип разделения ответственности:
* `msg.payload` используется для передачи самой команды (что нужно сделать).
- Мы рассмотрели два уровня контракта:
* Расширенный: использует JSON-объекты в `payload`, что позволяет передавать сложные, многопараметрические команды и создавать очень гибкие сценарии.
Применение стандартизированного контракта на ваших объектах дает четыре неоспоримых преимущества, которые напрямую влияют на качество вашей работы и репутацию как инженера:
Что дальше?
Теперь, когда у нас есть надежный инструмент для отправки команд, следующим логическим шагом будет научиться управлять устройствами, которые требуют взаимной блокировки для безопасной работы. В следующем уроке мы рассмотрим шаблон "Интерлок" (Interlock), который необходим для управления реверсивными двигателями (например, приводами штор, ворот, окон), где одновременное включение двух реле может привести к выходу оборудования из строя. Мы будем использовать наш контракт сообщений для отправки безопасных команд "открыть" и "закрыть".