ГлавнаяАкадемияСценарии умного дома: режимы, состояния, приоритеты → Практика: Создание Flow для переключения Home/Away

Практика: Создание Flow для переключения Home/Away

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

Введение и архитектура Flow

> 🔗 Связанный материал: Перед началом работы убедитесь, что вы изучили материалы по проектированию графа состояний (урок COURSE-07-M02-L01) и типам триггеров (урок COURSE-07-M02-L02). Это основа для понимания логики, реализуемой в этом уроке.

Цель данного практического занятия — консолидировать полученные знания и создать законченный, отказоустойчивый и масштабируемый поток (Flow) в Node-RED для управления базовыми режимами системы умного дома: «Дома» (Home) и «Нет дома» (Away). Мы пройдем полный цикл от получения команды на смену режима до фиксации нового состояния и его трансляции по всей системе.

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

Архитектура нашего потока будет состоять из следующих ключевых узлов 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`, который будет слушать команды.

  • Перетащите на холст узел `mqtt in`.
  • Откройте его настройки.
  • Server: Выберите ваш MQTT-брокер (обычно настроен как `localhost:1883`).
  • Topic: Укажите топик для получения команд. Согласно нашей стандартной иерархии, это будет `hi/system/mode/set`.
  • QoS: Установите `1` — At least once. Это гарантирует доставку команды, даже если были кратковременные проблемы со связью.
  • Output: `a UTF-8 string`. Это упростит начальную обработку.
  • Name: "Команды управления режимом".
  • Теперь любая система, опубликовав сообщение `"Home"` или `"Away"` в топик `hi/system/mode/set`, сможет инициировать смену режима. Вы можете добавить несколько таких узлов для разных источников, если хотите их логически разделить, например:

    Все эти узлы `mqtt in` должны быть соединены с одним и тем же следующим узлом — нормализатором.

    Использование Inject для ручного тестирования

    Для отладки и тестирования крайне неудобно каждый раз использовать внешнее приложение. Добавим узлы `inject` для эмуляции команд.

  • Перетащите на холст два узла `inject`.
  • Настройка первого (`Home`):
  • * `Payload`: `string`

    * Значение: `Home`

    * `Topic`: `manual/home` (полезно для отслеживания источника)

    * `Name`: "SET: Home"

  • Настройка второго (`Away`):
  • * `Payload`: `string`

    * Значение: `Away`

    * `Topic`: `manual/away`

    * `Name`: "SET: Away"

    Соедините выходы всех узлов `mqtt in` и `inject` в одну точку.

    Нормализация входящих сообщений

    Нельзя гарантировать, что все источники будут отправлять данные в одинаковом формате. Кто-то пришлет `"Home"`, кто-то — `{"mode": "Home"}`, а кто-то — `"home"` в нижнем регистре. Наша логика должна быть защищена от সুবিধят этого. Создадим простой `function`-узел для нормализации.

  • Добавьте узел `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-ноды

  • Добавьте на холст еще один узел `function`. Назовите его "Логика FSM: Home/Away".
  • Соедините выход узла "Нормализация входа" с входом этого узла.
  • Поместите в него следующий код:
  • // 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;

    Разберем ключевые моменты этого кода:

    ---

    Шаг 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` для глобального стейта

  • Добавьте узел `mqtt out` и соедините его с выходом узла "Обновление HI_STATE".
  • Откройте его настройки:
  • * Server: Ваш MQTT-брокер.

    * Topic: `hi/state`. Мы публикуем глобальный объект в единый корневой топик состояний.

    * QoS: `1`.

    * Retain: `true`.

    * Name: "Публикация HI_STATE (Broadcast)".

    Установка флага retain (удержание) в `true` является критически важной. Она говорит MQTT-брокеру сохранить последнее опубликованное в этот топик сообщение. Когда новое устройство, дашборд или сторонний сервис подпишется на `hi/state`, оно немедленно получит полный актуальный снимок системы (включая режим Home/Away), а не будет запрашивать данные по кусочкам. Это делает архитектуру максимально предсказуемой.

    Комплексная отладка

    Чтобы убедиться, что интеграция с `HI_STATE` работает корректно, используем узлы `debug`.

  • Добавьте узел `debug` после узла "Нормализация входа". Убедитесь, что он выводит `msg.payload`.
  • Добавьте еще один узел `debug` параллельно подключив его к узлу "Обновление HI_STATE" или разместив перед `mqtt out`. Настройте его на вывод `complete message object`.
  • Разверните (Deploy) Flow.
  • Теперь проведите полный тест-кейс:

    1. Сообщение `"Home"` от первого `debug`.

    2. Сообщение от второго `debug` с объектом, где `msg.payload` теперь представляет собой полный объект `HI_STATE` с обновленным блоком `{ system: { mode: "Home", ... } }`.

    3. Статус основной `function`-ноды должен измениться на `Переход: Away -> Home`.

    * В окне отладки не должно появиться новых сообщений, так как сработала блокировка от дублирования.

    * Статус `function`-ноды изменится на `Режим уже 'Home'. Без изменений.`.

    ---

    Итоги и следующие шаги

    тоги и следующие шаги

    В рамках этого урока мы создали надежный, централизованный и персистентный Flow для