ГлавнаяАкадемияСценарии умного дома: режимы, состояния, приоритеты → Настройка проекта: структура Flow, персистентный контекст, стандарты именования

Настройка проекта: структура Flow, персистентный контекст, стандарты именования

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

Архитектура сценарного слоя в Node-RED: структура и точки входа

> 🔗 Связанный материал: Концепция и технические приемы работы с Flow и Subflow будут подробно рассмотрены в последующих уроках. На данном этапе важно понять их роль в создании логической структуры проекта.

При проектировании системы автоматизации в Node-RED одним из первых и наиболее важных архитектурных решений является выбор способа организации потоков (`Flows`) и определение четких точек входа для данных. Существует два фундаментальных подхода: по физическому расположению и по функциональному назначению. Выбор правильного подхода напрямую влияет на понятность, масштабируемость и обслуживаемость вашего проекта.

Сравнение подходов к структурированию проекта

| Критерий | Физическая структура ("По комнатам") | Функциональная структура ("По системам") |

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

| Принцип организации | Один Flow на каждую комнату или этаж (e.g., "Гостиная", "Кухня"). | Один Flow на каждую инженерную систему (e.g., "Освещение", "Климат"). |

| Преимущества | Интуитивно понятно на начальном этапе. Легко найти все, что относится к одной комнате. | Логика сценариев не размазана по проекту. Легко отлаживать сложные взаимодействия. Масштабируемость. |

| Недостатки | Сценарии, затрагивающие несколько комнат (e.g., "Выключить все"), требуют сложных связей между Flow. Высокая связанность, сложность отладки. | Требует большей дисциплины и предварительного проектирования. Логика для одной комнаты может быть разнесена по разным Flow. |

| Рекомендация | Подходит для очень простых инсталляций (1-2 комнаты) без сложных сценариев. | Предпочтительный и обязательный подход для профессиональных инсталляций на платформе HI. |

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

