ГлавнаяАкадемияСценарии умного дома: режимы, состояния, приоритеты → Шаг 3: Реализация 3+ аварийных и 5+ штатных сценариев

Шаг 3: Реализация 3+ аварийных и 5+ штатных сценариев

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

Введение: Capstone Project — финальная сборка системы

ведение: Capstone Project — финальная сборка системы

Поздравляем! Вы добрались до финального проекта этого курса — урока, где все изученные нами концепции и разработанные механизмы объединятся в единую, интеллектуальную и оркестрованную систему умного дома. До этого момента мы планомерно создавали ее составные части.

Ретроспектива: Наш путь по курсу

Вспомним пройденные этапы разработки нашей системы (модули M01-M09):

M06-M07: Мы научились реагировать на события из внешнего мира, такие как заход солнца для режима «Вечер» и команды с физических выключателей (включая логику ручного переопределения автоматики — override*).

Архитектура финальной сборки

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

  • Сценарии-инициаторы: Модифицируем потоки (протечка, отсутствие, закат), чтобы они больше не управляли устройствами напрямую. Их единственной задачей станет отправка запроса на смену состояния в наш FSM. Это ключевой принцип UX, когда датчики не диктуют действия, а лишь сообщают о событиях.
  • State Machine — арбитр: Готовый FSM из модуля M02 получит запрос, сравнит приоритет запрашиваемого состояния с текущим и либо разрешит, либо отклонит переход (например, отклонит включение режима «Вечер», если система находится в аварийном режиме «Протечка»).
  • Центральный исполнитель: Мы создадим новый, единый поток, который будет слушать успешные смены состояний (`system/state/changed`) и уже оттуда централизованно запускать конкретные действия: перекрывать воду, выключать свет, менять уставки климата или сбрасывать override-флаги.
  • Эта "сборка" и есть суть нашего сценарного слоя. Мы отказываемся от хаотичных прямых связей в пользу строгого порядка, где FSM выступает главным дирижером.

    > 🔗 Связанный материал: Убедитесь, что ваш конечный автомат (FSM) из модуля `M02` и система журналирования из `M03` полностью развернуты и работают. Вся логика, которую мы будем сегодня интегрировать, будет взаимодействовать именно с ними.

    Практика: Шаг 1. Интеграция аварийного сценария 'Протечка воды'

    рактика: Шаг 1. Интеграция аварийного сценария 'Протечка воды'

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

    Задача: Адаптировать существующий сценарий «Протечка» (из модуля `M04`). Поток обнаружения должен запрашивать у FSM переход в состояние `LEAK`. Исполнительная логика (закрытие кранов, отправка уведомлений) должна быть вынесена в отдельный поток, реагирующий на успешный переход в это состояние.

    > ⚠️ Внимание: Разделение на "запрос" и "действие" не снижает надежности. Оба потока работают локально на контроллере. Мы лишь добавляем логический слой арбитража, что критически важно для предотвращения конфликтов в более сложных системах.

    Часть A: Модификация потока обнаружения

    Откройте ваш flow со сценарием протечки. Сейчас он выглядит примерно так: `[mqtt in: leak sensor]` -> `[function: prepare command]` -> `[modbus-write: close valve]`. Мы должны его изменить.

  • Узел: `mqtt in`. Оставляем как есть, он слушает топик `hi/devices/leak_detector/+/state`.
  • Узел: `function` (переименуйте в "Request LEAK state"). Удалите из него старый код, формирующий команду для Modbus. Вставьте новый код, который формирует запрос для FSM.
  • // Получаем payload от датчика
    

    const data = msg.payload;

    // Реагируем только на событие "протечка обнаружена"

    if (data.value !== true) {

    return null; // Игнорируем сообщения "протечка устранена"

    }

    // Формируем стандартизированное сообщение для state machine

    msg.payload = {

    command: "setState",

    payload: {

    state: "LEAK", // Запрашиваемое состояние

    priority: 100, // Максимальный приоритет из нашего графа

    source: data.source // Источник события для журнала

    }

    };

    // Устанавливаем топик, который слушает наша state machine

    msg.topic = "system/fsm/command";

    node.status({fill:"red", shape:"dot", text:"LEAK detected! Requesting FSM..."});

    return msg;

    > 💡 Почему приоритет 100? Аварийные состояния (протечка, пожар, вторжение) должны безоговорочно вытеснять любые другие режимы (override). Если в момент протечки активирован сценарий набора ванной (приоритет 50) или базовый режим `HOME` (приоритет 10), высокоприоритетный `LEAK` перебьет их и заблокирует смену состояний на более низкие, пока авария не будет устранена.

  • Узел: `mqtt out`. Подключите выход `function` к этому узлу и настройте его на отправку сообщений в топик `system/fsm/command`.
  • Удаление: Удалите старые узлы `modbus-write` и другие исполнительные узлы из этого потока. Его задача теперь — только запрашивать состояние.
  • Часть B: Создание центрального потока действий

    Теперь создадим (или дополним) поток, который будет выполнять действия на основе команд от FSM.

  • Узел `mqtt in`: Создайте новый узел, который подписан на топик `system/state/changed`. Этот топик публикует сообщения наш FSM после каждой успешной смены состояния.
  • Узел `switch` (имя: "Route by State"): Подключите его к `mqtt in`. Он будет маршрутизировать поток в зависимости от нового состояния. Настройте его на проверку `msg.payload.currentState`.
  • * Правило 1: `==` "LEAK" -> Выход 1

    * Правило 2: `==` "AWAY" -> Выход 2

    * Правило 3: `==` "EVENING" -> Выход 3

    * ...и так далее для всех состояний.

  • Логика для 'LEAK': К выходу 1 узла `switch` мы подключаем исполнительную логику, которую ранее удалили из потока обнаружения.
  • * Узел `function` (имя: "Prepare Valve Close Command"):

        // Закрываем вводной кран холодной воды (например, Coil 10)

    msg.payload = {

    'value': true, 'fc': 5, 'unitid': 1, 'address': 10

    };

    // Возвращаем второе сообщение для горячей воды (Coil 11)

    const msgHot = {

    payload: {

    'value': true, 'fc': 5, 'unitid': 1, 'address': 11

    }

    };

    node.status({ fill:"red", shape:"ring", text:"Executing LEAK actions!" });

    return [msg, msgHot];

    * Узел `modbus-write`: Подключите два узла `modbus-write` к выходам `function`, каждый для своего крана.

    * Узел Уведомления: Параллельно подключите узел для отправки тревожного SMS/Push сообщения.

    Теперь сценарий протечки полностью интегрирован в новую архитектуру.

    Часть C: UX и сброс состояния (Override)

    Аварийный сценарий `LEAK` обладает наивысшим приоритетом, поэтому система не сможет самостоятельно вернуться в обычный режим (например, `HOME`), даже если датчик перестал фиксировать воду. Это осознанное UX/override решение: пользователь должен явно подтвердить, что проблема устранена, осмотрев место протечки.

    Для реализации этого сброса (clear override) добавьте отдельный небольшой поток:

  • Триггер (Button в Dashboard или физическая кнопка): Узел `ui_button` с названием "Сброс протечки".
  • Узел `function`: Формирует команду сброса состояния:
  •    msg.payload = {

    command: "clearState",

    payload: {

    state: "LEAK"

    }

    };

    msg.topic = "system/fsm/command";

    return msg;

  • Вывод этой функции отправляется в токен `system/fsm/command`. Только после этого FSM отменит блокировку приоритетом 100 и вернётся к предыдущему активному состоянию (или запросит расчет текущего по расписанию).
  • Часть D: Чек-лист проверки интеграции

    Чтобы убедиться, что новая логика отрабатывает корректно и нет логических разрывов, выполните следующий тест-план через инъекции (Inject nodes) в Node-RED:

    Практика: Шаг 2. Интеграция штатного режима 'Никого нет' (Away)

    рактика: Шаг 2. Интеграция штатного режима 'Никого нет' (Away)

    Повторим ту же процедуру для сценария "Никого нет", который мы создали ранее в курсе (модуль `M05`). Его базовая логика была основана на узле `trigger`, и по истечении таймера он напрямую выключал свет и климат.

    Задача: Изменить поток детектора отсутствия так, чтобы он запрашивал состояние `AWAY` у конечного автомата, а не управлял устройствами напрямую. Затем добавить в центральный поток действий новую ветку для выполнения всех команд этого режима.

    > 💡 Подсказка: Комбинирование нескольких источников для определения присутствия (Wi-Fi, BLE, движение) остается актуальным. Эти источники по-прежнему являются входами для узла `trigger`. Мы меняем только то, что происходит после `trigger`.

    Часть A: Модификация потока детектора отсутствия

  • Найдите поток с узлом `trigger`, который отсчитывает 15 минут без движения.
  • Выход узла `trigger` сейчас, вероятно, ведет к `function`, который затем управляет устройствами. Замените код в этом `function` узле (назовем его "Check & Request AWAY").
  • // Этот код выполняется, когда trigger-нода сообщила об отсутствии движения 15 мин.
    
    

    // Предполагаем, что состояние входной двери и текущее состояние FSM

    // доступны в контексте (flow.get), обновляемые другими потоками.

    const doorState = flow.get("door_front_state") || { value: "unknown" };

    const systemState = flow.get("systemState") || { priority: 0 };

    // Задел для UX-исключения (проверка активности медиа-системы)

    const tvPower = flow.get("tv_power_consumption_w") || 0;

    const isTvOn = tvPower > 30; // Если потребляем больше 30Вт, кто-то смотрит ТВ

    // ПРЕДУСЛОВИЯ:

    // 1. Дверь закрыта

    // 2. Нет более приоритетного состояния (например, FIRE или LEAK)

    // 3. Телевизор не работает (override-защита)

    if (doorState.value === false && systemState.priority < 10 && !isTvOn) {

    // Условия выполнены. Формируем запрос для FSM.

    msg.payload = {

    command: "setState",

    payload: {

    state: "AWAY",

    priority: 10, // Штатный приоритет режима 'Никого нет'

    source: "Absence Detector Scenario"

    }

    };

    msg.topic = "system/fsm/command";

    node.status({fill:"blue", shape:"dot", text:"Requesting AWAY mode"});

    return msg;

    } else {

    // Условия не выполнены, отменяем переход

    node.status({fill:"yellow", shape:"ring", text:"AWAY blocked. Door open, TV on, or priority conflict."});

    return null;

    }

  • Подключите выход этого `function` к тому же `mqtt out`, который отправляет команды в `system/fsm/command`.
  • Удалите из этого потока все старые узлы, напрямую управляющие светом, розетками и термостатами.
  • Часть B: Добавление действий для 'AWAY' в центральный поток

    Вернитесь к нашему центральному потоку действий (который начинается с `mqtt in: system/state/changed` и обрабатывает подтвержденные состояния).

  • Найдите узел `switch` ("Route by State").
  • От его выхода, который соответствует `currentState == 'AWAY'`, постройте новую цепочку действий:
  • * Узел `function` (имя: "Prepare AWAY commands"): формирует сразу несколько команд для разных систем умного дома.

    // Подготовка пакета команд для режима AWAY
    

    const lightOff = { payload: "OFF", topic: "zigbee2mqtt/light_group_all/set/state" };

    const socketsOff = { payload: "OFF", topic: "zigbee2mqtt/socket_group_media/set/state" };

    const climateEco = {

    payload: JSON.stringify({

    system_mode: "heat",

    temperature_setpoint_hold: true,

    occupied_heating_setpoint: 18

    }),

    topic: "zigbee2mqtt/thermostat_livingroom/set"

    };

    node.status({ fill:"blue", shape:"ring", text:"Executing AWAY actions!" });

    // Возвращаем вложенный массив. Каждый элемент массива уйдет на отдельный выход.

    // Если выход один - передаем массив объектов, и узел mqtt out отработает их последовательно.

    return [[lightOff, socketsOff, climateEco]];

    * Узел `mqtt out`: Подключите к выходу `function` узел `mqtt out` (оставьте topic пустым, так как он задается внутри `msg.topic` для каждого устройства).

    Часть C: Чек-лист проверки и тест-план

    Перед тем как двигаться дальше, проведите тестирование новой архитектуры по расширенному тест-плану:

    UX-исключения и Override-механизмы

    > ⚠️ UX/Override: Главная проблема таймеров отсутствия — ложные срабатывания, когда вы дома, но находитесь в статичном положении (например, лежите неподвижно перед телевизором, играете в консоль или читаете книгу). Датчики движения могут вас "потерять".

    Для предотвращения ложного запуска `AWAY` мы интегрировали в код "Check & Request AWAY" проверку исключений:

    Практика: Шаг 3. Интеграция контекстно-зависимого сценария 'Вечер'

    рактика: Шаг 3. Интеграция контекстно-зависимого сценария 'Вечер'

    Этот сценарий на основе `cron-plus` (из модуля `M06`) запускается по заходу солнца. Ранее он мог напрямую включать какую-то световую сцену. Теперь он будет запрашивать состояние `EVENING`.

    > ℹ️ Информация: Перевод системы в состояние `EVENING` гораздо мощнее, чем простое включение света. На это состояние могут быть "подписаны" и другие системы: например, шторы могут закрыться, а музыкальный плейлист смениться на вечерний. FSM позволяет централизованно анонсировать это событие.

    Часть A: Модификация потока-триггера

  • Найдите поток с узлом `cron-plus`, настроенным на `sunset`.
  • После него должен стоять `function` узел ("Check & Request EVENING"). Его задача — проверить, есть ли кто-то дома, и если да, запросить переход в вечерний режим.
  • // Этот код выполняется по заходу солнца
    

    const systemState = flow.get("systemState") || { state: "UNKNOWN" };

    // Вечерний режим имеет смысл только если кто-то дома (состояние HOME или уже EVENING)

    const allowedStates = ["HOME", "GUESTS"];

    if (allowedStates.includes(systemState.state)) {

    // Запрашиваем переход в состояние EVENING

    msg.payload = {

    command: "setState",

    payload: {

    state: "EVENING",

    priority: 25,

    source: "Sunset Scenario"

    }

    };

    msg.topic = "system/fsm/command";

    return msg;

    } else {

    // Никого нет дома или активен более приоритетный режим — ничего не делаем

    node.status({ fill:"grey", shape:"dot", text:"Blocked, state is " + systemState.state});

    return null;

    }

  • Подключите этот `function` к `mqtt out` (`system/fsm/command`) и удалите прямые команды управления светом.
  • Часть B: Добавление действий для 'EVENING'

    Снова возвращаемся в центральный поток действий.

  • От третьего выхода узла `switch` ("Route by State") строим логику для вечера.
  • Узел `function` (имя: "Prepare DALI Evening Scene"):
  •     msg.payload = {

    command: "scene",

    address: "g1", // Группа светильников гостиной

    value: 5, // Вызвать сцену №5 («Вечер»)

    fadeTime: 10 // Плавное включение за 10 секунд

    };

    node.status({ fill:"magenta", shape:"ring", text:"Executing EVENING actions!" });

    return msg;

  • Узел `node-red-contrib-dali-lighting` (или аналогичный): принимает `msg` и отправляет команду в шину DALI.
  • Часть C: UX и обработка ручного вмешательства (Override)

    > ⚠️ Важно: Переход в состояние `EVENING` не должен вызывать дискомфорт у пользователей, если они уже переопределили освещение вручную (например, включив яркий свет для чтения).

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

    Чек-лист проверки сценария 'Вечер'

    Практика: Шаг 4. Сквозное тестирование интегрированной системы

    рактика: Шаг 4. Сквозное тестирование интегрированной системы

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

    Шаг 1: Использование «виртуального пульта»

    Узлы `inject` — ваш главный инструмент для unit- и интеграционного тестирования в Node-RED. Создайте на отдельной вкладке "Testing" паттерн из нескольких узлов `inject`, которые будут имитировать реальные события. Это позволит проверить реакцию системы на любые вводные данные:

    * `Topic`: `hi/devices/leak_detector/bathroom/state`

    * `Payload` (JSON): `{"value": true, "source": "VIRTUAL_leak_detector_bathroom"}`

    `Topic`: тот, что на входе вашего `trigger` узла, например `hi/devices/motion/living_room/state`*

    * `Payload` (JSON): `{"event": "no_motion_for_15_min"}`

    Примечание:* для быстрого теста узла `trigger` временно уменьшите задержку с 15 минут до 10 секунд.

    * Используйте кнопку на узле `cron-plus` (вкладка Inject в свойствах узла) или создайте отдельный `inject` с соответствующей строкой события. * `Topic`: `system/action/reset_leak`

    * `Payload` (JSON): `{"action": "reset", "initiator": "VIRTUAL_admin"}`

    Шаг 2: Анализ журнала событий FSM и проверка Override

    Журнал событий конечного автомата (созданный нами ранее в рамках сквозного пути по курсу), теперь становится бесценным инструментом для отладки логики арбитража. Проведите последовательный тест (Test Plan), чтобы проверить механизм Override:

  • Создание аварии: С помощью `inject` симулируйте протечку в ванной.
  • Проверка перехода: Зайдите в вашу базу данных `event_log` (или консоль/лог-файл Node-RED) и проверьте цепочку принятия решений:
  •     INFO: State transition requested. Source: VIRTUAL_leak_detector_bathroom, Target: LEAK, Priority: 100.

    INFO: Current state is HOME (Priority 20). New priority is higher. Transition ALLOWED.

    SUCCESS: System state changed from HOME to LEAK.

  • Тестирование блокировки (Override test): Чрезвычайно важный шаг. Не сбрасывая состояние `LEAK`, попробуйте симулировать отсутствие (нажав `inject` для `no_motion`), а затем наступление ночи.
  • Подтверждение арбитража: Снова посмотрите в лог. Ожидаемое поведение — жесткая блокировка низкоприоритетных команд:
  •     INFO: State transition requested. Source: Absence Detector Scenario, Target: AWAY, Priority: 10.

    WARN: Current state is LEAK (Priority 100). New priority is lower. Transition DENIED.

  • Сброс аварии: Отправьте команду сброса и убедитесь, что система корректно возвращается в базовое состояние (`HOME` или `AWAY`, в зависимости от текущих условий).
  • Этот тест доказывает работоспособность архитектуры: конечный автомат (FSM) корректно выполняет арбитраж и предотвращает применение штатных или низкоприоритетных сценариев во время аварии (логика Override отрабатывает идеально, надежно защищая дом).

    Шаг 3: Маршрутизация и использование `debug` узлов

    Для визуального контроля прохождения сигналов в реальном времени расставьте узлы `debug` в критических точках маршрута:

    > 💡 Подсказка: В свойствах узла `debug` измените настройку Output с `msg.payload` на `complete msg object`. Это позволит вам видеть не только тело сообщения, но и `msg.topic`, что критически важно при отладке MQTT-маршрутизации.

    > ⚠️ Важно: После успешного завершения сквозного тестирования обязательно деактивируйте (кнопкой на самом узле) или удалите вспомогательные `debug` узлы. Оставленные активными, они будут забивать системный журнал Node-RED и впустую расходовать ресурсы процессора (CPU и RAM) контроллера в production-среде.

    Заключение: Система в сборе

    аключение: Система в сборе

    Поздравляем! Вы завершили самый важный этап — сборку. Мы взяли разрозненные, но функциональные потоки и оркестровали их с помощью центрального конечного автомата. Мы на практике реализовали паттерн "Запрос → Арбитраж → Действие".

    Ваш путь по курсу

    Оглядываясь назад, важно оценить пройденный путь по курсу. Архитектура умного дома не появилась из ниоткуда, она стала результатом последовательных шагов по проектированию и интеграции. Наша ретроспектива охватывает ключевые этапы (от M01 до M09):

    Ключевой результат этой работы:

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

    > 💡 Связанный материал: Обновите ваш граф состояний из модуля M01. Теперь он не просто теоретическая схема, а точное отражение работающей системы. Эта живая документация будет незаменима при дальнейшей поддержке и развитии проекта.

    Что дальше?

    На следующем, финальном, шаге нашего проекта мы создадим простой, но эффективный пользовательский интерфейс с помощью `node-red-dashboard`. Мы разработаем дашборд, который позволит пользователю: