ГлавнаяАкадемияNode-RED: установка, flows, msg/JSON, отладка → Практика: Комбинация паттернов для управления освещением

Практика: Комбинация паттернов для управления освещением

Урок 6 · Node-RED: установка, flows, msg/JSON, отладка · 30 мин · theory

Введение: От теории к комплексному решению

ведение: От теории к комплексному решению

На предыдущих уроках мы последовательно изучили ряд фундаментальных паттернов проектирования в Node-RED. Каждый из них решает свою, четко очерченную задачу: управление состоянием, фильтрация сообщений, запуск по расписанию и другие. Однако истинная мощь и гибкость платформы HI раскрывается не при использовании этих паттернов по отдельности, а при их грамотной комбинации для решения комплексных, реальных задач автоматизации. Теория важна, но без практики она остается лишь набором абстрактных концепций.

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

> ℹ️ Информация: Этот урок предполагает, что вы уже знакомы с базовыми паттернами, рассмотренными в предыдущих уроках этого модуля. Мы не будем повторять их основы, а сфокусируемся на их совместном применении. Если такие понятия, как конечный автомат или защита от дребезга, вызывают у вас вопросы, рекомендуем вернуться к соответствующим урокам.

В рамках этого практического занятия мы будем использовать и комбинировать следующие ключевые паттерны:

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

Сценарий и начальная настройка: Умное освещение в гостиной

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

Требования к системе

