ГлавнаяАкадемияСценарии умного дома: режимы, состояния, приоритеты → Обзор архитектуры: глобальные переменные, Flow/Subflow, управление состоянием

Обзор архитектуры: глобальные переменные, Flow/Subflow, управление состоянием

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

Введение: от конечного автомата к реализации в Node-RED

Вспомним концепцию конечного автомата (Finite State Machine, FSM) как идеальную теоретическую модель для управления сложной системой, какой является современный умный дом. Эта модель оперирует тремя ключевыми понятиями: Состояния (States), События (Events) и Переходы (Transitions). Например, система может находиться в состоянии «Дома» или «Никого». Событие «Последний человек покинул дом» вызывает переход из состояния «Дома» в состояние «Никого», что, в свою очередь, запускает определенные действия: выключение света, снижение уставки термостата и постановку на охрану.

Теперь наша задача — перевести эту элегантную абстрактную модель на язык практической реализации в среде Node-RED на контроллере HI. Нам нужны конкретные инструменты, которые позволят надежно хранить состояния, обрабатывать события и организовывать логику переходов.

Основными строительными блоками для нашей архитектуры сценарного слоя в Node-RED станут:

  • Глобальный контекст (Global Context): Это централизованное хранилище данных, доступное из любого места в нашем проекте. Именно здесь мы будем хранить текущие состояния нашей системы. Он станет нашим «единым источником правды».
  • Потоки (Flows): Это отдельные вкладки в редакторе Node-RED. Мы будем использовать их для логической декомпозиции проекта на функциональные домены, такие как «Освещение», «Климат», «Безопасность», что значительно упрощает навигацию и поддержку.
  • Подпотоки (Subflows): Это переиспользуемые блоки логики, которые можно упаковать в один кастомный узел. Они идеальны для стандартизации повторяющихся задач, например, для обработки показаний датчика или управления приводом.
  • Давайте сопоставим абстрактные понятия FSM с нашими инструментами в Node-RED:

    | Понятие FSM | Реализация в Node-RED |

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

    | Состояние (State) | Переменная в глобальном контексте, которая хранит текущее значение. Например, `global.get("system.presence")`. |

    | Событие (Event) | Объект сообщения `msg`, который инициирует изменение. Например, MQTT-сообщение от кнопки или сигнал от датчика. |

    | Переход (Transition) | Логика внутри узла `Function` или `Switch`, которая анализирует событие и изменяет состояние в глобальном контексте. |

    | Действие (Action) | Потоки, которые реагируют на изменение состояния (а не на исходное событие) и выполняют команды для устройств. |

    Таким образом, мы построим архитектуру, где разные части системы не общаются друг с другом напрямую, а реагируют на изменения в общем, централизованном состоянии. Этот подход является ключом к созданию по-настояшему надежной и масштабируемой системы автоматизации.

    ---

    Глобальный контекст: единый источник правды

    📋 Ключевые понятия:

    Из всех видов контекста в Node-RED, глобальный контекст играет центральную роль в нашей архитектуре. Мы будем использовать его как единый источник правды (Single Source of Truth) — центральное хранилище, которое в любой момент времени содержит достоверную информацию о текущем состоянии всей системы автоматизации.

    Зачем это нужно? Представьте, что логика климат-контроля должна знать, дома ли кто-то, чтобы поддерживать комфортную температуру. А система безопасности должна понимать то же самое, чтобы решить, нужно ли включать охрану. Вместо того чтобы каждый из этих модулей имел свою собственную логику определения присутствия, они оба будут просто обращаться к одной глобальной переменной, например, `global.get("system.presence")`.

    Типичные данные, которые следует хранить в глобальном контексте:

    Структурирование и именование

    Чтобы избежать хаоса, крайне важно придерживаться строгой системы именования. Лучшая практика — использовать вложенную структуру, похожую на объекты в JavaScript, разделяя уровни точкой.

    Пример хорошей структуры глобального контекста:

    {
    

    "system": {

    "presence": "home",

    "mode": "normal",

    "time_of_day": "day"

    },

    "security": {

    "state": "disarmed",

    "sensors": {

    "door_main": "closed",

    "window_kitchen": "closed"

    }

    },

    "climate": {

    "living_room": {

    "setpoint": 22.0,

    "current_temp": 22.4,

    "mode": "auto"

    }

    },

    "lighting": {

    "group_living_room": "off"

    }

    }

    Такая организация позволяет легко находить нужные переменные и понимать их принадлежность. Для работы с вложенными переменными используется стандартный синтаксис:

    // Запись нового состояния присутствия
    

    global.set("system.presence", "away");

    // Чтение состояния главного датчика двери

    let doorState = global.get("security.sensors.door_main");

    ⚠️ Внимание: Никогда не записывайте в глобальный контекст сложные объекты, такие как полный `msg`. Это приводит к перерасходу памяти и замедлению работы контроллера. Храните только атомарные значения (числа, строки, boolean) и простые, заранее спроектированные структуры данных.

    Персистентность: переживая перезагрузку

    Что произойдет с нашими состояниями, если контроллер перезагрузится из-за сбоя питания? Если контекст хранится только в оперативной памяти (in-memory), все состояния будут потеряны, и система запустится в неопределенном состоянии по умолчанию. Это недопустимо для серьезных инсталляций.

    Контроллеры HI поставляются с предварительно настроенным персистентным контекстом. Это означает, что все данные из `global` и `flow` контекстов автоматически сохраняются в файловую систему контроллера.

    Как это работает:
  • При вызове `global.set("key", "value")` данные не только сохраняются в оперативной памяти для быстрого доступа, но и с определенной периодичностью (или принудительно) записываются в специальный JSON-файл на диске контроллера.
  • При старте Node-RED после перезагрузки система автоматически считывает этот файл и восстанавливает все переменные в глобальном контексте.
  • Таким образом, если система была в режиме «Никого» до отключения питания, она вернется в этот же режим после восстановления работы, обеспечивая предсказуемость и надежность.

    ---

    Декомпозиция логики: Flow и Subflow

    По мере роста проекта файл `flows.json` может превратиться в огромную, запутанную «паутину» из сотен узлов. Поддерживать и отлаживать такую систему становится практически невозможно. Ключ к созданию масштабируемой архитектуры — это грамотная декомпозиция, то есть разделение сложной системы на более простые и независимые части. В Node-RED для этого есть два мощных инструмента: Flows и Subflows.

    Разделение на домены с помощью Flow

    Самый первый и простой шаг к порядку — это использование вкладок (Flows) для группировки логики по функциональным доменам. Создайте отдельные вкладки для каждой крупной подсистемы вашего объекта.

    Рекомендуемая структура вкладок:

    Нумерация вкладок помогает поддерживать постоянный порядок их следования в интерфейсе.

    Стандартизация с помощью Subflow

    Если вы заметили, что одна и та же последовательность из нескольких узлов повторяется в проекте снова и снова, это верный признак того, что ее пора вынести в подпоток (Subflow). Subflow — это, по сути, создание вашего собственного, кастомного узла Node-RED.

    Критерии для создания Subflow:

    > 💡 Подсказка: Используйте префиксы в именах Subflow (например, `SUB-LOGIC-`, `SUB-DRV-`), чтобы легко ориентироваться в палитре узлов и понимать их назначение: `LOGIC` для логических операций, `DRV` для взаимодействия с драйверами устройств.

    Практический пример: Subflow `SUB-DRV-DeviceControl`

    Давайте создадим универсальный Subflow для управления любым устройством. Он будет принимать на вход команду, обновлять состояние устройства в глобальном контексте и отправлять физическую команду по MQTT.

    Задача Subflow:
  • Принять на вход сообщение с ID устройства, новым состоянием и типом команды.
  • Обновить соответствующую переменную в `global` контексте.
  • Сформировать и отправить MQTT-сообщение для физического устройства.
  • Настройка Subflow: * `DEVICE_ID`: Уникальный ID устройства (например, `light_living_room_main`).

    * `MQTT_TOPIC`: MQTT-топик для управления устройством (например, `cmnd/sonoff_1/POWER`).

    Логика внутри Subflow:
    [Input] --> [Function: "Update Global & Prepare CMD"] --> [MQTT Out] --> [Output]
    
    Код для узла `Function`:
    // Получаем переменные окружения, заданные при размещении узла
    

    const deviceId = env.get("DEVICE_ID");

    const mqttTopic = env.get("MQTT_TOPIC");

    // Входящее сообщение, например: { payload: "ON" }

    const command = msg.payload;

    // 1. Обновляем состояние в глобальном контексте

    // Путь к переменной формируется динамически

    // например, global.set("devices.light_living_room_main.state", "ON")

    global.set("devices." + deviceId + ".state", command);

    // 2. Готовим исходящее сообщение для MQTT

    msg.topic = mqttTopic;

    // msg.payload уже содержит нужную команду ("ON", "OFF", etc.)

    node.status({fill:"green", shape:"dot", text: `${deviceId}: ${command}`});

    return msg;

    Теперь в основном потоке вместо сложной цепочки узлов мы можем просто поставить один наш узел `SUB-DRV-DeviceControl`, указав в его свойствах `DEVICE_ID` и `MQTT_TOPIC`. Вся логика инкапсулирована, и основной поток стал чище и понятнее.

    ---

    Практика: реализация состояний "Дома" и "Никого"

    Давайте на практическом примере соберем воедино все рассмотренные концепции. Мы создадим централизованный сценарий, который управляет главным системным состоянием `system.presence` и покажем, как другие подсистемы на него реагируют.

    Цель: Создать единый механизм для переключения режимов «Дома» (`home`) и «Никого» (`away`), который будет служить источником правды для всех остальных сценариев.

    Шаг 1: Триггеры (События)

    Решение о смене режима может приниматься на основе множества событий. Наша задача — собрать их все и направить в один центральный обработчик.

    * MQTT-сообщение:

    * Топик: `cmnd/system/presence/set`

    * Payload: `{"presence": "away"}`

    * MQTT-сообщение от драйвера входа:

    * Топик: `stat/switch/main_door`

    * Payload: `{"action": "single_click"}`

    * HTTP-запрос или MQTT-сообщение:

    * Топик: `stat/geolocation/presence`

    * Payload: `{"users_home": 0}`

    Шаг 2: Централизация логики

    Все эти триггеры должны вести к одному узлу `Function`, который и будет принимать финальное решение о смене состояния. Разместим этот поток на вкладке 00-System.

    Схема потока:
    [MQTT In: cmnd/system/presence/set] --+
    

    |

    [MQTT In: stat/switch/main_door] -----+--> [Function: "Presence FSM"] --> [Дальнейшая обработка...]

    |

    [MQTT In: stat/geolocation/presence]--+

    Код для узла `Function: "Presence FSM"`:
    // Получаем текущее состояние из глобального контекста для предотвращения лишних действий
    

    const currentState = global.get("system.presence") || "unknown";

    let newState = currentState;

    // Анализируем триггер и принимаем решение

    switch (msg.topic) {

    case "cmnd/system/presence/set":

    // Прямая команда из интерфейса

    if (msg.payload.presence === "home" || msg.payload.presence === "away") {

    newState = msg.payload.presence;

    }

    break;

    case "stat/switch/main_door":

    // Кнопка у двери работает как тумблер (toggle)

    if (currentState === "home") {

    newState = "away";

    } else {

    newState = "home";

    }

    break;

    case "stat/geolocation/presence":

    // Логика по геолокации

    if (msg.payload.users_home > 0) {

    newState = "home";

    } else {

    newState = "away";

    }

    break;

    }

    // Проверяем, изменилось ли состояние

    if (newState !== currentState) {

    // 1. Устанавливаем новое состояние в глобальный контекст - это САМОЕ ГЛАВНОЕ ДЕЙСТВИЕ

    global.set("system.presence", newState);

    // 2. (Опционально) Генерируем стандартизированное системное событие для логирования

    msg.topic = "event/system/presence_changed";

    msg.payload = {

    from: currentState,

    to: newState,

    trigger_topic: msg.topic // Сохраняем исходный топик для аудита

    };

    node.status({ fill: "blue", shape: "dot", text: `Presence set to: ${newState}` });

    return msg; // Отправляем сообщение дальше для логирования или других действий

    } else {

    // Состояние не изменилось, ничего не делаем

    node.status({ fill: "grey", shape: "ring", text: `Presence unchanged: ${currentState}` });

    return null; // Останавливаем поток

    }

    Шаг 3: Реакция других подсистем

    Теперь самое интересное. Логика климата или безопасности не имеет никакой связи с кнопками или геолокацией. Она просто реагирует на изменение `global.get("system.presence")`.

    Пример на вкладке "02-Climate":

    Предположим, у нас есть поток, который каждые 5 минут проверяет и корректирует работу кондиционера.

    Код в узле `Function` этого потока:
    const presence = global.get("system.presence");
    

    const currentTemp = msg.payload.temperature; // Температура пришла от датчика

    let setpoint;

    // Логика выбора уставки в зависимости от глобального состояния

    if (presence === "home") {

    setpoint = global.get("climate.living_room.day_setpoint") || 23; // Комфортная температура

    } else { // away, vacation, etc.

    setpoint = global.get("climate.living_room.eco_setpoint") || 18; // Энергосберегающая температура

    }

    // Далее логика сравнения currentTemp с setpoint и отправка команды кондиционеру

    // ...

    return msg;

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

    ---

    Итоги: построение масштабируемой архитектуры

    В этом уроке мы сделали ключевой шаг от теоретических моделей к практической реализации сценарного слоя. Мы определили три столпа, на которых будет строиться наша архитектура автоматизации в Node-RED:

    Эта комбинация формирует слабосвязанную архитектуру. Вместо того чтобы создавать клубок прямых связей, где климат-контроль напрямую слушает кнопку у двери, мы строим систему, где подсистемы реагируют на абстрактные изменения состояний (`system.presence` изменилось на `away`). Такой подход кардинально повышает надежность, так как отказ одного триггера не обрушит всю систему. Он также упрощает отладку и, что самое важное, позволяет легко добавлять новые функции, не ломая старые.

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

    Что дальше?

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