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

Практика: Flow для режима 'Отпуск' с имитацией присутствия

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

Введение и архитектура Flow

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

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

> 🔗 Связанный материал: Для полного понимания логики приоритетов, на которой строится этот урок, настоятельно рекомендуется повторить материал урока COURSE-07-M03-L04 «Реализация механизма приоритетов и блокировок в Node-RED». Концепция глобальных состояний была подробно рассмотрена в уроке COURSE-07-M03-L01 «Режим 'Отпуск': имитация присутствия, отключение потребителей, климат-контроль».

Архитектура потока

Чтобы избежать создания запутанного и сложного для отладки «спагетти-flow», мы разделим нашу задачу на четыре логических блока, которые будут реализованы на одной или нескольких вкладках Node-RED и связаны между собой с помощью узлов `link in` / `link out` или через общие MQTT-топики:

  • Блок управления состоянием (State Manager): Это «мозг» всего режима. Он принимает команды на активацию/деактивацию, устанавливает глобальное состояние системы и запускает/останавливает другие блоки.
  • Блок отключения нагрузок (Power Saver): При активации режима этот блок отправляет команды на выключение всех второстепенных потребителей (розетки, медиа-системы) и переводит систему климат-контроля в экономичный режим.
  • Блок имитации присутствия (Presence Simulator): Ядро этого блока — генератор случайных событий, который в вечернее время включает и выключает свет в разных комнатах, создавая видимость, что в доме кто-то есть.
  • Блок управления приоритетами (Priority Lock): При активации режима система должна «захватить» управление, установив глобальный флаг-блокировку. Этот флаг будет проверяться другими сценариями (например, «Вечерний свет по датчику движения»), чтобы предотвратить их срабатывание, пока активен режим «Отпуск».
  • Упрощенная архитектурная схема:
    [MQTT In: hi/modes/vacation/set]
    

    |

    v

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

    | 1. State Manager | --("ON")--> [Link Out: Start Vacation]

    | (Управляет состоянием | --("OFF")--> [Link Out: End Vacation]

    | `global.vacationMode`) |

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

    |

    (При любом изменении)

    |

    v

    [MQTT Out: hi/modes/vacation/state] (Обратная связь для UX)

    [Link In: Start Vacation] -> +--------------------------+

    | 2. Power Saver | -> Отправка команд на отключение розеток, климата

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

    | 3. Presence Simulator | -> Запуск цикла имитации

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

    | 4. Priority Lock | -> Установка `global.scenarioLock = 'vacation'`

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

    [Link In: End Vacation] -> +--------------------------+

    | Восстановление нагрузок | -> Включение розеток, возврат климата в авто-режим

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

    | Остановка имитации | -> Отправка стоп-сигнала в симулятор

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

    | Снятие блокировки | -> Установка `global.scenarioLock = 'none'`

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

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

    ---

    Управление состоянием режима «Отпуск»

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

    Точка входа: MQTT

    Создадим точку входа для управления режимом. Это будет узел `mqtt in`, подписанный на определенный топик.

  • Разместите на холсте узел `mqtt in`.
  • Настройте его параметры:
  • * Server: Выберите ваш настроенный MQTT-брокер.

    * Topic: `hi/modes/vacation/set`

    * QoS: `1` — для гарантированной доставки команды.

    * Output: `a string`

    Этот узел будет принимать сообщения с `payload`, равным `ON` или `OFF`.

    Маршрутизация и сохранение состояния

    Полученную команду нужно обработать: изменить состояние и направить поток по нужной ветке логики.

  • После узла `mqtt in` поставьте узел `switch`. Он будет выполнять роль маршрутизатора. Настройте его для проверки `msg.payload`:
  • * Правило 1: `==` (строка) `ON`

    * Правило 2: `==` (строка) `OFF`

  • К первому выходу узла `switch` подключите узел `change`. Его задача — установить глобальную переменную и флаг блокировки.
  • * Правило 1: `set` `global.vacationMode` to `true` (boolean).

    * Правило 2: `set` `global.scenarioLock` to `'vacation'` (string).

  • Ко второму выходу узла `switch` подключите другой узел `change`.
  • * Правило 1: `set` `global.vacationMode` to `false` (boolean).

    * Правило 2: `set` `global.scenarioLock` to `'none'` (string).

    > 💡 Подсказка: Используйте `global.set('vacationMode', true, 'file')` для немедленной записи состояния в файловый контекст. Это гарантирует, что даже при внезапном сбое питания контроллер «запомнит» активный режим. Чтобы эта функция работала, необходимо настроить персистентный контекст.

    > ℹ️ Для сохранения состояния между перезагрузками необходимо использовать персистентный контекст. Настройка хранилища в файле `settings.js` подробно рассмотрена в уроке COURSE-07-M01-L05. В коде это реализуется через указание имени хранилища: `global.get('myVar', 'file')`.

    Итоговая схема блока управления состоянием:
    [mqtt in: hi/modes/vacation/set] -> [switch: ON/OFF] --+-- [change: set global.vacationMode=true, global.scenarioLock='vacation'] -> [link out: Start Vacation]
    

    |

    +-- [change: set global.vacationMode=false, global.scenarioLock='none'] -> [link out: End Vacation]

    Эта простая, но надежная конструкция является фундаментом для всего сценария.

    ---

    Практика: Реализация имитации присутствия

    Имитация присутствия — ключевая функция безопасности в режиме «Отпуск». Наша задача — создать непредсказуемый, но правдоподобный паттерн включения и выключения света в разных частях дома в вечернее время. Мы будем использовать надежный и легко читаемый паттерн "асинхронного цикла" на базе узлов `function` и `delay`.

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

    Создание асинхронного цикла

    Этот паттерн разделяет логику генерации событий и их исполнение.

  • Запуск и остановка цикла:
  • * Создайте узел `link in` с именем `Start Vacation`. Он будет получать сигнал на запуск нашего симулятора.

    * К нему подключите узел `inject`, настроенный на отправку одного сообщения при получении сигнала на вход (`Inject once after 0.1 seconds, then nothing`). Это даст первоначальный толчок для запуска цикла.

    * Создайте второй узел `link in` с именем `End Vacation`. Он będzie останавливать цикл, сбрасывая состояние узлов `delay`.

  • Генератор событий (узел `function`):
  • Это ядро симулятора. Подключите его после `inject`. Узел будет настроен на 2 выхода:

    * Выход 1: Отправляет команды на управление светом (включение и выключение).

    * Выход 2: Отправляет "служебное" сообщение для управления самим циклом (задержка до следующего события).

    Пример кода для узла `function` "Presence Simulator Logic":
    // Список групп освещения, участвующих в имитации
    

    const lightGroups = [

    { name: 'living_room', topic: 'hi/lighting/living_room/set' },

    { name: 'kitchen', topic: 'hi/lighting/kitchen/set' },

    { name: 'hallway', topic: 'hi/lighting/hallway_main/set' }

    ];

    // Функция для получения случайного числа в диапазоне

    function getRandomInt(min, max) {

    min = Math.ceil(min);

    max = Math.floor(max);

    return Math.floor(Math.random() * (max - min + 1)) + min;

    }

    const now = new Date();

    const currentHour = now.getHours();

    // --- 1. Проверка времени ---

    // Имитация работает только с 19:00 до 23:00

    if (currentHour < 19 || currentHour >= 23) {

    // Если еще не вечер или уже поздно, ждем до следующего вечера

    const tomorrow19 = new Date();

    tomorrow19.setHours(19, 0, 0, 0);

    if (now.getTime() > tomorrow19.getTime()) {

    tomorrow19.setDate(tomorrow19.getDate() + 1); // Уже прошло 19:00, планируем на завтра

    }

    const waitTime = tomorrow19.getTime() - now.getTime();

    node.status({ fill: "grey", shape: "dot", text: "Waiting for next evening" });

    // Отправляем на выход 2 сообщение для цикла с задержкой до вечера

    return [null, { payload: 'wait', delay: waitTime }];

    }

    // --- 2. Выбор случайного света и параметров ---

    const randomGroup = lightGroups[getRandomInt(0, lightGroups.length - 1)];

    const randomBrightness = getRandomInt(60, 100); // Случайная яркость от 60% до 100%

    const durationOn = getRandomInt(5, 20) 60 1000; // Включен от 5 до 20 минут (в мс)

    const nextInterval = getRandomInt(10, 30) 60 1000; // Следующее событие через 10-30 минут (в мс)

    // --- 3. Формирование команд на управление светом (для Выхода 1) ---

    // Команда на включение, уходит немедленно

    const on_command = {

    topic: randomGroup.topic,

    payload: JSON.stringify({ state: "ON", brightness: randomBrightness, transition: 5 })

    };

    // Команда на выключение, уходит с задержкой, которую обработает узел `delay`

    const off_command = {

    topic: randomGroup.topic,

    payload: JSON.stringify({ state: "OFF", transition: 5 }),

    delay: durationOn // Узел 'delay' использует это свойство

    };

    // --- 4. Формирование сообщения для цикла (для Выхода 2) ---

    // Это сообщение запустит следующий цикл симуляции через `nextInterval`

    const loop_command = {

    payload: 'next_tick',

    delay: nextInterval // Задержка до следующего события

    };

    node.status({

    fill: "blue", shape: "dot",

    text: `Light: ${randomGroup.name} for ${Math.round(durationOn/60000)}m. Next in ${Math.round(nextInterval/60000)}m.`

    });

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

    // Выход 1: массив команд для света

    // Выход 2: сообщение для цикла

    return [ [on_command, off_command], loop_command ];

    Настройте узел `function` на 2 выхода.
  • Исполнительный механизм (`delay` и `mqtt out`):
  • * Управление светом: Подключите первый выход узла `function` к узлу `delay`. В настройках `delay` выберите `Action: Delay message based on msg.delay`. После него поставьте `mqtt out`. Эта простая связка обработает и немедленную команду `ON`, и отложенную команду `OFF`.

    * Управление циклом: Подключите второй выход узла `function` ко второму узлу `delay`, настроенному так же (`Action: Delay message based on msg.delay`). Выход этого `delay` узла заведите обратно на вход узла `function`. Так мы создаем бесконечный асинхронный цикл.

    * Остановка: Узел `link in: End Vacation` подключите к входам `reset` обоих узлов `delay`. Это очистит все ожидающие сообщения (команды OFF и следующий запуск цикла), немедленно и чисто останавливая симуляцию.

    Схема симулятора:
                                    (Запуск)
    

    |

    v

    [link in: Start] -> [inject] -> +---------------------------+

    | function: Simulator Logic |

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

    | |

    (Выход 1: Команды света) (Выход 2: Управление циклом)

    | |

    v v

    [delay: on msg.delay] [delay: on msg.delay] -----+

    | | (обратная связь) |

    v +--------------------+

    [link in: End] -> [Reset] -> [mqtt out]

    |

    +------------> [Reset] -> (вход узла `delay` цикла)

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

    ---

    Интеграция с приоритетами и управление нагрузками

    Теперь, когда у нас есть работающий режим, его нужно правильно «вписать» в экосистему умного дома. Это означает, что при активации «Отпуска» все остальные, менее приоритетные сценарии, должны быть заблокированы.

    > 🔗 Связанный материал: Механизм блокировок, который мы здесь применяем, детально разобран в уроке COURSE-07-M03-L05 «Ручной Override: как временно отключить автоматику и корректно вернуться».

    Захват приоритета и отключение потребителей

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

  • Установка блокировки: В блоке управления состоянием мы уже настроили узел `change` для установки `global.set('scenarioLock', 'vacation', 'file')`. Это — наш сигнал для всей системы, что главный приоритет теперь у режима «Отпуск».
  • Отключение нагрузок: От узла `link in: Start Vacation` создайте последовательность узлов, которые отправят команды на отключение. Для этого можно использовать несколько узлов `change` и `mqtt out` или один узел `function`.
  • Пример узла `function` "Shutdown Non-Essentials":
    // Список топиков для отключения и перехода в эконом-режим
    

    const shutdownTargets = [

    // Отключение медиа-зоны

    { topic: 'hi/sockets/media_center/set', payload: { state: 'OFF' } },

    // Отключение розеток в кабинете

    { topic: 'hi/sockets/office_desk/set', payload: { state: 'OFF' } },

    // Перевод климата в эконом-режим (+18С зимой, +28С летом)

    { topic: 'hi/climate/main/set_mode', payload: 'eco' },

    { topic: 'hi/climate/main/set_temp', payload: 18 },

    // Отключение водонагревателя

    { topic: 'hi/appliances/water_heater/set', payload: { state: 'OFF' } }

    ];

    // Для Node-RED > 1.0 можно вернуть массив сообщений

    // Каждое сообщение будет отправлено как отдельное

    // Это позволяет отправить команды параллельно

    node.send(shutdownTargets);

    // Для старых версий Node-RED нужно использовать цикл

    // for (const cmd of shutdownTargets) {

    // node.send({ topic: cmd.topic, payload: cmd.payload });

    // }

    // Отображаем статус

    node.status({ fill: "yellow", shape: "ring", text: "Shutting down consumers..." });

    // Возвращаем null, т.к. сообщения отправлены через node.send()

    return null;

    Подключите этот `function` к узлу `mqtt out`. Узел отправит все команды, сформированные в массиве.

    Проверка блокировки в других сценариях

    Теперь в каждом сценарии, который не должен работать в режиме «Отпуск» (например, включение света по движению), необходимо добавить проверку. В самом начале потока этого сценария поставьте узел `switch` или `function`.

    Пример `function` узла "Check Scenario Lock":
    // Получаем текущий статус блокировки
    

    const lock = global.get('scenarioLock', 'file') || 'none';

    // Если блокировка установлена НЕ нами и НЕ снята, останавливаем поток

    if (lock !== 'none') {

    node.status({ fill: "grey", shape: "ring", text: `Blocked by: ${lock}` });

    return null; // Прервать выполнение сценария

    }

    // Если блокировки нет, продолжаем выполнение

    node.status({ fill: "green", shape: "dot", text: "Lock check: OK" });

    return msg;

    Этот простой узел, добавленный в начало каждого второстепенного сценария (например, `FLOW-AUTO-LIGHT-012`), делает всю систему предсказуемой и управляемой.

    Корректное освобождение (деактивация режима)

    При получении команды `OFF` мы должны выполнить обратные действия:

  • Снять блокировку: Установить `global.set('scenarioLock', 'none', 'file')`. Мы это уже настроили в блоке управления состоянием.
  • Остановить симулятор: Отправить сигнал на `link out: End Vacation`, который сбросит триггеры симулятора.
  • Вернуть потребителей в авто-режим: Отправить команды для перевода климата в `auto` и, при необходимости, включить некоторые розетки.
  • Для реализации пункта 3 создадим узел `function` "Restore Consumers" и подключим его после узла `link in: End Vacation`.

    // Список топиков для возврата в обычный режим
    

    const restoreTargets = [

    // Возврат климата в автоматический режим

    { topic: 'hi/climate/main/set_mode', payload: 'auto' },

    // Включение водонагревателя обратно

    { topic: 'hi/appliances/water_heater/set', payload: { state: 'ON' } }

    ];

    node.send(restoreTargets);

    node.status({ fill: "green", shape: "ring", text: "Restoring consumers..." });

    return null;

    И направим его выход в общий узел `mqtt out`.

    ---

    Итоги, тестирование и обратная связь для пользователя

    Мы создали многокомпонентный, но логичный и структурированный flow для управления режимом «Отпуск». Он включает в себя управление состоянием, имитацию присутствия, энергосбережение и интеграцию с системой приоритетов.

    > ℹ️ Информация: Не забывайте активно использовать узел `debug` на каждом этапе разработки. Настройте его на вывод полного объекта `msg` на выходах ключевых узлов (`function`, `switch`, `mqtt in`), чтобы видеть все свойства сообщения и корректно отлаживать логику.

    Стратегия тестирования

    Для проверки работоспособности сценария выполните следующие шаги:

  • Активация: Отправьте в топик `hi/modes/vacation/set` сообщение `ON`.
  • * Проверка:

    * В логах или через `debug` убедитесь, что `global.vacationMode` стал `true`.

    * Убедитесь, что `global.scenarioLock` установился в `vacation`.

    * Проверьте, что на соответствующие MQTT-топики ушли команды на отключение розеток и перевод климата в `eco`.

    * Проверьте, что симулятор присутствия запустился (в узле `function` симулятора появится статус).

  • Работа имитации (в вечернее время):
  • * Проверка:

    * Наблюдайте за логами MQTT или физическими устройствами: свет должен включаться и выключаться в соответствии с логикой симулятора.

    * Проверьте, что интервалы и выбор групп света носят случайный характер.

  • Проверка блокировки:
  • * Проверка: Попробуйте активировать сценарий, который должен быть заблокирован (например, пройдите мимо датчика движения). Убедитесь, что свет не включился, а в статусе узла проверки блокировки появилось сообщение `Blocked by: vacation`.

  • Деактивация: Отправьте в топик `hi/modes/vacation/set` сообщение `OFF`.
  • * Проверка:

    * Убедитесь, что `global.vacationMode` стал `false`.

    * Критически важно: Убедитесь, что `global.scenarioLock` сбросился в `none`.

    * Проверьте, что симулятор остановился.

    * Проверьте, что климат-контроль вернулся в режим `auto`.

    * Повторно проверьте работу сценария по датчику движения — теперь он должен сработать.

    Обеспечение UX (User Experience)

    Пользователь должен получать четкую обратную связь о состоянии системы. Добавьте в самый конец блока управления состоянием узел `mqtt out` для отправки статуса:

    Чтобы сформировать такой payload, перед узлом `mqtt out` поставьте узел `function`:

    // Формируем сообщение статуса
    

    const isActive = global.get('vacationMode') === true;

    msg.payload = {

    active: isActive,

    timestamp: Date.now()

    };

    return msg;

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

    Заключение

    Построение комплексных сценариев, таких как режим «Отпуск», требует строгого архитектурного подхода. Разделение логики на независимые блоки (управление состоянием, симуляция, энергосбережение) позволяет избежать хаоса в flow. Использование персистентного контекста гарантирует устойчивость системы к перезагрузкам, а механизм глобальных блокировок (`scenarioLock`) бесшовно интегрирует новый режим в существующую экосистему умного дома, не допуская конфликта сценариев. Успешно реализовав этот паттерн, вы получили универсальную базу для создания любых других сложных режимов работы.

    Что дальше

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