ГлавнаяАкадемияСценарии умного дома: режимы, состояния, приоритеты → Контракты сообщений для управления режимами

Контракты сообщений для управления режимами

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

Введение в контракты сообщений

> 💡 Подсказка: Мышление в терминах контрактов, а не просто потоков данных, — ключевой шаг от любительской автоматизации к созданию профессиональных, поддерживаемых систем на контроллерах HI.

В контексте Node-RED и сложных систем автоматизации, контракт сообщения — это формализованное, документированное и стабильное описание структуры объекта `msg`, передаваемого между узлами, потоками (flows) и даже различными системами (например, через MQTT). По своей сути, это соглашение о том, какие данные, в каком формате и под какими именами полей будут передаваться для выполнения 특정ных операций.

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

Ключевые преимущества использования контрактов:

Можно провести прямую аналогию с API (Application Programming Interface) в мире веб-разработки. Контракты сообщений — это внутренний API вашей системы автоматизации. Они определяют правила взаимодействия между ее независимыми частями.

Рассмотрим типичную проблему, которую решают контракты, — "магические строки" и неструктурированные данные. На начальном этапе очень велик соблазн управлять режимами, передавая в `msg.payload` простые строки:

`'home'`, `'away'`, `'night'`

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

Без контракта пришлось бы создавать сложную, запутанную логику, пытаясь "угадать" контекст по косвенным признакам или передавать данные в других свойствах объекта `msg`, таких как `msg.topic`. Это хрупкий и плохо поддерживаемый подход. Контракт сообщения решает эту проблему элегантно, инкапсулируя все необходимые данные в единый, структурированный и предсказуемый объект.

---

Структура контракта для запроса смены режима

Основная идея контракта — упаковать все данные, относящиеся к одной операции, в единый вложенный объект внутри `msg.payload`. Это позволяет избежать конфликтов с другими полями, которые могут уже присутствовать в сообщении, например, с данными от исходного сенсора. Для управления режимами мы будем использовать объект `msg.payload.mode_request`.

Этот объект-команда должен содержать всю информацию, необходимую центральному обработчику режимов для принятия решения.

Обязательные и опциональные поля

Структура контракта `mode_request` делится на обязательные (должны присутствовать всегда) и опциональные (используются для расширенной логики) поля.

| Поле | Тип | Обязательное? | Описание и примеры |

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

| `target` | `String` | Да | Целевой режим. Возможные значения строго определены: `'home'`, `'away'`, `'night'`. |

| `source` | `String` | Да | Уникальный идентификатор источника команды. Позволяет понять, что именно вызвало смену режима. Формат: `<тип>.<расположение>.<имя>`. Примеры: `'geo.user_a'`, `'switch.living_room.main'`, `'timer.sunset'`, `'security.alarm_panel'`. |

| `priority` | `Number` | Да | Приоритет команды (целое число от 1 до 100). Используется для разрешения конфликтов, когда две команды приходят одновременно. Чем выше число, тем выше приоритет. |

| `user` | `String` | Нет | Идентификатор пользователя, инициировавшего действие. Важно для аудита и персонализации. Пример: `'alice'`, `'bob'`. |

| `force` | `Boolean` | Нет | Флаг принудительной смены режима. Если `true`, обработчик может проигнорировать некоторые блокирующие условия (например, если режим "Ночь" запрещено включать днем). Используется для ручного управления. По умолчанию `false`. |

| `delay_ms` | `Number` | Нет | Задержка в миллисекундах перед фактической активацией режима. Полезно для сценария "Я ухожу": команда отправляется при закрытии двери, но режим `away` активируется через 60000 мс (1 минуту), давая время покинуть дом. |

Пример полного объекта `msg.payload`

Вот как будет выглядеть полное сообщение, отправленное от настенного выключателя в спальне для активации режима "Ночь" с высоким приоритетом.

{

"topic": "system/mode/request",

"payload": {

"mode_request": {

"target": "night",

"source": "switch.bedroom.master_sleep",

"priority": 80,

"user": "alice",

"force": false

}

},

"_msgid": "a1b2c3d4.e5f6g7"

}

А вот пример запроса на переход в режим "Нет дома", инициированный геолокационным триггером последнего ушедшего пользователя, с задержкой в 2 минуты для предотвращения ложных срабатываний.

{

"topic": "system/mode/request",

"payload": {

"mode_request": {

"target": "away",

"source": "geo.last_user_left",

"priority": 50,

"delay_ms": 120000

}

},

"_msgid": "b2c3d4e5.f6g7h8"

}

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

---

Структура контракта для оповещения о смене режима

> 🔗 Связанный материал: Подробно архитектура широковещательных оповещений через MQTT на контроллерах HI рассматривается в курсе по межсервисному взаимодействию: `COURSE-09`, `MODULE-01`.

Если `mode_request` — это сообщение-команда (запрос на действие), то после успешной обработки этой команды система должна сгенерировать сообщение-событие (констатация факта). Это событие оповещает все заинтересованные подсистемы о том, что глобальный режим изменился.

Это разделение критически важно: десятки устройств могут запрашивать смену режима, но только один центральный узел (наш subflow) имеет право ее совершить и объявить о результате. Это создает единый и надежный источник правды (Source of Truth) о состоянии системы.

Контракт для такого оповещения мы назовем `mode_state`. Это сообщение будет рассылаться широковещательно, как правило, через MQTT по определенному топику, например `system/mode/state`.

Ключевые поля контракта `mode_state`

Этот контракт проще, чем `mode_request`, и носит чисто информационный характер.

| Поле | Тип | Описание и примеры |

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

| `current` | `String` | Новый, только что установленный режим. Пример: `'night'`. |

| `previous` | `String` | Режим, который был до изменения. Полезно для логики, которая должна срабатывать только при определенных переходах (например, с `home` на `away`, но не с `night` на `away`). |