Рекомендуемая структура Flow для контроллера HI

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

  • `CORE:` (Ядро системы)
  • * Назначение: Управление глобальными состояниями, переменными, режимами и инициализацией системы. Это мозг вашей автоматизации.

    * Примеры Flow:

    * `CORE: Globals & State`: Здесь происходит инициализация глобального объекта состояний `global.HI_STATE` при старте контроллера, управление основным конечным автоматом дома (режимы: "День", "Ночь", "Отъезд", "Тревога").

    * `CORE: Time & Schedule`: Управление всеми временными событиями, таймерами, расписаниями (например, запуск ночного режима в 23:00).

    * `CORE: Error Handling`: Централизованный обработчик ошибок, подключенный к узлам `Catch` со всего проекта. Он отвечает за логирование сбоев в MySQL и отправку уведомлений.

  • `DRIVERS:` (Драйверы оборудования)
  • * Назначение: Этот слой является "аппаратной абстракцией" (Hardware Abstraction Layer). Его задача — обеспечить стандартизированное взаимодействие с физическим оборудованием, скрывая детали протоколов.

    * Примеры Flow:

    * `DRIVERS: MQTT`: Единая точка входа (Entry Point) и выхода для всех MQTT-сообщений. Здесь происходит первичная валидация, парсинг топиков и преобразование сообщений в унифицированный формат системы (согласно паттерну "Контракт сообщения").

    * `DRIVERS: Modbus RTU`: Все узлы для опроса устройств по шине RS-485. Этот Flow преобразует массивы регистров в понятные JSON-объекты и отправляет их в логический слой.

    * `DRIVERS: DALI`: Управление шиной освещения DALI.

    * `DRIVERS: Controller I/O`: Прямое взаимодействие со встроенными входами и выходами контроллера (22 универсальных входа, 22 реле).

  • `LOGIC:` (Сценарная логика)
  • * Назначение: Здесь живет сама автоматизация. Эти Flow получают стандартизированные данные от слоя `DRIVERS` (через точки входа), обрабатывают их в соответствии со сценариями и текущим состоянием из `CORE`, а затем отправляют команды обратно в `DRIVERS`.

    * Примеры Flow:

    * `LOGIC: Lighting`: Вся логика управления освещением: по датчикам движения, по времени суток, диммирование, световые сцены.

    * `LOGIC: Climate (HVAC)`: Управление отоплением, вентиляцией и кондиционированием.

    * `LOGIC: Security`: Логика охранной и пожарной сигнализации, контроль доступа.

  • `UI:` (Пользовательские интерфейсы)
  • * Назначение: Формирование данных для панелей управления, мобильных приложений и голосовых ассистентов.

    * Примеры Flow:

    * `UI: Dashboard`: Подготовка данных для отображения в Node-RED Dashboard.

    * `UI: Mobile App Bridge`: Адаптация данных для взаимодействия с внешним мобильным приложением (например, через MQTT или HTTP API).

    Эта структура гарантирует низкую связанность и высокую сплоченность модулей, что является золотым стандартом в разработке программного обеспечения и ключевым фактором успеха в сложных проектах автоматизации. Особое внимание уделяется точкам входа — четко определенным Flow (например, в слое DRIVERS), через которые внешние сигналы концентрированно попадают в систему для дальнейшей маршрутизации, исключая хаотичные подключения напрямую к логике.

    ---

    Персистентный контекст: память системы

    > ⚠️ Внимание: Избегайте частой записи в персистентный контекст (например, данных с сенсоров каждую секунду). Это может привести к избыточной нагрузке на файловую систему EEPROM или встроенный накопитель контроллера и снижению его ресурса. Сохраняйте только ключевые переменные состояния при их изменении.

    В Node-RED существует три типа контекста — это области памяти для хранения переменных: `node`, `flow` и `global`.

    По умолчанию, все эти контексты хранятся в оперативной памяти (RAM). Это означает, что при перезагрузке контроллера, перезапуске службы Node-RED или после каждого `deploy` (развертывания) проекта все накопленные данные бесследно исчезают. Для системы умного дома это катастрофа: после сбоя питания система не сможет вспомнить, какой режим был активен, какой свет был включен, и вернется в состояние по умолчанию.

    Для решения этой проблемы необходимо активировать персистентный контекст, который сохраняет данные в энергонезависимой памяти. На контроллерах HI, работающих под управлением Debian, для этого используется встроенная файловая система.

    Активация персистентного контекста

    Настройка производится в конфигурационном файле Node-RED `settings.js`, который на контроллере HI обычно находится по пути `/home/node-red/.node-red/settings.js` или `/root/.node-red/settings.js`.

  • Подключитесь к контроллеру по SSH.
  • Откройте файл `settings.js` в текстовом редакторе, например, `nano`:
  •     nano /home/node-red/.node-red/settings.js

  • Найдите закомментированный раздел `contextStorage`. Если его нет, добавьте его.
  • Приведите его к следующему виду, чтобы активировать хранилище на базе файловой системы:
  • //... (другие настройки)
    
    

    contextStorage: {

    // Имя хранилища по умолчанию

    default: {

    module: "memory" // Оставляем 'memory', чтобы не засорять диск временными данными

    },

    // Наше персистентное хранилище

    persistent_store: {

    module: "localfilesystem",

    config: {

    // Директория для хранения файлов контекста.

    // Убедитесь, что у пользователя node-red есть права на запись!

    dir: "/var/lib/node-red/context",

    // Опционально: кэшировать значения в памяти для ускорения чтения

    cache: true

    }

    }

    },

    //... (другие настройки)

  • Сохраните файл (`Ctrl+X`, затем `Y` и `Enter`).
  • Перезапустите службу Node-RED, чтобы изменения вступили в силу:
  •     sudo systemctl restart nodered.service

    Использование персистентного контекста

    После перезапуска в узлах `function`, `change` и других появляется возможность указывать имя хранилища.

    Пример в узле `function`:
    // Получаем состояние системы из персистентного хранилища
    

    let hiState = global.get('HI_STATE', 'persistent_store') || {};

    // Обновляем режим дома и сохраняем в персистентное хранилище

    hiState.houseMode = 'night';

    global.set('HI_STATE', hiState, 'persistent_store');

    node.status({fill:"blue", shape:"dot", text:"Режим: " + hiState.houseMode});

    return msg;

    Теперь, даже после полной перезагрузки контроллера, вызов и проверка объекта `HI_STATE` вернет последние сохраненные значения, позволяя системе восстановить свое актуальное состояние.

    ---

    Стандарты именования: Nodes, Topics, Variables

    > 💡 Обязательный шаг: Создайте в проекте отдельный Flow с именем `DOCS: Naming & Globals`. Внутри него с помощью узлов `comment` задокументируйте все принятые в проекте стандарты именования и список ключевых глобальных переменных с их описанием. Это станет бесценным ресурсом при развитии проекта и передаче его другому инженеру.

    Единообразная и осмысленная система именования — это признак профессионального проекта. Она делает потоки читаемыми, упрощает поиск и отладку, предотвращает ошибки.

    Стандарт именования узлов (Nodes)

    Имя узла должно коротко и ясно отвечать на два вопроса: Что делает? и Над каким объектом?.

    Формат: `[ДЕЙСТВИЕ] Объект или Сущность`

    | Правильно | Неправильно | Пояснение |

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

    | `[SET] Режим Дома = Ночь` | `установить ночь` | Непонятно, что устанавливается. |

    | `[GET] Температура Гостиная` | `Modbus Getter` | Имя по умолчанию не несет никакой информации. |

    | `[ROUTE] Команда Освещения` | `Switch` | Неясно, что именно маршрутизируется. |

    | `[LOG] Ошибка связи Modbus` | `Запись в MySQL` | Непонятно, что именно логируется. |

    Стандарт для MQTT топиков

    Структура MQTT топиков критически важна для маршрутизации и безопасности. Иерархическая структура позволяет использовать wildcards (`+` и `#`) для подписки на группы событий.

    Формат: `Project/Location/Device/Parameter/[set]` Примеры:

    С такой структурой легко подписаться на все события в гостиной (`HI/floor1/livingroom/#`) или на все команды управления светом во всем доме (`HI/+/+/main_light/state/set`).

    Стандарт для переменных контекста

    Чтобы избежать хаоса и конфликтов имен в `flow` и `global` контекстах, используется структурированный подход. Сердцем системы всегда должен выступать единый объект глобального состояния.

    Формат для Flow-переменных и вложенных свойств: `scope.category.location_or_function.parameter` * `state`: Текущие состояния устройств или систем.

    * `config`: Настройки и уставки.

    * `status`: Статусы подсистем.

    * `timer`: Идентификаторы таймеров для их сброса.

    Примеры:

    Этот стандарт делает код в `function` узлах самодокументируемым и предотвращает случайные перезаписи переменных из разных частей проекта.

    ---

    Пример: инициализация глобальных переменных (global.HI_STATE)

    Рассмотрим практическую задачу по созданию потока, который будет устанавливать начальное состояние системы при каждом запуске контроллера, используя настроенный персистентный контекст и единый объект состояния `HI_STATE`.

    Цель: Создать Flow `CORE: Globals`, который при старте проверяет наличие глобального объекта `global.HI_STATE`. Если он не существует (первый запуск), узел присваивает ему базовый объект с первоначальными состояниями (например, режим `"day"`). Если существует — оставляет как есть, позволяя системе восстановиться.

    Шаг 1: Создание Flow и начальных узлов

  • Создайте новый Flow и назовите его `CORE: Globals`.
  • Добавьте узел `inject`. В его настройках:
  • * Установите галочку `Inject once after 0.1 seconds`, затем `none`. Это заставит узел сработать один раз сразу после запуска или развертывания.

    * `Name`: `[TRIGGER] On Startup`

  • Добавьте узел `function` и соедините его с выходом узла `inject`.
  • * `Name`: `[INIT] Global Variables`

    Ваш Flow должен выглядеть так:

    `[TRIGGER] On Startup` -> `[INIT] Global Variables`

    Шаг 2: Написание логики в узле `function`

    Откройте узел `[INIT] Global Variables` и вставьте следующий код:

    // Стандарты и константы, определенные для проекта
    

    const PERSISTENT_STORE = 'persistent_store'; // Имя хранилища из settings.js

    const STATE_KEY = 'HI_STATE'; // Единый ключ глобального состояния

    const DEFAULT_STATE = { houseMode: 'day' }; // Объект базового состояния по умолчанию

    node.log("Запуск инициализации глобальных переменных...");

    // 1. Пытаемся прочитать объект состояния из персистентного контекста

    let currentHiState = global.get(STATE_KEY, PERSISTENT_STORE);

    // 2. Проверяем, был ли объект найден (инициализирован)

    if (currentHiState === undefined) {

    // Переменная не найдена. Вероятно, это первый запуск системы.

    node.warn(`Глобальный объект '${STATE_KEY}' не найден. Устанавливаем базовое состояние по умолчанию.`);

    // 3. Устанавливаем базовое значение и сохраняем его в персистентный контекст

    global.set(STATE_KEY, DEFAULT_STATE, PERSISTENT_STORE);

    // Обновляем локальную ссылку для дальнейшей логики

    currentHiState = DEFAULT_STATE;

    // Формируем сообщение для логирования или дальнейших действий

    msg.payload = {

    event: "GLOBAL_INIT",

    variable: STATE_KEY,

    action: "created",

    value: currentHiState

    };

    } else {

    // Переменная найдена. Система успешно восстановила свои параметры.

    node.log(`Состояние '${STATE_KEY}' успешно восстановлено.`);

    // Обновляем структуру, если в новой версии кода добавлены новые параметры в DEFAULT_STATE

    // (Подход Object.assign позволяет сохранить старые настройки и добавить новые)

    currentHiState = Object.assign({}, DEFAULT_STATE, currentHiState);

    global.set(STATE_KEY, currentHiState, PERSISTENT_STORE);

    // Формируем сообщение для логирования

    msg.payload = {

    event: "GLOBAL_INIT",

    variable: STATE_KEY,

    action: "restored",

    value: currentHiState

    };

    }

    // 4. Выводим текущий статус для наглядности (показываем один из ключевых параметров)

    node.status({ fill: "green", shape: "dot", text: "Режим: " + currentHiState.houseMode });

    // Передаем сообщение дальше для возможного логирования

    return msg;

    Шаг 3: Логирование и проверка

  • Добавьте узел `debug` после узла `function`, чтобы видеть результат его работы в панели отладки. Назовите его `[DEBUG] Init Result`.
  • Разверните проект (`Deploy`).
  • В панели отладки вы увидите одно из двух сообщений:
  • * При первом запуске: `Глобальный объект 'HI_STATE' не найден. Устанавливаем базовое состояние по умолчанию.`

    * При последующих запусках: `Состояние 'HI_STATE' успешно восстановлено.`

    Что мы получили:

    Мы создали отказоустойчивый механизм инициализации. Он гарантирует, что у системы всегда будет определен единый объект состояния (`HI_STATE`), и при этом он не затирает последние известные статусы после перезагрузки. Это фундаментальный паттерн для построения надежной сценарной логики.

    ---

    Итоги и чек-лист для старта проекта

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

  • Четкая структура и точки входа: Функциональное разделение Flow на `CORE`, `DRIVERS`, `LOGIC` и `UI`, а также явное выделение входных потоков позволяет изолировать логику и упростить поддержку проекта.
  • Персистентный контекст: Сохранение единого объекта состояния в энергонезависимой памяти — это единственный способ обеспечить восстановление системы после сбоев.
  • Строгие стандарты именования: Единая система имен для узлов, переменных и топиков делает проект читаемым, понятным и защищает от трудноуловимых ошибок.
  • Дисциплина в следовании этим правилам с самого первого дня работы над проектом окупится сторицей на этапах отладки, эксплуатации и дальнейшего развития системы.

    Чек-лист для старта нового проекта на контроллере HI

    Перед тем, как написать первую строчку сценарной логики, каждый инженер должен выполнить следующие шаги:

    Что дальше?

    Теперь, когда у нас есть надежный каркас проекта, мы готовы перейти к созданию самой логики. В следующем уроке мы применим полученные знания на практике и создадим наш первый конечный автомат (State Machine) для управления режимами всего дома (`День`, `Ночь`, `Отъезд`), используя инициализированный нами единый объект состояний `global.HI_STATE` и структурированный подход к обработке событий, проходящих через подготовленные точки входа.