ГлавнаяАкадемияОсновы умного дома → Практика: Анализ контракта события

Практика: Анализ контракта события

Урок 5 · Основы умного дома · 30 мин · theory

Введение: Зачем анализировать контракт события?

> 🔗 Связанный материал: Этот урок является практическим продолжением теоретического занятия «От сигнала к событию: контракт сообщения». Убедитесь, что вы усвоили основные концепции, прежде чем приступать к практике.

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

Представьте систему, где каждый датчик «говорит» на своем собственном, уникальном языке. Датчик движения сообщает о срабатывании словом `"true"`, датчик протечки — числом `1`, а кнопка выключателя — строкой `"ON"`. Без предварительного анализа и приведения к общему стандарту, построение единого сценария, который бы реагировал на все эти события, превращается в хаотичный и сложный процесс, полный условных проверок. Система становится хрупкой, трудноотлаживаемой и практически немасштабируемой.

Строгий и понятный контракт решает эту проблему. Когда все компоненты системы общаются с использованием единого, заранее определенного формата (например, JSON-объекта со стандартными полями `value`, `ts`, `source`), мы получаем следующие преимущества:

Цели этого урока: научиться «читать», интерпретировать и анализировать сообщения от реальных устройств в среде Node-RED. Мы разберем, как устроены MQTT-сообщения, как извлекать из них полезную информацию и как преобразовывать «сырые» события в надежную основу для ваших сценариев автоматизации.

---

Анатомия MQTT-сообщения: Топик и Payload

> 💡 Подсказка: Для анализа MQTT-трафика в реальном времени рекомендуется использовать специализированные утилиты, например, MQTT Explorer. Это настольное приложение позволяет в наглядной древовидной форме видеть все публикуемые топики и их содержимое, что значительно ускоряет процесс отладки и понимания структуры данных в вашей сети.

Любое сообщение в протоколе MQTT, который является стандартом для взаимодействия устройств на нашей платформе, состоит из двух ключевых частей: топика (Topic) и полезной нагрузки (Payload).

Структура Топика

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

Рассмотрим реальный пример топика от многофункционального датчика Wirenboard, интегрированного в нашу систему:

`/devices/wb-msw-v3_21/controls/Sound Level`

Давайте разберем его по частям:

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

Управляющие топики и топики состояния