| `timestamp` | `Number` | Временная метка (Unix timestamp в миллисекундах), когда произошла смена режима. |

| `source` | `String` | Причина смены режима. Это поле копируется из поля `source` исходного сообщения `mode_request`, которое привело к смене. Это обеспечивает полную отслеживаемость (traceability) событий. |

Жизненный цикл сообщения и пример

  • Запрос: Узел геолокации формирует `msg` с `payload.mode_request` и отправляет его в subflow управления режимами.
  • Обработка: Subflow валидирует запрос, проверяет условия (например, приоритет), меняет значение в глобальном контексте (`flow.system_mode`).
  • Оповещение: После успешного изменения subflow формирует новое сообщение с `payload.mode_state` и отправляет его на свой выход, который подключен к узлу `mqtt out`.
  • Реакция: Другие потоки (управление светом, климатом, охраной), подписанные на `system/mode/state` через `mqtt in`, получают это сообщение и реагируют соответствующим образом.
  • Пример сообщения-оповещения, которое получит вся система после обработки запроса из предыдущего раздела:

    {
    

    "topic": "system/mode/state",

    "payload": {

    "mode_state": {

    "current": "night",

    "previous": "home",

    "timestamp": 1678886400000,

    "source": "switch.bedroom.master_sleep"

    }

    },

    "retain": true,

    "_msgid": "c3d4e5f6.g7h8i9"

    }

    > ℹ️ Информация: Обратите внимание на флаг `retain: true`. Для сообщений о состоянии это является хорошей практикой. Любой новый или перезагруженный компонент системы, подписавшись на этот MQTT-топик, немедленно получит последнее актуальное состояние, даже если оно не менялось уже несколько часов.

    ---

    Применение контрактов в Node-RED: Валидация и обработка

    > ⚠️ Внимание: Сообщение, не соответствующее контракту, должно быть немедленно отброшено и залогировано. Попытка "додумать" или исправить неполные данные на лету приведет к непредсказуемому поведению системы и усложнит отладку.

    Теоретическое проектирование контрактов бесполезно без их практического применения. Рассмотрим, как в Node-RED обеспечить соблюдение этих правил.

    Формирование контракта из простых сообщений

    Не все устройства могут сразу выдать сложный JSON. Например, кнопка Zigbee или простой MQTT-клиент может отправлять только строку `home`. В этом случае нам нужен узел-"адаптер", который превратит простое сообщение в полноценный `mode_request`. Обычно для этого используется узел `Function`.

    Пример: У нас есть MQTT-топик `system/mode/simple_set`, куда можно отправить простую строку.
    // Код для узла Function, подключенного после 'mqtt in'
    
    

    const targetMode = msg.payload;

    const validModes = ['home', 'away', 'night'];

    // 1. Простая валидация

    if (typeof targetMode !== 'string' || !validModes.includes(targetMode)) {

    node.warn(`Получена некорректная команда в simple_set: ${targetMode}`);

    return null; // Отбрасываем сообщение

    }

    // 2. Формирование полного контракта

    msg.payload = {

    mode_request: {

    target: targetMode,

    source: "mqtt.simple_set", // Указываем источник

    priority: 70, // Средний приоритет для ручных команд

    force: true // Ручные команды обычно принудительные

    }

    };

    return msg;

    Теперь этот узел можно подключить ко входу нашего центрального subflow.

    Валидация входящих сообщений

    Внутри subflow управления режимами первый шаг — это всегда валидация. Лучший инструмент для этого — узел `Switch`. Он должен работать как охранник, который пропускает дальше только сообщения, соответствующие контракту.

    Настройте узел `Switch` в начале subflow следующим образом:

  • Правило 1 (Основной путь):
  • * `Property`: `msg.payload.mode_request.target`

    * `Условие`: `is not null` (или `is of type string`)

    * Этот выход ведет к основной логике обработки.

  • Правило 2 (`otherwise`):
  • * Этот выход будет ловить все сообщения, которые не прошли проверку.

    * Подключите этот выход к последовательности узлов для логирования ошибок. Например: `Function` (формирует текст ошибки) -> `Debug` (вывод в консоль) -> Узел для записи в `audit_log` в MySQL.

    Таким образом, если кто-то отправит в subflow сообщение вида `msg.payload = "home"`, оно будет поймано и отброшено, не вызывая сбоев в основной логике.

    Безопасное извлечение значений (JSONata)

    Когда мы работаем с опциональными полями, такими как `delay_ms`, прямой доступ `msg.payload.mode_request.delay_ms` может вызвать ошибку, если поле отсутствует. Чтобы избежать этого, можно использовать проверки в узле `Function` или, что более элегантно, JSONata в узлах `Change` или `Switch`.

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

    Более лаконичный вариант с помощью оператора `$lookup`:

    `$lookup(payload.mode_request, "delay_ms")` вернет `undefined` без ошибки, если поля нет.

    Полная, безопасная конструкция для извлечения с значением по умолчанию:

    `$lookup(payload.mode_request, "delay_ms") != null ? payload.mode_request.delay_ms : 0`

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

    ---

    Резюме и лучшие практики документирования

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

    Мы рассмотрели два ключевых контракта:

  • `mode_request` (Команда): Структурированный запрос на смену режима, содержащий всю необходимую для принятия решения информацию: цель, источник, приоритет и опциональные параметры.
  • `mode_state` (Событие/Состояние): Широковещательное оповещение о свершившемся факте смены режима, которое служит единым источником правды для всей системы.
  • Лучшие практики

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

    Создайте единое место, где будут описаны все контракты сообщений, используемые в вашем проекте. Это может быть:

    Документация по каждому контракту должна включать:

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

    Что дальше

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