Практика: Реализация ручного override для группы света
Введение: Проблема ручного управления в автоматизированных системах
Автоматизация, особенно в контексте умного дома, призвана сделать жизнь комфортнее, предвосхищая потребности жильцов. Сценарии, основанные на датчиках движения, времени суток или других триггерах, эффективно справляются с рутинными задачами, такими как управление освещением. Однако существует фундаментальный конфликт: самая интеллектуальная автоматика не способна на 100% угадать намерения человека в каждый конкретный момент времени.
Представьте ситуацию: вы уютно устроились с книгой в кресле. Автоматика, не зафиксировав движения в течение 5 минут, решает, что в комнате никого нет, и гасит свет. Вам приходится махать рукой, чтобы "оживить" датчик. Это не комфорт, а раздражение. Именно для решения таких проблем и был разработан паттерн "Ручной Override" (Ручное Управление/Переопределение).
> 💡 Ключевая концепция: Этот урок закладывает основу для важнейшего принципа проектирования — иерархии приоритетов. Его суть в том, что команда от человека всегда должна быть главнее, чем автоматический сценарий. В этом практическом уроке мы реализуем этот принцип с нуля.
Основная задача — предоставить пользователю простой и интуитивно понятный способ временно "поставить на паузу" автоматический сценарий. Когда пользователь вручную включает или выключает свет с помощью настенного выключателя, система должна понять это как прямое указание и прекратить попытки управлять освещением по датчику движения. При этом, важно предусмотреть механизм возврата в автоматический режим, чтобы система не "зависла" в ручном управлении навсегда.
В рамках этой практической работы мы создадим комплексный поток в Node-RED, который решает эту задачу. Мы будем использовать следующие ключевые узлы:
- `mqtt in`: Для получения данных от физических устройств — датчика движения и настенного выключателя.
- `rbe` (Report by Exception): Для фильтрации повторяющихся сообщений, чтобы избежать "дребезга" сигналов и лишней нагрузки на систему.
- `trigger`: Для создания таймеров — как для выключения света по тайм-ауту, так и для автоматического сброса режима ручного управления.
- `function`: "Мозг" нашего сценария. Здесь будет реализована вся логика управления состоянием, установки флагов и формирования команд.
- `mqtt out`: Для отправки управляющих команд на исполнительное устройство — релейный модуль, управляющий группой света.
В результате вы получите готовый, надежный и масштабируемый паттерн, который можно будет адаптировать для управления не только освещением, но и климатом, шторами и другими подсистемами вашего объекта.
---
Практика: Создание базового сценария освещения
Прежде чем внедрять сложную логику ручного управления, необходимо создать основу — простой автоматический сценарий включения света по движению и выключения по тайм-ауту. Это позволит нам пошагово тестировать и усложнять систему.
Цель: Свет в гостиной должен включаться при обнаружении движения и выключаться, если движение не фиксируется в течение 5 минут.Шаг 1: Создание потока и настройка входных данных
* Сервер (Broker): Выберите ваш настроенный MQTT-брокер.
* Топик: `hi/devices/pir_living_room/motion`
* Выход (Output): `разобранный JSON объект`
* Имя: `Движение в гостиной`
Мы предполагаем, что датчик отправляет сообщения в формате JSON, соответствующем нашему контракту сообщений:
{
"motion": true,
"battery": 87,
"source": "pir_living_room_01",
"ts": 1678886400000
}
Шаг 2: Фильтрация и запуск таймера
* Свойство: `msg.payload.motion`
* Правило: `является true`
* Отправить: (первое сообщение) — JSON `{ "state": "ON" }`
* затем, после: `5 минут`
* отправить: (второе сообщение) — JSON `{ "state": "OFF" }`
* Действие при получении нового сообщения: `продлить таймер`
* Имя: `Таймер на 5 минут`
Эта конфигурация работает следующим образом:
- При первом обнаружении движения (`motion: true`) узел `trigger` немедленно отправит сообщение `{"state": "ON"}`.
- Одновременно он запустит 5-минутный таймер.
- Если в течение этих 5 минут придет новое сообщение о движении, таймер сбросится и начнет отсчет заново.
- Если за 5 минут новых сообщений о движении не поступит, узел отправит сообщение `{"state": "OFF"}`.
Шаг 3: Формирование и отправка команды
* Имя: `Формат для реле`
* Код:
// Входящее msg.payload: { "state": "ON" } или { "state": "OFF" }
// Устанавливаем топик для группы света в гостиной
msg.topic = 'hi/devices/light_group_living_room/set';
// Payload уже в правильном формате, но мы можем добавить
// метаданные для аудита.
msg.audit = {
trigger: "auto_motion",
source: "pir_living_room_01",
requested_state: msg.payload.state
};
// Визуальный статус для отладки
if (msg.payload.state === "ON") {
node.status({ fill: "green", shape: "dot", text: "Включить по движению" });
} else {
node.status({ fill: "red", shape: "dot", text: "Выключить по таймеру" });
}
return msg;
* Сервер (Broker): Выберите ваш MQTT-брокер.
* Топик: Оставьте пустым, так как он устанавливается в узле `function`.
* Имя: `Управление светом гостиной`
ASCII-схема базового потока:[mqtt in]─────────[switch]─────────[trigger]─────────[function]─────────[mqtt out]
(Движение) (msg.payload.motion) (Таймер 5 мин) (Формат для реле) (Управление светом)
Теперь у вас есть рабочий сценарий автоматического управления светом. Протестируйте его: сымитируйте отправку MQTT-сообщения о движении и убедитесь, что свет включается, а через 5 минут (или установленное вами время) — выключается.
---
Реализация логики Override с помощью контекста Flow
Теперь, когда у нас есть работающая автоматика, добавим возможность ручного управления и механизм ее блокировки.
> 💡 Подсказка: Используйте `flow` контекст вместо `global`, чтобы изолировать логику override в рамках одного сценария. Это предотвратит случайные конфликты между разными автоматизациями и сделает ваш проект более модульным — это ключевой принцип для создания надежных и масштабируемых решений, таких как переиспользуемые компоненты (subflows).
Шаг 1: Добавление ручного управления
* Топик: `hi/devices/wall_switch_living_room/action`
* Имя: `Настенный выключатель`
Предположим, выключатель при нажатии отправляет сообщение с `msg.payload`, равным `{"action": "toggle"}`.
Шаг 2: Создание флага Override
* Имя: `Ручное управление + Override`
* Код:
// Получаем текущее состояние флага override из контекста потока
let isOverrideActive = flow.get('manualOverride') || false;
// Получаем последнее известное состояние света (если оно есть)
let lastLightState = flow.get('lightState') || "OFF";
// 1. Активируем режим ручного управления
// При любом нажатии на физический выключатель, мы переходим в ручной режим
flow.set('manualOverride', true);
// 2. Определяем новое состояние света (логика "toggle")
let newLightState = (lastLightState === "ON") ? "OFF" : "ON";
flow.set('lightState', newLightState); // Сохраняем новое состояние
// 3. Формируем команду для реле
msg.payload = { "state": newLightState };
msg.topic = 'hi/devices/light_group_living_room/set';
// 4. Добавляем данные для аудита
msg.audit = {
trigger: "manual_switch",
source: "wall_switch_living_room_01",
requested_state: newLightState
};
// 5. Устанавливаем визуальный статус
node.status({ fill: "blue", shape: "ring", text: `Override: ${newLightState} в ${new Date().toLocaleTimeString()}` });
return msg;
Шаг 3: Блокировка автоматики
Теперь самое главное: нужно заставить автоматический сценарий "уважать" флаг `manualOverride`.
* Имя: `Проверка Override`
* Свойство: `flow.manualOverride` (выберите тип `flow`)
* Правило 1: `is false` или `is undefined`. Соедините этот выход (`1`) с узлом `trigger`.
* Правило 2: `иначе` (`is true`). Этот выход (`2`) никуда не подключайте. Он будет отбрасывать сообщения.
Теперь, когда `flow.manualOverride` установлен в `true`, сообщения от датчика движения не смогут запустить или продлить таймер `trigger`, и автоматика будет эффективно заблокирована.
Обновленная ASCII-схема:// Автоматический поток
[mqtt in]────[Проверка Override]──┬──[trigger]──[function]──┬──[mqtt out]
(Движение) (switch) | (Таймер) (Формат) | (Свет)
| |
v отброс |
(сообщения блокированы) |
|
// Ручной поток |
[mqtt in]────────[function: "Ручное управление"]────────────+
(Выключатель) (установка flow.manualOverride)
Протестируйте систему: включите свет по движению, затем нажмите настенный выключатель, чтобы его выключить. Теперь, даже если вы будете двигаться, свет не должен включаться, так как система находится в режиме ручного управления.
---
Механизмы возврата в автоматический режим
Заблокировать автоматику — это половина дела. Не менее важно обеспечить надежный и предсказуемый способ вернуться в автоматический режим. Существует два основных подхода: автоматический (по тайм-ауту) и ручной (по действию пользователя).
> ⚠️ Внимание: Критически важно обеспечить пользователю обратную связь. Если ручной режим активен, это должно быть визуально понятно — например, с помощью светодиодной индикации на выключателе или специальной иконки в мобильном приложении. Отсутствие обратной связи ведет к непредсказуемому поведению системы и разочарованию жильцов.
1. Автоматический сброс по таймеру
Этот механизм является "страховкой" от ситуации, когда пользователь активировал ручной режим и забыл про это.
* Имя: `Ручное управление + Запуск сброса`
* Выходы: `2`
* Код:
// ... (код из предыдущего шага) ...
// 3. Формируем команду для реле
msg.payload = { "state": newLightState };
msg.topic = 'hi/devices/light_group_living_room/set';
// ... (код аудита и статуса) ...
// Создаем второе сообщение для запуска таймера сброса
let resetTimerMsg = { payload: "start_reset_timer" };
// Отправляем команду на свет на первый выход, а команду на запуск таймера - на второй
return [msg, resetTimerMsg];
* Имя: `Таймер сброса Override (2ч)`
* Отправить: `ничего` (не отправлять сразу)
* затем, после: `2 часов`
* отправить: `{ "action": "reset_override" }`
* Действие при получении: `запустить/перезапустить таймер`
* Имя: `Сброс флага Override`
* Код:
// Сбрасываем флаг, возвращая систему в автоматический режим
flow.set('manualOverride', false);
node.status({ fill: "green", shape: "dot", text: `Авто-сброс в ${new Date().toLocaleTimeString()}` });
// Важно! После сброса нужно синхронизировать состояние света.
// Если в комнате есть движение, свет должен остаться включенным.
// Эту логику мы рассмотрим в разделе "Синхронизация состояния".
// Пока что просто сбрасываем флаг.
// Останавливаем поток, чтобы это сообщение не пошло на реле
return null;
2. Ручной сброс
Пользователь должен иметь возможность сам вернуть автоматику, не дожидаясь таймера. Для этого можно использовать, например, двойное нажатие клавиши.
3. Синхронизация состояния
Когда `override` сбрасывается, может возникнуть неприятная ситуация: в комнате есть люди, а свет гаснет, потому что автоматика "не в курсе". Решение — при сбросе флага проверить текущее состояние датчика движения.
* Имя: `Сохранить состояние движения`
* Код: `flow.set('lastMotionState', msg.payload.motion); return msg;`
* Код:
flow.set('manualOverride', false);
node.status({ fill: "green", shape: "dot", text: `Возврат в Авто-режим в ${new Date().toLocaleTimeString()}` });
let lastMotionState = flow.get('lastMotionState') || false;
// Если после сброса в комнате есть движение,
// нужно заново запустить логику автоматического включения.
if (lastMotionState === true) {
// Формируем сообщение, идентичное сообщению от датчика движения,
// чтобы "подтолкнуть" автоматическую ветку.
return { payload: { motion: true } };
}
// Если движения нет, ничего не делаем и останавливаем поток
return null;
---
Итоги: Тестирование и обобщение паттерна
Мы создали многокомпонентный, но логичный и надежный поток для управления освещением, который учитывает как автоматические сценарии, так и потребность пользователя в ручном контроле.
Финальная ASCII-схема потока://----------- ВЕТКА АВТОМАТИЗАЦИИ -----------
[mqtt in]──[func: Сохр. сост.]──[switch: Проверка Override]─┬─[trigger: Таймер 5м]──[func: Формат]─┬──[mqtt out]
(Движение) (flow.lastMotionState) (flow.manualOverride) | | (Свет)
| |
└─────(Если движения нет)────────────────>┘
//----------- ВЕТКА РУЧНОГО УПРАВЛЕНИЯ -----------
[mqtt in]──────────[switch: Тип нажатия]──┬─(toggle)──[func: Уст. Override]─┬──(на 1-й вых.)─────>┘
(Выключатель) (msg.payload.action) | (2 вых.) |
| └─(на 2-й вых.)─[trigger: Таймер 2ч]─┬─[func: Сброс Override]─┬─(если есть движение)─> (в trigger: Таймер 5м)
└─(double_click)──────────────────────────────────────────┘ └─(если нет движения)─> (null)
План тестирования
Чтобы убедиться в корректной работе системы, проведите следующий набор тестов:
* Зайдите в комнату. Ожидаемый результат: Свет включается.
* Покиньте комнату. Ожидаемый результат: Свет выключается через 5 минут.
* Войдите в комнату, свет включился. Нажмите настенный выключатель. Ожидаемый результат: Свет погас.
* Продолжайте двигаться. Ожидаемый результат: Свет НЕ включается. Режим `override` активен.
* Активируйте ручной режим. Ничего не делайте. Ожидаемый результат: Через 2 часа (или тестовое время) система возвращается в автоматический режим. Если в комнате есть движение, свет включится.
* Активируйте ручной режим. Совершите действие ручного сброса (например, двойное нажатие). Ожидаемый результат: Система немедленно возвращается в автоматический режим. Светодиод на выключателе (если есть) меняет цвет.
Обобщение паттерна "Override"
Созданный нами механизм является универсальным. Его можно и нужно применять для других подсистем:
- Управление климатом: Автоматика поддерживает +22°C по расписанию. Пользователь вручную выставляет на термостате +24°C. Это действие должно устанавливать флаг `climateOverride` и блокировать сценарий расписания на несколько часов.
- Управление шторами: Шторы закрываются автоматически на закате. Пользователь вручную открывает их с кнопки. Это действие устанавливает `curtainsOverride` и предотвращает их повторное закрытие до полуночи или до ручного сброса.
Этот урок стал практическим введением в один из важнейших принципов проектирования умного дома — иерархию приоритетов сценариев. Наш поток является первым практическим воплощением этого правила, устанавливая простой, но нерушимый принцип: Ручное управление > Автоматизация. В дальнейшем мы будем надстраивать эту иерархию, добавляя уровни для режимов, расписаний и сценариев безопасности.
Что дальше
В следующем уроке мы рассмотрим еще один важный аспект пользовательского опыта — создание сложных, но интуитивно понятных "макро-сценариев" или "сцен", таких как "Я ушел", "Кино" или "Уборка", которые управляют несколькими подсистемами одновременно.