ГлавнаяАкадемияСценарии умного дома: режимы, состояния, приоритеты → Шаг 2: Реализация state machine и журнала событий

Шаг 2: Реализация state machine и журнала событий

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

---

---

Введение: от графа состояний к коду

ведение: от графа состояний к коду

На предыдущих занятиях мы изучили теоретические основы конечных автоматов, спроектировав граф состояний для нашего проекта. Эта визуальная модель позволила нам определить все возможные режимы работы объекта (например, "Дома", "Отсутствие", "Ночь", "Тревога") и правила, по которым система должна переключаться между ними. Теперь наша задача — перевести эту абстрактную схему в работающий, надежный и отлаживаемый программный код на платформе Node-RED.

> 🔗 Связанный материал: Для полного понимания контекста этого урока необходимо изучить материалы предыдущих занятий: Введение в конечные автоматы (FSM) и Масштабирование логики с помощью Subflows, где мы заложили теоретическую базу и рассмотрели инструменты для ее реализации.

От концепции к логике переключений

Центральной идеей, которая позволит нам реализовать спроектированный граф, является конечный автомат (или State Machine). Это модель, описывающая систему, способную находиться в одном из конечного числа состояний в любой момент времени. Переход из одного состояния в другое происходит в ответ на внешнее или внутреннее событие.

Давайте формализуем ключевые понятия, с которыми будем работать:

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

Почему именно конечный автомат?

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

> ⚠️ Важное замечание по архитектуре:

> Для реализации State Machine в рамках всего дома мы будем использовать global context. В отличие от flow context, который изолирован внутри одной вкладки, хранение состояния в `global` позволяет обращаться к текущему режиму дома (например, `global.get("home_state")`) из любого узла или subflow в любой части проекта. Это критически важно для обеспечения синхронности действий всех подсистем.

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

Выбор инструментов в Node-RED для реализации State Machine

ыбор инструментов в Node-RED для реализации State Machine

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

Сравнение подходов

| Критерий | Готовые узлы (e.g., `node-red-contrib-finite-statemachine`) | Собственная реализация (на узлах `Function`) |

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

| Скорость разработки | Высокая для простых автоматов. | Средняя, требует написания initial-кода. |

| Прозрачность логики | Низкая. Логика переходов скрыта внутри узла. | Высокая. Вся логика находится в одном месте (в коде узла `Function`). |

| Гибкость | Ограничена возможностями узла. | Максимальная. Можно реализовать любую, даже самую сложную логику приоритетов. |

| Отладка | Сложная. Трудно понять, почему переход не произошел. | Простая. Можно использовать `node.warn()` или `node.error()`. |

| Интеграция | Требует "обвязки" узлами для перехвата событий. | Встроенная. Логирование — часть логики самого автомата. |

| Зависимости | Внешняя. Зависимость от стороннего разработчика. | Отсутствуют. Используются только базовые узлы Node-RED. |

Для проекта уровня сертификации Интегратор (Integration) или Архитектор автоматизации (Architect), где требуется максимальная надежность, собственная реализация на узле `Function` является предпочтительным методом. В рамках текущего модуля мы разберем базовую структуру кода, а сложную реализацию для многоуровневых систем рассмотрим в уроке Проектирование сложных состояний.

Упаковка State Machine в Subflows

Реализовав логику автомата в узле `Function`, её часто необходимо тиражировать (например, если в каждой комнате дома используется свой независимый автомат для освещения). Чтобы не копировать код и упростить поддержку, рекомендуется оборачивать такие `Function`-узлы в подпотоки. Детальнее этот подход мы разбираем в уроке Subflows. Подпоток позволяет создать визуальный интерфейс для ввода начальных параметров, при этом сама логика State Machine остается единой для всех экземпляров.

Хранение состояния: уровни контекста

