ГлавнаяАкадемияСценарии умного дома: режимы, состояния, приоритеты → Практика: Реализация FLOW-AUTO-LIGHT-012

Практика: Реализация FLOW-AUTO-LIGHT-012

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

Базовые режимы: Home/Away и управление состоянием

> 🔗 Связанный материал: Данный урок является практической реализацией одной из ключевых концепций системы режимов: управление состоянием "Дома/Не дома" (Home/Away). Мы рассмотрим это на примере сценария SCN-LIGHT-012 "Я ушел", описанного в первом модуле, в уроке "Каталог сценариев: Свет и Комфорт".

Основная цель этого практического занятия — создать фундаментальный механизм управления состоянием "Дома/Не дома" (Home/Away) в Node-RED. Мы реализуем это на примере сценария SCN-LIGHT-012 "Я ушел", создав поток FLOW-AUTO-LIGHT-012. Управление глобальными состояниями, или режимами, — это ключевой навык инженера по автоматизации, позволяющий создавать по-настоящему интеллектуальные системы.

Давайте декомпозируем эту задачу на фундаментальные компоненты, которые станут основой для управления режимами:

  • Триггер (Trigger): Это событие, которое инициирует выполнение сценария. В нашем случае, основным триггером является получение системного сообщения о том, что в доме больше никого нет.
  • Реализация: Мы будем использовать протокол MQTT как основу для системных сообщений. Сценарий будет активироваться, когда в топик `hi/system/presence` приходит сообщение с `payload`, равным строке `"away"`. Такой подход обеспечивает слабую связанность (decoupling) — триггеру не важно, как* система определила отсутствие людей (постановка на охрану, геолокация, ручная кнопка). Он реагирует только на финальное событие.

  • Изменение состояния (State Change): Система должна не просто выполнить действие, но и запомнить, что она перешла в новое глобальное состояние. Это критически важно для логики других сценариев. Например, сценарий включения света по движению не должен срабатывать, если в доме никого нет (режим "away").
  • * Реализация: Мы введем глобальную переменную контекста `is_home`. При активации сценария "Я ушел" эта переменная будет установлена в `false`. Ключевой момент — мы будем использовать персистентный контекст, чтобы это состояние сохранялось даже после перезагрузки контроллера HI.

  • Генерация команд (Command Generation): После срабатывания триггера и изменения состояния, поток должен сформировать конкретные команды для выключения всех групп освещения. На реальных объектах освещение может управляться по разные протоколам (KNX, DALI, MQTT-реле), и наш поток должен уметь работать со всеми.
  • * Реализация: Мы создадим центральный узел `function`, который будет действовать как агрегатор команд. Он сформирует массив стандартизированных JavaScript-объектов, где каждый объект представляет одну команду для выключения определенной группы света, независимо от протокола.

  • Отправка в шины (Dispatch): Сформированные команды необходимо направить в соответствующие физические или логические шины.
  • * Реализация: С помощью узла `switch` мы будем маршрутизировать каждое командное сообщение на основе его структуры. Сообщения для KNX пойдут на узел `knx-out`, для MQTT — на `mqtt out` и так далее.

  • Обратная связь (Feedback): Хотя в данном сценарии активная обратная связь пользователю не требуется (он уже ушел), система должна логировать свои действия для возможности последующего анализа и отладки.
  • * Реализация: Поток будет настроен на использование узлов `Status` для визуальной диагностики и, в случае ошибок, будет перенаправлять их в централизованный обработчик, как было рассмотрено в паттернах Node-RED.

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

    ---

    Триггеры и управление состоянием 'Присутствие'

    Первым шагом в реализации нашего потока является создание механизма, который будет слушать системные события и управлять глобальным состоянием присутствия. Состояние "дома" / "не дома" является одним из самых фундаментальных в любой системе автоматизации.

    > 💡 Подсказка: Для обеспечения максимальной отказоустойчивости необходимо настроить персистентный контекст. В файле `settings.js` вашего экземпляра Node-RED на контроллере HI найдите секцию `contextStorage` и убедитесь, что у вас есть хранилище типа `file`. Это гарантирует, что система будет "помнить" состояние `is_home` даже после сбоя питания или плановой перезагрузки.

    Начнем с построения потока.

  • Создание MQTT-триггера:
  • * Перетащите на холст узел `mqtt in`.

    * Сконфигурируйте его:

    * Server: Выберите ваш MQTT-брокер (обычно он запущен на самом контроллере HI по адресу `localhost:1883`).

    * Topic: Укажите системный топик `hi/system/presence`.

    * QoS: Установите `2` (Exactly once) для максимальной надежности доставки этого важного сообщения.

    * Name: Дайте узлу осмысленное имя, например, `[MQTT] System Presence`.

  • Обработка состояния и логика:
  • * Соедините выход узла `mqtt in` с входом узла `function`. Этот узел будет сердцем нашей логики управления состоянием.

    * Name: Назовите узел `Set is_home Context`.

    * Вставьте в него следующий JavaScript-код:

        // Получаем значение из payload. Приводим к строке и нижнему регистру для надежности.

    const presenceState = String(msg.payload).toLowerCase();

    // Получаем текущее значение из глобального контекста.

    // 'file' указывает на использование персистентного хранилища.

    const currentIsHome = global.get('is_home', 'file') || false;

    let newIsHome;

    // Основная логика для определения нового состояния

    if (presenceState === 'away') {

    newIsHome = false;

    node.status({ fill: "yellow", shape: "ring", text: "Presence: Away" });

    } else if (presenceState === 'home') {

    newIsHome = true;

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

    } else {

    // Если пришло некорректное сообщение, логируем ошибку и останавливаем поток

    node.warn(`Invalid payload received in hi/system/presence: ${msg.payload}`);

    node.status({ fill: "red", shape: "dot", text: "Error: Invalid payload" });

    return null; // Прекращаем выполнение

    }

    // Устанавливаем новое значение в глобальный персистентный контекст

    global.set('is_home', newIsHome, 'file');

    // Проверяем, изменилось ли состояние и является ли оно "away"

    // Сценарий "Я ушел" запускается только в момент перехода в состояние 'away'

    if (newIsHome === false && currentIsHome === true) {

    // Состояние изменилось на "away". Пропускаем сообщение дальше для запуска сценария.

    // Мы можем добавить полезные данные для следующих узлов.

    msg.event = 'presence.changed.to.away';

    return msg;

    }

    // Во всех остальных случаях (переход в 'home', или состояние не изменилось)

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

    return null;

    Этот код не просто устанавливает переменную. Он реализует важную логику:

    Теперь у нас есть надежный централизованный механизм, который управляет глобальным состоянием 'Присутствие' и инициирует зависимые сценарии только в момент смены этого состояния.

    ---

    Агрегация и управление группами освещения

    После того как наш основной поток изменил состояние системы на 'Away', он передает управление дальше. Теперь наша задача — выполнить действия, связанные с этим режимом. В данном случае — сформировать полный список команд для выключения всего света. Подход "один поток для всех" здесь наиболее эффективен, поскольку вся логика управления сценарием "Я ушел" находится в одном месте, что упрощает поддержку и модификацию.

    Мы будем использовать один узел `function` для создания массива команд. Этот узел будет подключен к выходу нашего предыдущего узла `Set is_home Context`.

  • Создание узла-агрегатора:
  • * Добавьте на холст узел `function` и назовите его `Generate 'All Off' Commands`.

    * Вставьте в него следующий код. Этот код является примером для объекта, где есть свет, управляемый по KNX, MQTT и DALI (через Modbus TCP шлюз).

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

    let commands = [];

    // --- Команды для KNX ---

    // Мы используем широковещательный Групповой Адрес (ГА) для выключения всего света в зоне KNX.

    // Это гораздо эффективнее, чем отправлять десятки индивидуальных команд.

    commands.push({

    name: "KNX All Lights Off",

    knx: {

    dpt: "DPT1.001", // Datapoint Type для On/Off

    value: 0, // 0 = OFF

    gad: "1/1/255" // Групповой адрес "Выключить свет - Всё здание"

    }

    });

    // --- Команды для MQTT ---

    // Для устройств, управляемых по MQTT (например, Wi-Fi реле), мы отправляем команды в их /set топики.

    commands.push({

    name: "MQTT Living Room Main Light Off",

    topic: "hi/light/living_room/main/set",

    payload: { "state": "OFF" } // Используем JSON payload, как мы определили в контракте сообщений

    });

    commands.push({

    name: "MQTT Kitchen Spotlights Off",

    topic: "hi/light/kitchen/spots/set",

    payload: { "state": "OFF" }

    });

    // --- Команды для DALI (через Modbus TCP шлюз) ---

    // Предположим, наш шлюз DALI-Modbus TCP имеет Slave ID=5 и адрес регистра 100

    // отвечает за широковещательную команду DALI OFF (значение 0).

    commands.push({

    name: "DALI Broadcast Off",

    payload: {

    'value': 0, // Значение 0 (DALI OFF)

    'fc': 6, // Function Code 6: Write Single Register

    'unitid': 5, // Modbus Slave ID шлюза DALI

    'address': 100 // Адрес регистра для команды

    },

    protocol: "modbus" // Добавляем маркер для маршрутизации

    });

    // Заменяем исходный msg.payload на наш массив команд

    msg.payload = commands;

    node.status({ fill: "blue", shape: "dot", text: `Generated ${commands.length} commands` });

    return msg;

  • Разделение массива:
  • * Теперь у нас есть одно сообщение, `msg.payload` которого — это массив из нескольких команд. Чтобы обработать их по отдельности, нам нужен узел `split`.

    * Добавьте узел `split` после узла `Generate 'All Off' Commands`.

    * Этот узел автоматически возьмет массив из `msg.payload` и создаст последовательность сообщений, где `msg.payload` каждого нового сообщения будет равен одному элементу из исходного массива. Например, первое сообщение будет содержать команду для KNX, второе — для MQTT света в гостиной, и так далее.

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

    ---

    Маршрутизация команд в физические шины

    На данном этапе у нас есть последовательность сообщений, каждое из которых является командой для определенной системы. Задача — направить каждое сообщение в правильный "туннель": KNX, MQTT или Modbus. Для этого идеально подходит узел `switch`.

    > ⚠️ Внимание: Никогда не создавайте "шторм телеграмм" (telegram storm) в шине KNX, отправляя команды на выключение каждому отдельному светильнику. Это приводит к высокой загрузке шины и возможной потере пакетов. Всегда используйте заранее настроенные групповые адреса (Group Address, GA) для управления целыми зонами или всем светом сразу (например, `1/1/255` для "Все освещение этажа OFF").

  • Создание узла-маршрутизатора:
  • * Подключите выход узла `split` ко входу узла `switch`.

    * Name: Дайте ему имя `Route Command by Protocol`.

    * Настройте правила маршрутизации. Узел `switch` будет проверять каждое входящее сообщение и отправлять его на один из своих выходов в зависимости от выполнения условия.

    * Настройте следующие правила:

    | Выход | Свойство | Правило | Значение | Назначение |

    | :---- | :------------------- | :--------------------------- | :----------------------- | :---------------------------------------- |

    | 1 | `msg.payload.knx` | `существует (is not null)` | | Для команд, предназначенных для шины KNX. |

    | 2 | `msg.payload.topic` | `содержит (contains)` | `hi/light/` | Для команд, предназначенных для MQTT. |

    | 3 | `msg.payload.protocol` | `==` (строка) | `modbus` | Для команд, идущих через Modbus (DALI). |

  • Подключение узлов вывода:
  • * Теперь соедините выходы узла `switch` с соответствующими узлами вывода.

    * Выход 1 (`KNX`): Соедините с узлом `knx-out` (из палитры `node-red-contrib-knx-ultimate`). В настройках этого узла нужно будет только выбрать правильный KNX шлюз. Узел автоматически возьмет все параметры (`dpt`, `value`, `gad`) из объекта `msg.payload.knx`.

    * Выход 2 (`MQTT`): Соедините с узлом `mqtt out`. Этот узел отправит `msg.payload.payload` в топик, указанный в `msg.payload.topic`. Важно убедиться, что структура сообщений соответствует той, что мы генерировали на предыдущем шаге.

    * Выход 3 (`Modbus`): Соедините с узлом `modbus-write`. В его настройках выберите Modbus TCP клиент, который "смотрит" на ваш DALI-шлюз. Узел `modbus-write` возьмёт все параметры (`value`, `fc`, `unitid`, `address`) непосредственно из `msg.payload.payload`.

    Дополнительно: Чтобы избежать слишком быстрой отправки команд в шину, особенно в Modbus RTU, можно добавить узел `delay` после узла `split` со включенной опцией "Rate Limit" (например, 10 сообщений в секунду). Это создаст небольшую паузу между командами и снизит пиковую нагрузку на шины и устройства.

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

    ---

    Сборка, тестирование и финализация потока

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

    > 🔗 Связанный материал: Подробные техники отладки и мониторинга потоков в Node-RED, включая эффективное использование узла `debug`, рассмотрены в модуле `COURSE-03-M04`.

    Итоговая структура потока

    Визуально наш поток выглядит так:

    [MQTT In] --(hi/system/presence)--> [Function: Set is_home] --(if state changed to 'away')--> [Function: Generate Commands] --> [Split] --> [Switch: Route by protocol]
    

    |-- (KNX) ---> [knx-out]

    |-- (MQTT) --> [mqtt out]

    |-- (Modbus) -> [modbus-write]

    Методы отладки и тестирования

    Прежде чем разворачивать поток на боевом объекте, его необходимо тщательно протестировать.

  • Симуляция триггера: Вместо того чтобы реально ставить систему на охрану, используйте узел `inject`.
  • * Настройте `inject` на отправку `msg.payload` типа `string` со значением `away`.

    * Подключите его ко входу узла `Set is_home Context`. Нажатие на этот узел сымитирует срабатывание сценария.

  • Инспекция сообщений: Разместите узлы `debug` в ключевых точках потока, чтобы "видеть" данные на каждом этапе:
  • * После `Generate 'All Off' Commands`: Убедитесь, что `msg.payload` содержит корректный массив со всеми командами.

    * После `Split`: Проверьте, что на выход подаются отдельные сообщения, каждое со своей командой в `msg.payload`.

    * После `Switch`: Подключите `debug` к каждому выходу маршрутизатора, чтобы убедиться, что команды правильно распределяются по протоколам.

    Лучшие практики финализации

    Что дальше: возможности для расширения

    Созданный нами поток — это не просто сценарий "Я ушел". Это центральный обработчик режима "Away". Его можно легко расширить, добавив новую логику в узел `Generate 'All Off' Commands`, чтобы управлять не только светом:

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

    В дальнейшем, в уроке M02-L07 при изучении режима Night, мы рассмотрим реализацию сценария "Ночная подсветка" (SCN-LIGHT-023), который требует работы с диммированием и имеет свои нюансы в части пользовательского опыта.