Крайне важно различать два типа топиков:

  • Топик состояния (State Topic): Это топик, в который устройство публикует свои текущие данные. Например, `/devices/wb-msw-v3_21/controls/Sound Level`. Мы можем только читать из него.
  • Управляющий топик (Control Topic): Это топик, в который мы отправляем команды устройству. Обычно он имеет суффикс `/on`, `/set` или аналогичный. Например, чтобы включить реле, мы можем отправить значение `1` в топик `/devices/my-relay/controls/Relay 1/on`.
  • Структура Полезной Нагрузки (Payload)

    Полезная нагрузка (Payload) — это непосредственно сами данные, которые передаются по топику. В современных системах, и в нашей платформе в частности, для payload практически всегда используется формат JSON (JavaScript Object Notation).

    JSON представляет данные в виде пар «ключ-значение».

    Пример простого JSON-payload:

    {
    

    "value": 25.5,

    "meta": {

    "type": "temperature",

    "units": "degC"

    }

    }

    Здесь `value` и `meta` — это ключи верхнего уровня. Значение ключа `meta` само является объектом с ключами `type` и `units`.

    Инспекция сообщения в Node-RED

    Лучший способ изучить структуру сообщения — использовать узел Debug.

  • Добавьте на поле узел `mqtt in` и настройте его на подписку на интересующий вас топик (для отладки можно использовать wildcard `#`, чтобы видеть все сообщения).
  • Соедините его с узлом `debug`.
  • В настройках узла `debug` измените `Output` с `msg.payload` на `complete msg object`.
  • Теперь, когда придет MQTT-сообщение, в боковой панели отладки вы увидите не просто payload, а весь объект `msg`. Это даст вам доступ ко всем его свойствам: `msg.topic`, `msg.payload`, `msg.qos`, `msg.retain` и другим, что является бесценным инструментом для анализа.

    ---

    Пример №1: Дискретное событие от датчика движения

    Рассмотрим один из самых распространенных типов событий в системе автоматизации — срабатывание датчика движения. Это дискретное (бинарное) событие, так как у него может быть только два состояния: «движение есть» или «движения нет».

    Предположим, у нас есть датчик движения, который публикует свои состояния в MQTT-топик `hi/office/corridor_1/motion`.

    Когда датчик обнаруживает движение, он отправляет следующее сообщение:

    {
    

    "value": 1,

    "timestamp": 1678886400000,

    "meta": {

    "type": "binary",

    "readonly": true

    }

    }

    Когда движение в зоне его видимости прекращается (обычно с некоторой задержкой), он отправляет другое сообщение:

    {
    

    "value": 0,

    "timestamp": 1678886520000,

    "meta": {

    "type": "binary",

    "readonly": true

    }

    }

    Анализ контракта

    Давайте проанализируем этот контракт:

    Практика в Node-RED

    В потоке Node-RED, когда мы получаем такое сообщение в `msg.payload`, как нам проверить, было ли движение?

    Неправильный подход:

    Проверять весь объект `msg.payload` на точное соответствие.

    // В узле Function - ПЛОХОЙ ПРИМЕР
    

    if (msg.payload === {"value": 1, "timestamp": 1678886400000, "meta": {"type": "binary", "readonly": true}}) {

    // Эта проверка никогда не сработает!

    // 1. Сравнение объектов в JS не работает так.

    // 2. Timestamp всегда будет разным.

    }

    Правильный подход:

    Проверять только то поле, которое несет в себе необходимую нам для логики информацию. В данном случае — `msg.payload.value`.

    // В узле Function - ПРАВИЛЬНЫЙ ПРИМЕР
    

    if (msg.payload.value === 1) {

    node.status({fill:"green", shape:"dot", text:"Движение обнаружено"});

    return msg; // Передать сообщение дальше для включения света

    } else {

    node.status({fill:"grey", shape:"ring", text:"Движения нет"});

    return null; // Прервать поток, так как ничего делать не нужно

    }

    Почему это так важно?

    Представьте, что производитель датчика выпускает новую прошивку, в которой в объект `meta` добавляется новое поле, например, `"version": "1.1"`.

    Если ваша логика была завязана на точное сравнение всего объекта, она сломается. Если же вы проверяете только `msg.payload.value`, ваш сценарий продолжит работать без изменений. Это и есть принцип построения отказоустойчивых и поддерживаемых систем: ваша логика должна быть как можно менее зависимой от тех частей контракта, которые не относятся напрямую к принимаемому решению.

    ---

    Пример №2: Комплексное событие от датчика климата

    > ⚠️ Внимание: Всегда проверяйте тип данных в `payload`! Значение `25.5` (тип `number`) и `"25.5"` (тип `string`) — это не одно и то же. Неправильный тип данных — одна из самых частых причин ошибок в узлах `switch` и `function`, когда логические сравнения (например, "температура > 26") не работают так, как ожидается. Если вы получаете данные в виде строки JSON, используйте узел `JSON` в Node-RED для автоматического преобразования (парсинга) строки в объект с правильными типами данных.

    Теперь рассмотрим более сложный случай — комплексное событие от климатического датчика, который измеряет сразу несколько параметров: температуру, влажность и уровень CO2.

    Такой датчик может отправлять в топик `hi/living_room/climate` одно большое сообщение, содержащее все свои показания на текущий момент.

    Пример `msg.payload`:

    {
    

    "temperature": 25.5,

    "humidity": 45.2,

    "co2": 810,

    "units": {

    "temperature": "°C",

    "humidity": "%",

    "co2": "ppm"

    },

    "timestamp": 1678890000000

    }

    Анализ контракта

    Доступ к данным в Node-RED

    Чтобы получить доступ к конкретному значению внутри такого сложного объекта, используется "точечная нотация":

    Пример использования в узле `function` для проверки уровня CO2:

    // Вход: msg.payload содержит JSON-объект выше
    

    const co2_level = msg.payload.co2;

    if (co2_level > 1000) {

    msg.payload = "HIGH"; // Подготовим команду для вентиляции

    node.status({fill:"red", shape:"dot", text:"CO2: " + co2_level + " ppm (High)"});

    return msg;

    } else if (co2_level > 800) {

    node.status({fill:"yellow", shape:"dot", text:"CO2: " + co2_level + " ppm (Elevated)"});

    return null; // Уровень повышен, но действий не требует

    } else {

    node.status({fill:"green", shape:"dot", text:"CO2: " + co2_level + " ppm (OK)"});

    return null; // Все в норме

    }

    Один "большой" JSON против нескольких "маленьких"

    Существует альтернативный подход, когда устройство отправляет каждый параметр в свой собственный под-топик:

    Сравним оба подхода:

    | Критерий | Один большой JSON (атомарный) | Несколько маленьких сообщений |

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

    | Атомарность | Высокая. Все данные относятся к одному моменту времени. | Низкая. Сообщения могут прийти с небольшой задержкой друг относительно друга. |

    | Простота логики | Выше для сценариев, требующих несколько параметров (например,计算тепловой индекс по температуре и влажности). | Ниже. Требуется сохранять состояние каждого параметра отдельно. |

    | Гибкость | Ниже. Чтобы получить один параметр, вы должны получить все. | Выше. Можно подписаться только на нужный топик (например, только на температуру). |

    | Нагрузка на сеть | Потенциально выше, если вам нужен только один параметр из десяти. | Эффективнее, если нужны не все параметры. |

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

    ---

    Переход от События к Состоянию

    > 🔗 Связанный материал: Мы подробно рассматривали механику работы с переменными контекста (context) и понятием состояния в уроке «Понятие состояния (State)».

    Анализ контракта события — это лишь первый шаг. Мы научились «читать» и извлекать данные. Но чтобы построить по-настоящему умную логику, одних событий недостаточно. Нам нужно состояние — память системы о текущих значениях параметров.

    Событие — это то, что произошло сейчас (например, пришло новое значение температуры `26.1`). Состояние — это то, что мы знаем о системе в любой момент времени (например, мы знаем, что `current_temperature` равно `26.1`, даже когда никаких событий не происходит).

    Давайте на практике превратим событие в состояние.

    Задача:

    Получить комплексное событие от климатического датчика и сохранить значение температуры в переменную контекста потока (flow context), чтобы другие узлы могли его использовать.

    Реализация в Node-RED

  • Получаем событие: Узел `mqtt in` подписан на топик `hi/living_room/climate` и получает JSON-объект, как в примере №2.
  • Извлекаем данные: Мы можем использовать узел `change` для этой задачи, это не требует написания кода.
  • * Добавьте узел `change`.

    * Создайте правило: Set `msg.payload` to `msg.payload.temperature`.

    * Это правило возьмет значение из `msg.payload.temperature` и перезапишет им весь `msg.payload`. На выходе из этого узла `msg.payload` будет уже не объектом, а простым числом, например, `25.5`.

  • Сохраняем состояние: Теперь нам нужно сохранить это значение. Используем еще один узел `change`.
  • * Добавьте второй узел `change`.

    * Создайте правило: Set `flow.current_temp` to `msg.payload`.

    * Это правило возьмет текущее значение из `msg.payload` (которое теперь равно `25.5`) и сохранит его в переменную `current_temp` в контексте потока.

    Весь поток будет выглядеть так:

    `[MQTT In]` -> `[Change: Extract Temp]` -> `[Change: Save to Flow Context]`

    Теперь, в любом другом узле `function` или `switch` на этом же потоке, вы можете получить доступ к сохраненному состоянию:

    // В другом узле, который, например, срабатывает по таймеру
    

    const saved_temp = flow.get("current_temp") || 0; // Получаем сохраненное значение

    if (saved_temp > 26) {

    // Логика включения кондиционера

    }

    Зачем это нужно?

    Рассмотрим сценарий: "включить кондиционер, если температура держится выше 26 градусов в течение 5 минут".

    Для его реализации нам нужно:

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

    ---

    Итоги и ключевые выводы

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

    Давайте закрепим ключевые моменты:

    Топик и Payload несут разную информацию. Топик говорит откуда пришли данные (контекст устройства), а payload — что* именно произошло (значения параметров).

    Что дальше?

    Теперь, когда вы умеете «читать» и понимать язык устройств, мы готовы перейти к следующему логическому шагу: созданию нашего первого полноценного сценария. В следующем уроке мы применим полученные знания для построения простого, но полезного flow: «Автоматическое включение света при обнаружении движения».