Практика: Flow для режима 'Отпуск' с имитацией присутствия
Введение и архитектура Flow
Данный урок посвящен практической реализации одного из самых востребованных и комплексных сценариев в умном доме — режима «Отпуск». Цель урока — спроектировать и создать в среде Node-RED полнофункциональный поток (flow), который не просто активирует режим, но и управляет имитацией присутствия, обеспечивает энергосбережение и, что наиболее важно, корректно интегрируется в общую систему приоритетов автоматизации объекта.
Мы объединим теоретические знания, полученные ранее, в единый работающий механизм. Этот сценарий — отличный пример того, как взаимодействуют несколько подсистем: управление освещением, розетками, климатом и глобальными состояниями.
> 🔗 Связанный материал: Для полного понимания логики приоритетов, на которой строится этот урок, настоятельно рекомендуется повторить материал урока COURSE-07-M03-L04 «Реализация механизма приоритетов и блокировок в Node-RED». Концепция глобальных состояний была подробно рассмотрена в уроке COURSE-07-M03-L01 «Режим 'Отпуск': имитация присутствия, отключение потребителей, климат-контроль».
Архитектура потока
Чтобы избежать создания запутанного и сложного для отладки «спагетти-flow», мы разделим нашу задачу на четыре логических блока, которые будут реализованы на одной или нескольких вкладках Node-RED и связаны между собой с помощью узлов `link in` / `link out` или через общие MQTT-топики:
[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`, подписанный на определенный топик.
* Server: Выберите ваш настроенный MQTT-брокер.
* Topic: `hi/modes/vacation/set`
* QoS: `1` — для гарантированной доставки команды.
* Output: `a string`
Этот узел будет принимать сообщения с `payload`, равным `ON` или `OFF`.
Маршрутизация и сохранение состояния
Полученную команду нужно обработать: изменить состояние и направить поток по нужной ветке логики.
* Правило 1: `==` (строка) `ON`
* Правило 2: `==` (строка) `OFF`
* Правило 1: `set` `global.vacationMode` to `true` (boolean).
* Правило 2: `set` `global.scenarioLock` to `'vacation'` (string).
* Правило 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`.
Это ядро симулятора. Подключите его после `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 выхода.
* Управление светом: Подключите первый выход узла `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: как временно отключить автоматику и корректно вернуться».
Захват приоритета и отключение потребителей
При активации режима мы должны выполнить два действия: установить флаг блокировки и отправить команды на отключение оборудования.
// Список топиков для отключения и перехода в эконом-режим
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` мы должны выполнить обратные действия:
Для реализации пункта 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`), чтобы видеть все свойства сообщения и корректно отлаживать логику.
Стратегия тестирования
Для проверки работоспособности сценария выполните следующие шаги:
* Проверка:
* В логах или через `debug` убедитесь, что `global.vacationMode` стал `true`.
* Убедитесь, что `global.scenarioLock` установился в `vacation`.
* Проверьте, что на соответствующие MQTT-топики ушли команды на отключение розеток и перевод климата в `eco`.
* Проверьте, что симулятор присутствия запустился (в узле `function` симулятора появится статус).
* Проверка:
* Наблюдайте за логами MQTT или физическими устройствами: свет должен включаться и выключаться в соответствии с логикой симулятора.
* Проверьте, что интервалы и выбор групп света носят случайный характер.
* Проверка: Попробуйте активировать сценарий, который должен быть заблокирован (например, пройдите мимо датчика движения). Убедитесь, что свет не включился, а в статусе узла проверки блокировки появилось сообщение `Blocked by: vacation`.
* Проверка:
* Убедитесь, что `global.vacationMode` стал `false`.
* Критически важно: Убедитесь, что `global.scenarioLock` сбросился в `none`.
* Проверьте, что симулятор остановился.
* Проверьте, что климат-контроль вернулся в режим `auto`.
* Повторно проверьте работу сценария по датчику движения — теперь он должен сработать.
Обеспечение UX (User Experience)
Пользователь должен получать четкую обратную связь о состоянии системы. Добавьте в самый конец блока управления состоянием узел `mqtt out` для отправки статуса:
- Topic: `hi/modes/vacation/state`
- После активации отправляйте `{"active": true, "timestamp": 1678886400000}`.
- После деактивации отправляйте `{"active": false, "timestamp": 1678886500000}`.
Чтобы сформировать такой payload, перед узлом `mqtt out` поставьте узел `function`:
// Формируем сообщение статуса
const isActive = global.get('vacationMode') === true;
msg.payload = {
active: isActive,
timestamp: Date.now()
};
return msg;
Мобильное приложение или панель управления могут подписаться на этот топик и отображать актуальный статус режима, давая пользователю уверенность в том, что его команда принята и система работает как надо.
Заключение
Построение комплексных сценариев, таких как режим «Отпуск», требует строгого архитектурного подхода. Разделение логики на независимые блоки (управление состоянием, симуляция, энергосбережение) позволяет избежать хаоса в flow. Использование персистентного контекста гарантирует устойчивость системы к перезагрузкам, а механизм глобальных блокировок (`scenarioLock`) бесшовно интегрирует новый режим в существующую экосистему умного дома, не допуская конфликта сценариев. Успешно реализовав этот паттерн, вы получили универсальную базу для создания любых других сложных режимов работы.
Что дальше
В следующем уроке мы рассмотрим реализацию режима «Гость», который имеет иные цели: не блокировать автоматику, а предоставлять упрощенный и ограниченный набор сценариев для временных жильцов. Мы также рассмотрим, как разные режимы могут взаимодействовать друг с другом.