Практика: Создание Flow для переключения Home/Away
Введение и архитектура Flow
> 🔗 Связанный материал: Перед началом работы убедитесь, что вы изучили материалы по проектированию графа состояний (урок COURSE-07-M02-L01) и типам триггеров (урок COURSE-07-M02-L02). Это основа для понимания логики, реализуемой в этом уроке.
Цель данного практического занятия — консолидировать полученные знания и создать законченный, отказоустойчивый и масштабируемый поток (Flow) в Node-RED для управления базовыми режимами системы умного дома: «Дома» (Home) и «Нет дома» (Away). Мы пройдем полный цикл от получения команды на смену режима до фиксации нового состояния и его трансляции по всей системе.
В основе нашего решения лежит принцип централизации логики. Вместо того чтобы разбрасывать логику переключения режимов по разным потокам, мы создадим единый «мозговой центр», отвечающий исключительно за эту задачу. Такой подход обладает рядом неоспоримых преимуществ:
- Предсказуемость: Состояние системы меняется только в одном, строго определенном месте. Это исключает «гонки состояний» и конфликты, когда два разных потока одновременно пытаются изменить режим.
- Простота отладки: При возникновении проблем с режимами вам потребуется анализировать только один, хорошо структурированный поток.
- Легкость модификации: Добавление нового режима или нового триггера не потребует переделки всей системы, а сведется к локальным изменениям в центральном Flow.
Архитектура нашего потока будет состоять из следующих ключевых узлов Node-RED:
| Узел (`Node`) | Назначение в нашем Flow |
| :------------ | :---------------------- |
| `mqtt in` | Является основной точкой входа для команд. Подписывается на топик `hi/system/mode/set` и принимает команды от внешних систем (мобильное приложение, геофенсинг, голосовой ассистент). |
| `inject` | Инструмент для ручного тестирования и отладки. Позволяет эмулировать отправку команд `Home` и `Away` напрямую в интерфейсе Node-RED без использования внешних триггеров. |
| `function` | Ядро нашего потока. Здесь будет реализована вся логика конечного автомата (State Machine): получение текущего состояния, проверка на необходимость смены, обновление состояния и формирование исходящего сообщения. |
| `mqtt out` | Публикует новое, установленное состояние системы в отдельный статусный топик (`hi/system/mode/status`). Это позволяет другим подсистемам (освещение, климат, безопасность) реагировать на смену режима. |
| `debug` | Незаменимый инструмент для отладки. Мы разместим узлы `debug` в ключевых точках потока, чтобы в реальном времени отслеживать, как трансформируется объект `msg` на каждом шаге. |
Фактически, мы реализуем программную модель графа состояний, который был спроектирован в уроке COURSE-07-M02-L01. Напомню, что он состоит из двух состояний (`Home`, `Away`) и четко определенных переходов между ними.
Общая схема потока (ASCII):// Входные триггеры
[mqtt in: .../mode/set] --+
\
[inject: "Home"] ----------+--> [function: Нормализация] --> [function: Главная логика] --> [mqtt out: .../mode/status] --> [debug]
/
[inject: "Away"] ---------+
Эта простая, но мощная архитектура станет основой для всех системных режимов вашего умного дома.
---
Шаг 1: Настройка триггеров входа
Первым шагом является создание точек входа для команд, инициирующих смену режима. Нам необходимо обеспечить прием команд из разных источников, но привести их к единому, нормализованному формату перед подачей в ядро логики.
Конфигурация MQTT-входа
Основным каналом управления режимами в экосистеме HI является протокол MQTT. Создадим узел `mqtt in`, который будет слушать команды.
Теперь любая система, опубликовав сообщение `"Home"` или `"Away"` в топик `hi/system/mode/set`, сможет инициировать смену режима. Вы можете добавить несколько таких узлов для разных источников, если хотите их логически разделить, например:
- `hi/triggers/geofence/mode/set` — от системы геопозиционирования.
- `hi/triggers/wallpanel/mode/set` — от настенной панели управления.
Все эти узлы `mqtt in` должны быть соединены с одним и тем же следующим узлом — нормализатором.
Использование Inject для ручного тестирования
Для отладки и тестирования крайне неудобно каждый раз использовать внешнее приложение. Добавим узлы `inject` для эмуляции команд.
* `Payload`: `string`
* Значение: `Home`
* `Topic`: `manual/home` (полезно для отслеживания источника)
* `Name`: "SET: Home"
* `Payload`: `string`
* Значение: `Away`
* `Topic`: `manual/away`
* `Name`: "SET: Away"
Соедините выходы всех узлов `mqtt in` и `inject` в одну точку.
Нормализация входящих сообщений
Нельзя гарантировать, что все источники будут отправлять данные в одинаковом формате. Кто-то пришлет `"Home"`, кто-то — `{"mode": "Home"}`, а кто-то — `"home"` в нижнем регистре. Наша логика должна быть защищена от সুবিধят этого. Создадим простой `function`-узел для нормализации.
// Входящее сообщение может быть строкой или объектом
let input = msg.payload;
let newMode = null;
if (typeof input === 'string') {
// Если пришла строка, приводим к нужному регистру
if (input.toLowerCase() === 'home') {
newMode = 'Home';
} else if (input.toLowerCase() === 'away') {
newMode = 'Away';
}
} else if (typeof input === 'object' && input !== null && input.hasOwnProperty('mode')) {
// Если пришел объект, извлекаем свойство 'mode'
if (typeof input.mode === 'string') {
if (input.mode.toLowerCase() === 'home') {
newMode = 'Home';
} else if (input.mode.toLowerCase() === 'away') {
newMode = 'Away';
}
}
}
// Если не удалось распознать команду, логируем ошибку и останавливаем поток
if (newMode === null) {
node.warn(`Нераспознанная команда для смены режима: ${JSON.stringify(input)}`);
return null;
}
// Перезаписываем msg.payload нормализованным значением
msg.payload = newMode;
msg.source = msg.topic || 'unknown'; // Сохраняем источник команды для логирования
return msg;
Теперь, независимо от формата входящих данных, на выход этого узла будет поступать сообщение, где `msg.payload` — это строго `"Home"` или `"Away"`, а все остальное отбрасывается.
---
Шаг 2: Реализация логики переключения в Function-ноде
Это сердце нашей системы. Здесь происходит основная магия: проверка текущего состояния и принятие решения о его изменении.
> 💡 Подсказка: Используйте `global` контекст вместо `flow` для хранения системного режима. Это гарантирует, что переменная `system_mode` будет доступна во всех вкладках (flows) вашего проекта в Node-RED, что является стандартом для глобальных состояний системы.
Создание основной Function-ноды
// 1. Получаем запрашиваемый режим из входящего сообщения
const newMode = msg.payload;
const source = msg.source || 'undefined';
// 2. Получаем ТЕКУЩИЙ режим из глобального контекста.
// Если переменная еще не установлена (первый запуск), по умолчанию ставим 'Away'.
// Это безопасное значение по умолчанию.
const currentMode = global.get('system_mode', 'Away');
// 3. Проверка на идентичность состояний.
// Если система УЖЕ находится в запрашиваемом режиме, ничего не делаем.
// Это защищает от "флапа" и ненужных повторных срабатываний сценариев.
if (newMode === currentMode) {
node.status({ fill: "blue", shape: "ring", text: `Режим уже '${currentMode}'. Без изменений.` });
return null; // Останавливаем поток
}
// 4. Если режимы отличаются, выполняем переход.
// Атомарно обновляем состояние в глобальном контексте.
global.set('system_mode', newMode);
// 5. Формируем богатое исходящее сообщение по "Контракту сообщения".
// Оно будет содержать всю необходимую информацию для других систем.
msg.payload = {
mode: newMode, // Новый установленный режим
previous_mode: currentMode, // Предыдущий режим
source: source, // Источник, инициировавший смену
timestamp: Date.now() // Временная метка смены режима
};
// 6. Обновляем визуальный статус узла для быстрой диагностики.
node.status({ fill: "green", shape: "dot", text: `Переход: ${currentMode} -> ${newMode}` });
// 7. Отправляем сообщение дальше по потоку
return msg;
Разберем ключевые моменты этого кода:
- `global.get('system_mode', 'Away')`: Мы читаем переменную `system_mode` из глобального хранилища. Второй аргумент, `'Away'`, — это значение по умолчанию. Оно будет использовано, если переменная еще не существует (например, после первого запуска контроллера). Выбор `'Away'` как начального состояния является безопасным, так как он предполагает, что в доме никого нет и все лишние потребители должны быть выключены.
- `if (newMode === currentMode)`: Это критически важная проверка. Она предотвращает повторное выполнение всей цепочки сценариев, если, например, система геопозиционирования отправит команду "Away" дважды. Это экономит ресурсы контроллера и предотвращает нежелательные эффекты (например, повторное мигание светом).
- `global.set('system_mode', newMode)`: Это единственная строка, которая фактически меняет состояние системы. Она атомарна и гарантирует, что новое значение будет доступно всем остальным потокам немедленно.
- Формирование `msg.payload`: Мы не просто передаем дальше строку `"Home"`. Мы создаем информативный JSON-объект. Это позволяет нижестоящим сценариям принимать более сложные решения. Например, сценарий безопасности может активироваться только при переходе в `Away` с источником `geofence`, но не `manual-test`.
---
Шаг 3: Интеграция с global.HI_STATE
Вместо того чтобы хранить состояния только в разрозненных переменных, мы интегрируем логику с единым объектом `global.HI_STATE`. Это обеспечивает централизованный доступ к актуальной информации обо всей системе и позволяет избежать необходимости управлять десятками независимых топиков.
Добавьте после основной Function-ноды новый узел `function` с названием "Обновление HI_STATE" и следующим кодом:
// Получаем актуальный снимок глобального состояния системы
let hiState = global.get('HI_STATE') || {};
// Убеждаемся, что системный раздел существует
if (!hiState.system) {
hiState.system = {};
}
// Извлекаем данные о новом режиме из предыдущего узла
const currentStateInfo = msg.payload;
// Обновляем параметры внутри глобального объекта
hiState.system.mode = currentStateInfo.mode;
hiState.system.previous_mode = currentStateInfo.previous_mode;
hiState.system.last_mode_change = currentStateInfo.timestamp;
hiState.system.last_trigger_source = currentStateInfo.source;
// Сохраняем обновленный объект в глобальный контекст
global.set('HI_STATE', hiState);
// Передаем обновленный глобальный объект дальше для публикации
msg.payload = hiState;
return msg;
Обновление объекта `HI_STATE` гарантирует, что любой сценарий в Node-RED может получить актуальный режим («Дома» или «Нет дома»), просто прочитав ветку `global.get('HI_STATE').system.mode`.
---
Шаг 4: Публикация состояния и отладка
Вместо использования локального паттерна `set` / `status` и трансляции изменений через отдельные топики (вроде `hi/system/mode/status`), мы будем транслировать весь обновленный объект состояний целиком.
Конфигурация `mqtt out` для глобального стейта
* Server: Ваш MQTT-брокер.
* Topic: `hi/state`. Мы публикуем глобальный объект в единый корневой топик состояний.
* QoS: `1`.
* Retain: `true`.
* Name: "Публикация HI_STATE (Broadcast)".
Установка флага retain (удержание) в `true` является критически важной. Она говорит MQTT-брокеру сохранить последнее опубликованное в этот топик сообщение. Когда новое устройство, дашборд или сторонний сервис подпишется на `hi/state`, оно немедленно получит полный актуальный снимок системы (включая режим Home/Away), а не будет запрашивать данные по кусочкам. Это делает архитектуру максимально предсказуемой.
Комплексная отладка
Чтобы убедиться, что интеграция с `HI_STATE` работает корректно, используем узлы `debug`.
Теперь проведите полный тест-кейс:
- Нажмите на `inject`-узел "SET: Home". В окне отладки вы должны увидеть:
2. Сообщение от второго `debug` с объектом, где `msg.payload` теперь представляет собой полный объект `HI_STATE` с обновленным блоком `{ system: { mode: "Home", ... } }`.
3. Статус основной `function`-ноды должен измениться на `Переход: Away -> Home`.
- Нажмите на `inject`-узел "SET: Home" еще раз.
* Статус `function`-ноды изменится на `Режим уже 'Home'. Без изменений.`.
- Нажмите на `inject`-узел "SET: Away". Процесс переключения повторится, и в объекте `HI_STATE` режим изменится на "Away".
---
Итоги и следующие шаги
тоги и следующие шаги
В рамках этого урока мы создали надежный, централизованный и персистентный Flow для