Настройка проекта: структура Flow, персистентный контекст, стандарты именования
Архитектура сценарного слоя в Node-RED: структура и точки входа
> 🔗 Связанный материал: Концепция и технические приемы работы с Flow и Subflow будут подробно рассмотрены в последующих уроках. На данном этапе важно понять их роль в создании логической структуры проекта.
При проектировании системы автоматизации в Node-RED одним из первых и наиболее важных архитектурных решений является выбор способа организации потоков (`Flows`) и определение четких точек входа для данных. Существует два фундаментальных подхода: по физическому расположению и по функциональному назначению. Выбор правильного подхода напрямую влияет на понятность, масштабируемость и обслуживаемость вашего проекта.
Сравнение подходов к структурированию проекта
| Критерий | Физическая структура ("По комнатам") | Функциональная структура ("По системам") |
| ----------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------- |
| Принцип организации | Один Flow на каждую комнату или этаж (e.g., "Гостиная", "Кухня"). | Один Flow на каждую инженерную систему (e.g., "Освещение", "Климат"). |
| Преимущества | Интуитивно понятно на начальном этапе. Легко найти все, что относится к одной комнате. | Логика сценариев не размазана по проекту. Легко отлаживать сложные взаимодействия. Масштабируемость. |
| Недостатки | Сценарии, затрагивающие несколько комнат (e.g., "Выключить все"), требуют сложных связей между Flow. Высокая связанность, сложность отладки. | Требует большей дисциплины и предварительного проектирования. Логика для одной комнаты может быть разнесена по разным Flow. |
| Рекомендация | Подходит для очень простых инсталляций (1-2 комнаты) без сложных сценариев. | Предпочтительный и обязательный подход для профессиональных инсталляций на платформе HI. |
Для создания сложных сценарных систем, которые являются темой данного курса, физическое разделение является неприемлемым. Представьте сценарий "Я ушел": он должен выключить свет во всех комнатах, перевести климат в эконом-режим, активировать систему безопасности. При физической структуре логика этого сценария будет разбросана по десятку разных Flow, что делает ее поддержку и отладку кошмаром.
Рекомендуемая структура Flow для контроллера HI
Для обеспечения чистоты и предсказуемости архитектуры сценарного слоя мы стандартизируем функциональную структуру проекта, состоящую из четырех основных типов Flow.
* Назначение: Управление глобальными состояниями, переменными, режимами и инициализацией системы. Это мозг вашей автоматизации.
* Примеры Flow:
* `CORE: Globals & State`: Здесь происходит инициализация глобального объекта состояний `global.HI_STATE` при старте контроллера, управление основным конечным автоматом дома (режимы: "День", "Ночь", "Отъезд", "Тревога").
* `CORE: Time & Schedule`: Управление всеми временными событиями, таймерами, расписаниями (например, запуск ночного режима в 23:00).
* `CORE: Error Handling`: Централизованный обработчик ошибок, подключенный к узлам `Catch` со всего проекта. Он отвечает за логирование сбоев в MySQL и отправку уведомлений.
* Назначение: Этот слой является "аппаратной абстракцией" (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 реле).
* Назначение: Здесь живет сама автоматизация. Эти Flow получают стандартизированные данные от слоя `DRIVERS` (через точки входа), обрабатывают их в соответствии со сценариями и текущим состоянием из `CORE`, а затем отправляют команды обратно в `DRIVERS`.
* Примеры Flow:
* `LOGIC: Lighting`: Вся логика управления освещением: по датчикам движения, по времени суток, диммирование, световые сцены.
* `LOGIC: Climate (HVAC)`: Управление отоплением, вентиляцией и кондиционированием.
* `LOGIC: Security`: Логика охранной и пожарной сигнализации, контроль доступа.
* Назначение: Формирование данных для панелей управления, мобильных приложений и голосовых ассистентов.
* Примеры Flow:
* `UI: Dashboard`: Подготовка данных для отображения в Node-RED Dashboard.
* `UI: Mobile App Bridge`: Адаптация данных для взаимодействия с внешним мобильным приложением (например, через MQTT или HTTP API).
Эта структура гарантирует низкую связанность и высокую сплоченность модулей, что является золотым стандартом в разработке программного обеспечения и ключевым фактором успеха в сложных проектах автоматизации. Особое внимание уделяется точкам входа — четко определенным Flow (например, в слое DRIVERS), через которые внешние сигналы концентрированно попадают в систему для дальнейшей маршрутизации, исключая хаотичные подключения напрямую к логике.
---
Персистентный контекст: память системы
> ⚠️ Внимание: Избегайте частой записи в персистентный контекст (например, данных с сенсоров каждую секунду). Это может привести к избыточной нагрузке на файловую систему EEPROM или встроенный накопитель контроллера и снижению его ресурса. Сохраняйте только ключевые переменные состояния при их изменении.
В Node-RED существует три типа контекста — это области памяти для хранения переменных: `node`, `flow` и `global`.
- `node.context`: Переменные видны только внутри одного узла (например, для подсчета срабатываний).
- `flow.context`: Переменные доступны всем узлам в пределах одного Flow (вкладки).
- `global.context`: Переменные доступны из любого узла во всем проекте.
По умолчанию, все эти контексты хранятся в оперативной памяти (RAM). Это означает, что при перезагрузке контроллера, перезапуске службы Node-RED или после каждого `deploy` (развертывания) проекта все накопленные данные бесследно исчезают. Для системы умного дома это катастрофа: после сбоя питания система не сможет вспомнить, какой режим был активен, какой свет был включен, и вернется в состояние по умолчанию.
Для решения этой проблемы необходимо активировать персистентный контекст, который сохраняет данные в энергонезависимой памяти. На контроллерах HI, работающих под управлением Debian, для этого используется встроенная файловая система.
Активация персистентного контекста
Настройка производится в конфигурационном файле Node-RED `settings.js`, который на контроллере HI обычно находится по пути `/home/node-red/.node-red/settings.js` или `/root/.node-red/settings.js`.
nano /home/node-red/.node-red/settings.js
//... (другие настройки)
contextStorage: {
// Имя хранилища по умолчанию
default: {
module: "memory" // Оставляем 'memory', чтобы не засорять диск временными данными
},
// Наше персистентное хранилище
persistent_store: {
module: "localfilesystem",
config: {
// Директория для хранения файлов контекста.
// Убедитесь, что у пользователя node-red есть права на запись!
dir: "/var/lib/node-red/context",
// Опционально: кэшировать значения в памяти для ускорения чтения
cache: true
}
}
},
//... (другие настройки)
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)
Имя узла должно коротко и ясно отвечать на два вопроса: Что делает? и Над каким объектом?.
Формат: `[ДЕЙСТВИЕ] Объект или Сущность`- [ДЕЙСТВИЕ] в квадратных скобках: `[GET]`, `[SET]`, `[VALIDATE]`, `[FORMAT]`, `[ROUTE]`, `[LOG]`.
- Объект или Сущность: `Режим Дома`, `Освещение Гостиная`, `Температура Улица`.
| Правильно | Неправильно | Пояснение |
| --------------------------------------- | ------------------- | ------------------------------------------------------------------------------ |
| `[SET] Режим Дома = Ночь` | `установить ночь` | Непонятно, что устанавливается. |
| `[GET] Температура Гостиная` | `Modbus Getter` | Имя по умолчанию не несет никакой информации. |
| `[ROUTE] Команда Освещения` | `Switch` | Неясно, что именно маршрутизируется. |
| `[LOG] Ошибка связи Modbus` | `Запись в MySQL` | Непонятно, что именно логируется. |
Стандарт для MQTT топиков
Структура MQTT топиков критически важна для маршрутизации и безопасности. Иерархическая структура позволяет использовать wildcards (`+` и `#`) для подписки на группы событий.
Формат: `Project/Location/Device/Parameter/[set]`- Project: Имя проекта или объекта (например, `HI`, `MyHome`).
- Location: Физическое расположение (например, `floor1/livingroom`, `outdoor/garden`). Используйте `_` вместо пробелов.
- Device: Тип или имя устройства (например, `main_light`, `thermostat`, `motion_sensor`).
- Parameter: Конкретный параметр устройства (`state`, `temperature`, `brightness`, `command`).
- [set]: Опциональный суффикс для топиков, предназначенных для отправки команд.
- Телеметрия с датчика температуры: `HI/floor1/livingroom/thermostat/temperature`
- Состояние основного света в гостиной: `HI/floor1/livingroom/main_light/state`
- Команда на изменение состояния света: `HI/floor1/livingroom/main_light/state/set`
С такой структурой легко подписаться на все события в гостиной (`HI/floor1/livingroom/#`) или на все команды управления светом во всем доме (`HI/+/+/main_light/state/set`).
Стандарт для переменных контекста
Чтобы избежать хаоса и конфликтов имен в `flow` и `global` контекстах, используется структурированный подход. Сердцем системы всегда должен выступать единый объект глобального состояния.
Формат для Flow-переменных и вложенных свойств: `scope.category.location_or_function.parameter`- scope: `global` или `flow`.
- category: Тип переменной (`state`, `config`, `status`, `timer`).
* `config`: Настройки и уставки.
* `status`: Статусы подсистем.
* `timer`: Идентификаторы таймеров для их сброса.
- location_or_function: Место или функциональная принадлежность (`livingroom`, `hvac`, `security`).
- parameter: Имя параметра.
- `global.HI_STATE`: Единый корневой объект, хранящий фундаментальные параметры и текущие состояния системы (например, `global.HI_STATE.houseMode`).
- `global.config.hvac.targetTemperature`: Целевая температура для всей системы климата.
- `flow.state.light.lastMotionTimestamp`: Временная метка последнего движения в Flow управления светом.
- `flow.config.dimming.min_level`: Уровень минимальной яркости в Flow управления диммированием.
Этот стандарт делает код в `function` узлах самодокументируемым и предотвращает случайные перезаписи переменных из разных частей проекта.
---
Пример: инициализация глобальных переменных (global.HI_STATE)
Рассмотрим практическую задачу по созданию потока, который будет устанавливать начальное состояние системы при каждом запуске контроллера, используя настроенный персистентный контекст и единый объект состояния `HI_STATE`.
Цель: Создать Flow `CORE: Globals`, который при старте проверяет наличие глобального объекта `global.HI_STATE`. Если он не существует (первый запуск), узел присваивает ему базовый объект с первоначальными состояниями (например, режим `"day"`). Если существует — оставляет как есть, позволяя системе восстановиться.Шаг 1: Создание Flow и начальных узлов
* Установите галочку `Inject once after 0.1 seconds`, затем `none`. Это заставит узел сработать один раз сразу после запуска или развертывания.
* `Name`: `[TRIGGER] On Startup`
* `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: Логирование и проверка
* При первом запуске: `Глобальный объект 'HI_STATE' не найден. Устанавливаем базовое состояние по умолчанию.`
* При последующих запусках: `Состояние 'HI_STATE' успешно восстановлено.`
Что мы получили:Мы создали отказоустойчивый механизм инициализации. Он гарантирует, что у системы всегда будет определен единый объект состояния (`HI_STATE`), и при этом он не затирает последние известные статусы после перезагрузки. Это фундаментальный паттерн для построения надежной сценарной логики.
---
Итоги и чек-лист для старта проекта
В этом уроке мы заложили прочный фундамент для построения сложного и надежного сценарного слоя. Успех любого проекта автоматизации держится на трех китах, которые мы рассмотрели:
Дисциплина в следовании этим правилам с самого первого дня работы над проектом окупится сторицей на этапах отладки, эксплуатации и дальнейшего развития системы.
Чек-лист для старта нового проекта на контроллере HI
Перед тем, как написать первую строчку сценарной логики, каждый инженер должен выполнить следующие шаги:
- [ ] 1. Настроить персистентность: Отредактировать файл `settings.js` на контроллере, добавив `contextStorage` с модулем `localfilesystem`.
- [ ] 2. Создать базовую структуру Flow: Создать пустые вкладки (Flow) с именами `CORE: Globals`, `CORE: Error Handling`, `DRIVERS: MQTT`, `LOGIC: Lighting` и т.д., в соответствии с предполагаемой структурой и точками входа.
- [ ] 3. Задокументировать стандарты: Создать Flow `DOCS: Naming & Globals`. В нем, используя узлы `comment`, описать принятые стандарты именования для узлов, MQTT-топиков и переменных контекста.
- [ ] 4. Реализовать инициализацию: Создать Flow `CORE: Globals` с логикой инициализации основного объекта (`global.HI_STATE`), чтобы гарантировать предсказуемое начальное состояние системы в любой ситуации.
Что дальше?
Теперь, когда у нас есть надежный каркас проекта, мы готовы перейти к созданию самой логики. В следующем уроке мы применим полученные знания на практике и создадим наш первый конечный автомат (State Machine) для управления режимами всего дома (`День`, `Ночь`, `Отъезд`), используя инициализированный нами единый объект состояний `global.HI_STATE` и структурированный подход к обработке событий, проходящих через подготовленные точки входа.