Наша задача — автоматизировать основное освещение в гостиной. Система должна реагировать на следующие события и удовлетворять следующим условиям:

  • Ручное управление: Пользователь должен иметь возможность в любой момент включить или выключить свет с помощью настенного выключателя стандарта KNX.
  • Автоматическое управление по движению: Свет должен автоматически включаться при обнаружении движения, но только в темное время суток.
  • Автоматическое выключение: Если свет был включен по движению, он должен автоматически выключаться через 5 минут после прекращения движения.
  • Приоритет ручного управления: Если свет был включен вручную (с выключателя или из приложения), он НЕ должен автоматически выключаться по таймеру отсутствия движения. Пользователь явно изъявил свою волю, и автоматика не должна ей мешать. Выключить свет в этом режиме можно только вручную.
  • Удаленное управление: Пользователь должен иметь возможность управлять светом через мобильное приложение, отправляющее команды по протоколу MQTT.
  • Проектирование и настройка входов

    Для реализации этого сценария нам понадобятся три источника данных (входных узла):

    Давайте создадим основу нашего потока, добавив и настроив эти три входных узла.

    1. Узел для KNX-выключателя:

    - Добавьте узел `knx-in device`.

    - Настройте его на соответствующий групповой адрес вашего выключателя.

    - Задайте имя узла, например, "Выключатель в гостиной".

    - Важно: этот узел будет генерировать сообщения с `msg.payload`, содержащим `1` или `0`.

    2. Узел для датчика движения (MQTT):

    - Добавьте узел `mqtt in`.

    - `Topic`: `hi/living_room/motion`

    - `Output`: `a parsed JSON object` (очень важно для автоматического преобразования строки в JSON).

    - `Name`: "Датчик движения"

    - Контракт сообщения от датчика:

         {

    "motion": true,

    "ts": 1678886400000

    }

    или

         {

    "motion": false,

    "ts": 1678886490000

    }

    3. Узел для команд из приложения (MQTT):

    - Добавьте еще один узел `mqtt in`.

    - `Topic`: `hi/living_room/light/set`

    - `Output`: `a parsed JSON object`

    - `Name`: "Команды из приложения"

    - Контракт входящей команды:

         {

    "command": "ON",

    "source": "app-ios-user123"

    }

    Чтобы различать источники сообщений в нашей основной логике, мы добавим узлы `change` после каждого входа, которые будут устанавливать свойство `msg.source`. Это стандартная практика для упрощения маршрутизации.

    ASCII-схема начальной настройки:
    (KNX Switch)     [knx-in device] --> [change: set msg.source to 'knx'] --+
    

    |

    (Motion Sensor) [mqtt in] --------> [change: set msg.source to 'motion']--+--> [Главная логика]

    |

    (Mobile App) [mqtt in] --------> [change: set msg.source to 'app'] ---+

    После этой настройки все сообщения, поступающие в узел "Главная логика", будут иметь стандартизированное свойство `msg.source`, что позволит нам легко определить, какое событие произошло.

    ---

    Ядро системы: Комбинация 'State Machine', 'Rules Engine' и 'Gate'

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

    > 💡 Подсказка: Храните состояние системы (state) в переменной контекста потока (flow context). Это делает логику наглядной и упрощает отладку, так как вы всегда можете проверить текущее состояние через вкладку 'Context Data' в боковой панели Node-RED.

    Реализация конечного автомата (State Machine)

    Наша система будет иметь три четко определенных состояния:

  • `off`: Свет выключен. Начальное состояние.
  • `manual_on`: Свет был включен вручную (с выключателя или из приложения). В этом состоянии автоматическое выключение заблокировано.
  • `auto_on`: Свет был включен автоматически по датчику движения. В этом состоянии автоматическое выключение разрешено.
  • Мы будем хранить текущее состояние в переменной контекста `flow.light_state`.

    Движок правил и шлюз (Rules Engine & Gate)

    Внутри `function` ноды мы пропишем правила перехода между состояниями и условия выполнения команд. Эти правила и будут нашим движком. Паттерн Gate будет реализован не как отдельный узел, а как условие внутри нашей логики: "если состояние `manual_on`, то игнорировать (блокировать) событие автоматического выключения".

    Создайте узел `function` и назовите его "Логика управления освещением". Вставьте в него следующий код:

    // --- 1. Получение текущего состояния и входных данных ---
    

    // Получаем текущее состояние из контекста потока. Если его нет, по умолчанию 'off'.

    let state = flow.get('light_state') || 'off';

    // Для отладки: выводим текущее состояние в статус узла

    node.status({ fill: "grey", shape: "ring", text: "State: " + state });

    // --- 2. Определяем вспомогательные функции ---

    // Функция для проверки, является ли текущее время ночным (упрощенный вариант)

    function isNight() {

    const hour = new Date().getHours();

    // Считаем ночью время с 20:00 до 7:00

    return (hour >= 20 || hour < 7);

    }

    // --- 3. Основной движок правил (Rules Engine) на основе источника сообщения ---

    switch (msg.source) {

    // --- Обработка ручных команд ---

    case 'knx':

    case 'app':

    let cmd = msg.payload.command || (msg.payload === 1 ? 'ON' : 'OFF');

    if (cmd === 'ON') {

    // Переход в состояние 'manual_on'

    flow.set('light_state', 'manual_on');

    node.status({ fill: "blue", shape: "dot", text: "Manual ON" });

    return { payload: 1 }; // Команда на включение света

    } else if (cmd === 'OFF') {

    // Переход в состояние 'off'

    flow.set('light_state', 'off');

    node.status({ fill: "black", shape: "dot", text: "OFF" });

    return { payload: 0 }; // Команда на выключение света

    }

    break;

    // --- Обработка событий от датчика движения ---

    case 'motion':

    // Срабатываем только если есть движение, система выключена и сейчас ночь

    if (msg.payload.motion === true && state === 'off' && isNight()) {

    // Переход в 'auto_on'

    flow.set('light_state', 'auto_on');

    node.status({ fill: "green", shape: "dot", text: "Auto ON" });

    return { payload: 1 }; // Команда на включение света

    }

    // Если движение обнаружено в состоянии 'auto_on', это сообщение просто пройдет дальше

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

    break;

    // --- Обработка команды от таймера автовыключения ---

    case 'timer_off':

    // Паттерн "Gate": проверяем, что система в состоянии 'auto_on'

    // Если свет был включен вручную (state === 'manual_on'), это условие не выполнится,

    // и команда автовыключения будет проигнорирована (заблокирована).

    if (state === 'auto_on') {

    flow.set('light_state', 'off');

    node.status({ fill: "black", shape: "dot", text: "OFF by timer" });

    return { payload: 0 }; // Команда на выключение света

    }

    break;

    }

    // Если ни одно из правил не сработало, не отправляем сообщение дальше.

    return null;

    Этот код элегантно реализует три паттерна одновременно. `switch (msg.source)` — это наш движок правил. `flow.get/set` — реализация конечного автомата. А условие `if (state === 'auto_on')` в секции `case 'timer_off'` — это и есть наш Gate.

    Подключите выходы всех узлов `change` (от `knx`, `mqtt`) ко входу этой `function` ноды. А выход `function` ноды подключите к узлу, который непосредственно управляет реле света (например, `modbus-write` или `rpi gpio out`).

    ---

    Стабилизация входов: Паттерн 'Debounce/Rate Limit'

    Физические устройства реального мира неидеальны. Контакты механических выключателей при замыкании могут несколько раз кратковременно замкнуться и разомкнуться, прежде чем установят стабильный контакт. Этот эффект называется дребезгом контактов. Если не отфильтровать эти ложные сигналы, наша логика может получить несколько команд `ON`/`OFF` за миллисекунды и повести себя непредсказуемо. Аналогично, некоторые датчики движения могут отправлять сообщения слишком часто, создавая избыточную нагрузку.

    Для решения этих проблем мы применим паттерн Debounce, используя стандартный узел `delay`.

    > ⚠️ Внимание: Слишком большое время debounce для выключателя (>250ms) может восприниматься пользователем как задержка или 'лаг' системы. Начинайте с малых значений (50-100ms) и увеличивайте только при необходимости.

    Защита от дребезга KNX выключателя

  • Найдите в вашем потоке узел `knx-in device`.
  • Между ним и узлом `change`, который устанавливает `msg.source`, вставьте узел `delay`.
  • Откройте настройки узла `delay` и сконфигурируйте его следующим образом:
  • * Action: `Rate Limit`

    * Rate: `1` message(s) per `100` milliseconds (это хорошее стартовое значение)

    * Drop intermediate messages: Установите эту галочку. Это режим debounce. Он пропустит только первое сообщение, а все последующие в течение 100 мс будут проигнорированы.

    * `Name`: "Debounce 100ms"

    ASCII-схема этого участка:
    [knx-in device] --> [delay: debounce 100ms] --> [change: set msg.source] --> [Главная логика]
    

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

    Ограничение потока от датчика движения

    Если ваш датчик движения слишком "разговорчив" (например, шлет `{"motion": true}` каждые 100 мс), это может создавать ненужную нагрузку. Мы можем ограничить частоту этих сообщений, чтобы обрабатывать их, например, не чаще одного раза в секунду.

  • Найдите узел `mqtt in` для датчика движения.
  • Между ним и узлом `change` вставьте узел `delay`.
  • Настройте его:
  • * Action: `Rate Limit`

    * Rate: `1` message(s) per `1` second.

    * Drop intermediate messages: Снимите эту галочку. Мы хотим получать последнее состояние, а не первое. Это режим throttle (регулирование).

    * `Name`: "Throttle 1s"

    Теперь, даже если датчик шлет 10 сообщений в секунду, наша логика будет получать только последнее из них раз в секунду. Этого более чем достаточно для своевременной реакции и экономии ресурсов контроллера.

    ---

    Автоматизация по времени: Паттерн 'Scheduler'

    Согласно нашим требованиям, свет, включенный автоматически, должен выключаться через 5 минут после последнего обнаруженного движения. Это классическая задача для паттерна Scheduler, который идеально реализуется с помощью узла `trigger`.

    Узел `trigger` работает по простому принципу:

  • Получив сообщение, он может отправить одно сообщение немедленно.
  • Затем он ждет заданный период времени.
  • Если за это время не пришло новых сообщений, он отправляет второе сообщение.
  • Если новое сообщение приходит, таймер сбрасывается и отсчет начинается заново.
  • Это в точности то, что нам нужно: каждое сообщение о наличии движения должно сбрасывать таймер выключения.

    Реализация таймера автовыключения

  • Найдите выход узла `change`, который идет от датчика движения (`msg.source = 'motion'`).
  • От этого узла сделайте два соединения: одно, как и раньше, к главному `function` узлу, а второе — к новому узлу `trigger`.
  • Настройте узел `trigger`:
  • * Send: `nothing` (первоначальное сообщение нам не нужно, оно уже ушло в основную логику)

    * then wait for: `5` minutes

    * then send: `a different message`

    * Payload: `{ "command": "AUTO_OFF" }` (JSON)

    * Handle resetting the timer if `msg.payload` contains: `{"motion": true}`. Это важное условие, которое гарантирует, что таймер будет сбрасываться только при обнаружении движения, а не при сообщении об его отсутствии.

    * `Name`: "Таймер выключения 5 мин"

  • Теперь выход из узла `trigger` нужно направить обратно в нашу систему. Но куда? Напрямую в главный `function` узел? Нет, это плохая практика. Вместо этого мы создадим еще один узел `change`
  • * `Set`: `msg.source`

    * `to`: `timer_off` (string)

    * `Name`: "Set source: timer_off"

  • И уже выход этого узла `change` мы подключаем ко входу нашего главного `function` узла "Логика управления освещением".
  • ASCII-схема потока для датчика движения:
                      +-----------------------------------------------> [Главная логика]
    

    |

    [mqtt in: motion] -> [change: set msg.source to 'motion'] -> [delay: throttle 1s]

    |

    +-> [trigger: 5 min] -> [change: set msg.source to 'timer_off'] ->+

    |

    [Главная логика] <--------------------------+

    Теперь наш `case 'timer_off'` в главном `function` узле будет получать сообщение `{ "command": "AUTO_OFF", "source": "timer_off" }` ровно через 5 минут после последнего сообщения от датчика движения с `{"motion": true}`. А так как мы реализовали паттерн `Gate`, эта команда сработает (выключит свет) только если система находится в состоянии `auto_on`. Задача решена элегантно и надежно.

    ---

    Итоги: Декомпозиция и лучшие практики

    тоги: Декомпозиция и лучшие практики

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

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

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

    Паттерн «Error Handler»: Отказоустойчивость системы

    Комплексное решение не будет полным без обработки исключительных ситуаций. При работе с реальным оборудованием (MQTT брокеры, шлюзы KNX/Modbus) связь может прерваться.

    > 💡 Совет: Состояние системы при ошибке связи должно быть предсказуемым. Если команда на выключение не прошла, `catch` может запустить повторную попытку через 5 секунд или отправить уведомление администратору.

    Рекомендации по отладке и документированию

    Используйте узлы `comment`: Размещайте комментарии рядом с ключевыми блоками. Объясните, почему* выбрано решение. Например: "Таймер авто-выключения. Сбрасывается при движении".

    Что дальше?

    Созданный сценарий можно развивать и дальше. Попробуйте самостоятельно:

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