Ключевой элемент любого конечного автомата — это память, где хранится его текущее состояние. Для этого в Node-RED используется механизм контекстов:

  • Flow Context: Идеален для локальных автоматов (например, «Состояние света в кухне»). Данные доступны только внутри одной вкладки.
  • Global Context: Используется для системных режимов («Дом», «Охрана», «Гости»), которые должны быть доступны из любой части проекта (разных Flow).
  • Главное преимущество использования контекста на контроллерах HI — его персистентность.

    > 💡 Подсказка: В настройках Node-RED контроллеров HI по умолчанию включено сохранение (persistence) контекстов на диск. Это гарантирует, что ваша state machine восстановит свое последнее активное состояние после перезапуска Node-RED или перезагрузки контроллера.

    Для реализации нашего автомата мы будем использовать `flow context`, если логика локальна, или `global context`, если режим влияет на весь дом.

    ⚠️ Важно: При использовании `global` контекста убедитесь, что имена переменных уникальны (напр. `global.set('sys_mode_main', 'home')`), чтобы избежать конфликтов между разными сценариями.

    Практика: Создание «движка» конечного автомата

    Теперь перейдем к созданию ядра нашей системы. Мы создадим один узел `Function`, который будет выполнять роль центрального "движка" состояний.

    Подготовка в Node-RED

  • Создайте новый поток (вкладку) в вашем рабочем пространстве Node-RED и назовите его "System State Machine".
  • Добавьте узел `Function` и назовите его "State Machine Engine". В настройках узла обязательно укажите 2 выхода (Outputs):
  • * Выход 1: Сообщения для запуска действий (управление устройствами и сценариями).

    * Выход 2: Сообщения для журнала событий (логирование успехов и ошибок).

    Код движка (State Machine Engine)

  • Вставьте следующий код в узел `Function`. Этот скрипт представляет собой скелет нашего автомата, который мы в дальнейшем сможем расширять и адаптировать под новые зоны.
  • // ID: FLOW-SYSTEM-FSM-001
    

    // Узел: State Machine Engine

    // --- 1. Определение состояний и переходов ---

    const transitions = {

    'undefined': { 'initialize': 'home_day' }, // Начальный переход при первом запуске

    'home_day': {

    'event_leave': 'away',

    'event_night': 'home_night',

    'event_arm': 'security_armed'

    },

    'home_night': {

    'event_leave': 'away',

    'event_day': 'home_day',

    'event_arm': 'security_armed'

    },

    'away': {

    'event_return': 'home_day',

    'event_arm': 'security_armed_away'

    },

    'security_armed': {

    'disarm': 'home_day'

    },

    'security_armed_away': {

    'disarm': 'away'

    }

    //... другие состояния и переходы

    };

    // --- 2. Получение события и текущего состояния ---

    const currentState = flow.get('system_state') || 'undefined';

    // Событие может приходить в topic или в payload. Отдаем приоритет topic.

    const event = msg.topic || msg.payload.event;

    // --- 3. Проверка валидности события и перехода ---

    if (!event) {

    node.warn(`[State Machine] Получено сообщение без события (event). Игнорируется.`);

    return null; // Прекращаем выполнение

    }

    const possibleTransitions = transitions[currentState];

    if (!possibleTransitions || !possibleTransitions[event]) {

    node.warn(`[State Machine] Недопустимый переход из '${currentState}' по событию '${event}'.`);

    // Формируем сообщение для журнала об ошибке

    const logMsg = {

    payload: {

    timestamp: Date.now(),

    event: event,

    from_state: currentState,

    to_state: currentState, // Состояние не изменилось

    status: "denied",

    details: `Transition from '${currentState}' on event '${event}' is not allowed.`

    }

    };

    return [null, logMsg]; // Отправляем сообщение только на второй выход (лог)

    }

    // --- 4. Выполнение перехода и обновление состояния ---

    const newState = possibleTransitions[event];

    flow.set('system_state', newState);

    // Обновляем статус узла для визуальной диагностики

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

    // --- 5. Формирование сообщений для действий и журнала ---

    // Сообщение для лога (успешный переход)

    const logMsg = {

    payload: {

    timestamp: Date.now(),

    event: event,

    from_state: currentState,

    to_state: newState,

    status: "success",

    details: `Переход из '${currentState}' в '${newState}' по событию '${event}'.`,

    source_msg: msg // Сохраняем исходное сообщение для отладки

    }

    };

    // Сообщение для запуска действий

    // Мы передаем информацию о старом и новом состоянии,

    // чтобы потоки-исполнители могли реализовать логику входа/выхода.

    const actionMsg = {

    topic: `statemachine/transition`,

    payload: {

    from: currentState,

    to: newState,

    event: event

    }

    };

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

    // Выход 1: actionMsg, Выход 2: logMsg

    return [actionMsg, logMsg];

    Архитектура скрипта: Как это работает

    * `actionMsg` (Выход 1): Информирует остальную логику (другие вкладки или узлы), что произошел успешный переход. Через поля `from` и `to` исполнители могут запускать логику входа (OnEnter) или выхода (OnExit) из режима.

    * `logMsg` (Выход 2): Содержит расширенный набор данных для записи в долгосрочное хранилище или отладочную консоль.

    > 💡 Совет по оптимизации и инкапсуляции:

    > Такое разделение на "выдачу заданий" и "логирование" является паттерном хорошей архитектуры: сам по себе движок не занимается напрямую переключением реле или отправкой Telegram-уведомлений. Если вам понадобится создать независимые state machine для обособленных зон (например, бани или гаража), этот узел имеет смысл перевести в переиспользуемый подпоток. Подробнее о работе с ними мы писали в уроке по Subflows (COURSE-07-M02-L03).

    Локальный тест-план (Чек-лист проверки)

    Перед тем как двигаться дальше, проверим автономную работу этого узла:

    Практика: Реализация журнала событий

    Надежный журнал — это ваш лучший друг во время отладки и эксплуатации системы. Он позволяет ответить на вопросы: "Почему вчера в 3 часа ночи включился свет?" или "Какой сценарий был активен во время сбоя?".

    > ⚠️ Внимание: Избегайте логирования каждого входящего сообщения. Записывайте в журнал только факты смены состояния или получения невалидного события. Иначе при высокой частоте событий вы рискуете создать чрезмерную нагрузку на базу данных и шину MQTT (log flood).

    Интеграция с архитектурой MQTT+MySQL (M08)

    В предыдущих модулях (в частности, в M08) мы уже спроектировали и развернули надежную подсистему логирования, связывающую MQTT-брокер и базу данных MySQL. Создание нового, изолированного потока записи логов для конечного автомата было бы грубым нарушением принципа DRY (Don't Repeat Yourself) и шагом назад в развитии нашей архитектуры.

    Вместо того чтобы создавать с нуля отдельный механизм отправки и обработки логов базы данных, мы переиспользуем уже готовую инфраструктуру из модуля M08. Наш узел `Function` ("State Machine Engine") уже настроен на генерацию стандартизированных JSON-объектов при каждом изменении состояния или ошибке перехода. Эти объекты идеально подходят для отправки в нашу существующую шину.

    Пример формируемого JSON-сообщения (успешный переход):

    {
    

    "timestamp": 1678886400000,

    "event": "event_night",

    "from_state": "home_day",

    "to_state": "home_night",

    "status": "success",

    "details": "Переход из 'home_day' в 'home_night' по событию 'event_night'."

    }

    Пошаговая интеграция с готовым Subflow

    Чтобы направить эти события в нашу глобальную базу данных, нам нужно лишь подключить второй выход нашего автомата к уже существующему Subflow логирования. Если вам нужно освежить в памяти принципы создания и использования подпотоков, обратитесь к уроку по Subflows.

  • Откройте поток "System State Machine", где расположен ваш узел "State Machine Engine".
  • В палитре узлов найдите категорию `subflows` и выберите узел логирования из модуля M08 (например, `M08 System Logger`). Добавьте его на рабочее пространство.
  • Откройте свойства узла `M08 System Logger` и, если это предусмотрено его настройками, укажите категорию логов: `statemachine_events`.
  • Соедините второй выход узла "State Machine Engine" (тот, что отвечает за `logMsg`) со входом узла `M08 System Logger`.
  • Архитектура решения:
               +-----------------------+
    

    (вход)---->| State Machine Engine |----(выход 1)---> [К потокам действий]

    +-----------------------+

    |

    +---(выход 2)--> [Subflow: M08 System Logger] ---> (MQTT -> MySQL)

    Теперь каждое отслеженное изменение состояния автоматически передается в M08 System Logger. Этот Subflow стандартизирует форматы, публикует данные в системный MQTT-топик, а уже существующий глобальный обработчик M08 перехватывает их и надежно сохраняет в таблицу MySQL на контроллере HI. Это наглядный пример того, как правильная архитектура позволяет легко интегрировать новые сложные механизмы (State Machine) в единую экосистему умного дома без дублирования кода.

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

    После настройки интеграции проведите базовое тестирование механизма:

    Резюме и следующие шаги

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

    Ключевые результаты:
  • Мы реализовали движок конечного автомата на одном узле `Function`, что обеспечивает прозрачность и максимальную гибкость логики.
  • Мы использовали `flow context` с файловой персистентностью для надежного хранения и восстановления состояния системы после перезагрузок.
  • Мы спроектировали и внедрили структурированный журнал событий, который фиксирует все изменения состояний и попытки невалидных переходов.
  • Мы интегрировали State Machine с существующей архитектурой MQTT+MySQL из модуля M08 через готовый Subflow (урок COURSE-07-M02-L03), избежав дублирования кода и создав единую систему ведения журнала событий.
  • Созданный нами движок пока работает в "вакууме". Он получает абстрактные события и генерирует абстрактные сообщения о действиях.

    В следующем уроке (COURSE-07-M01-L06) мы "оживим" нашу систему: