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

От сигнала к событию: контракт сообщения

Урок 1 · Основы умного дома · 30 мин · theory
id: COURSE-01-M02-L02

title: "От сигнала к событию: контракт сообщения"

level: foundation

course: COURSE-01

module: COURSE-01-M02

tags: [mqtt, node-red, json, data-model, events]

prerequisites:

- COURSE-01-M02-L01

version: 1.0.0

status: published

learning_outcomes:

- "Объяснять разницу между 'сигналом' и 'событием' в контексте умного дома."

- "Проектировать базовый 'контракт сообщения' для передачи данных с устройств."

- "Использовать ноду 'function' в Node-RED для преобразования сырых данных в структурированный объект события."

- "Понимать важность стандартизации формата сообщений для надежности системы."

---

# Урок COURSE-01-M02-L02: От сигнала к событию: контракт сообщения

Введение: от сигнала к событию

> 🔗 Связанный материал: Для полного понимания этой секции необходимо изучить материал урока COURSE-01-M02-L01 "Что такое сигнал: аналоговый vs дискретный".

В предыдущем уроке мы установили, что сигнал — это сырое, необработанное значение, поступающее от аппаратного входа контроллера. Например, значение `1` от датчика движения (геркона), `0` от датчика протечки, или `23.5` от датчика температуры.

Сигналы являются фундаментом автоматизации, но сами по себе они несут ограниченную информацию. Рассмотрим простой сигнал `1` от дискретного входа. Что он означает?

  • Дверь открылась?
  • Зафиксировано движение?
  • Нажата кнопка вызова?

Без дополнительной информации (контекста) система не может принять верное решение. Сигнал отвечает на вопрос «Что?», но оставляет без ответа критически важные вопросы: «Где?», «Когда?» и «Откуда?».

В этом заключается ключевое различие между сигналом и событием.

📋 Ключевые понятия:

  • Сигнал (Signal): Необработанное значение от датчика или входа (`0`, `1`, `22.5`, `450`).
  • Событие (Event): Сигнал, обогащенный контекстом. Событие — это информационное сообщение, которое несет в себе не только значение, но и метаданные: время возникновения, источник, местоположение и другие атрибуты.
  • Контракт сообщения (Message Contract): Формальное, стандартизированное описание структуры события. Это соглашение между всеми компонентами системы о том, в каком формате они обмениваются данными.

Использование неструктурированных сигналов напрямую в сценариях автоматизации приводит к созданию хрупких и трудноподдерживаемых систем. Каждый раз, когда вы захотите использовать данные от датчика, вам придется «вспоминать», что означает `1` для этого конкретного устройства, в каком топике оно публикуется и как его обработать.

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

---

Анатомия события в MQTT: топик и полезная нагрузка (payload)

Как мы обсуждали ранее, протокол MQTT является транспортным уровнем для обмена сообщениями в нашей экосистеме. Каждое сообщение в MQTT состоит из двух основных частей: топика (topic) и полезной нагрузки (payload). При создании системы событий мы используем обе эти части для передачи контекста.

1. Топик (Topic) — Адрес События

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

`/префикс/локация/устройство/параметр`

  • Префикс: Определяет тип данных или домен. Например, `/devices` для сырых данных от драйверов оборудования и `/events` для очищенных, стандартизированных событий. Это позволяет легко отделить «черновые» данные от «чистовых».
  • Локация: Описывает физическое расположение устройства. Может быть многоуровневой, например: `office/floor1/room101` или `cottage/garage`.
  • Устройство: Уникальный идентификатор или логическое имя устройства, например, `motion_sensor_01` или `climate_control`.
  • Параметр: Конкретная характеристика, о которой сообщает устройство: `temperature`, `motion`, `state`.
Пример:
  • Сырой сигнал: Драйвер Modbus для датчика температуры в переговорной комнате №101 публикует значение `22.3` в топик: `/devices/wb-ms_v2_28/temperature`. Этот топик описывает физическое устройство (`wb-ms_v2` с адресом `28`) и его параметр.
  • Структурированное событие: После обработки и обогащения контекстом, наша система публикует новое сообщение в топик: `/events/office/floor1/room101/climate/temperature`. Этот топик описывает логическую суть события, а не конкретную железку. Если вы завтра замените датчик на другую модель, топик события останется прежним, и ни один сценарий не сломается.

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

2. Полезная нагрузка (Payload) — Содержание События

Если топик — это адрес на конверте, то полезная нагрузка — это само письмо. В `payload` мы помещаем детальную информацию о событии в стандартизированном формате. Для сырых сигналов `payload` часто содержит одно-единственное значение (`22.3`, `1`, `true`), но для структурированных событий мы всегда используем формат JSON (JavaScript Object Notation).

В Node-RED эти два компонента представлены в объекте `msg`:

  • `msg.topic` - содержит строку топика.
  • `msg.payload` - содержит полезную нагрузку.

Связь между ними позволяет создавать мощную логику. Мы можем использовать `msg.topic` для маршрутизации, а `msg.payload` — для выполнения условных действий.

---

Контракт сообщения: стандартная структура msg.payload

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

Формат Полезной Нагрузки (Payload)

Мы используем JSON-объект со следующими полями:

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

|------|------|------------|----------------|

| `ts` | String | Временная метка события в формате ISO 8601 (например, `2023-10-27T12:35:01.123Z`). Используем UTC (Z) для однозначности. | Обязательное |

| `val`| any | Непосредственно значение сигнала (число, строка, булево). | Обязательное |

| `src`| String | Источник данных. Обычно это топик сырого сигнала (например, `/devices/wb-m1w2_34/temperature`). Критически важно для отладки. | Обязательное |

| `unit`| String | Единица измерения (`°C`, `ppm`, `%`, `V`). | Рекомендуемое |

| `type`| String | Тип данных (`float`, `integer`, `boolean`, `string`). Помогает избежать ошибок при обработке. | Рекомендуемое |

| `meta`| Object | Объект с дополнительными метаданными, которые могут быть полезны для сценариев (например, имя комнаты, ID пользователя, и т.д.). | Опциональное |

Пример 1: Событие от датчика температуры

Сырой сигнал: топик `/devices/wb-msw-v3_25/temperature`, payload `24.8`.

Стандартизированное событие (payload в топике `/events/office/floor1/room101/climate/temperature`):

{

"ts": "2023-10-27T12:40:15.589Z",

"val": 24.8,

"src": "/devices/wb-msw-v3_25/temperature",

"unit": "°C",

"type": "float",

"meta": {

"room_name": "Переговорная 'Озеро'"

}

}

Этот объект самодостаточен. Любой сценарий, получивший его, немедленно знает:

  • Когда это произошло (`ts`).
  • Что произошло: температура изменилась до `24.8` градусов Цельсия (`val`, `unit`).
  • Откуда пришли исходные данные (`src`).
  • Где это находится (`meta.room_name` и сам топик).

Пример 2: Событие от датчика движения (геркона на двери)

Сырой сигнал: топик `/devices/wb-mdm3_50/Input 1`, payload `1`.

Стандартизированное событие (payload в топике `/events/office/main_entrance/security/door_opened`):

{

"ts": "2023-10-27T12:42:01.012Z",

"val": true,

"src": "/devices/wb-mdm3_50/Input 1",

"type": "boolean",

"meta": {

"device_type": "геркон"

}

}

Обратите внимание, как мы преобразовали `1` в более понятное логическое `true`. Это упрощает написание сценариев: вместо `if (msg.payload.val == 1)` можно писать `if (msg.payload.val)`.

---

Практика в Node-RED: преобразование сигнала в событие

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

Сценарий:

Система получает данные от датчика температуры, подключенного через модуль расширения. Драйвер модуля публикует температуру в виде строки в топик `/devices/wb-ms_v2_28/temperature`.

Задача:

Создать поток, который:

  • Подписывается на топик `/devices/wb-ms_v2_28/temperature`.
  • Преобразует входящее сырое значение в JSON-объект согласно нашему контракту.
  • Публикует новое, структурированное событие в топик `/events/lab/room1/climate/temperature`.
  • Структура потока (Flow):
    [MQTT In]────────>[Function]────────>[MQTT Out]
    

    │

    └─────────────>[Debug: Raw]

    [Debug: Event]─────┘ (подключен к выходу Function)

    Шаг 1: Настройка узла `MQTT In`
    • Сервер: Выберите ваш MQTT-брокер (обычно `localhost:1883`).
    • Топик: `/devices/wb-ms_v2_28/temperature`
    • QoS: `0`
    • Вывод: `строка`
    • Имя: `Raw Temp Signal`
    Шаг 2: Настройка узла `Function` (ядро преобразования)

    Это самый важный узел. Он содержит JavaScript-код, который выполняет преобразование.

    • Имя: `Build Event Object`
    • Код: Скопируйте следующий код в редактор функции.
    // Сохраняем исходный топик для поля 'src'
    

    const sourceTopic = msg.topic;

    // Извлекаем сырое значение. MQTT-драйверы часто присылают

    // значения в виде строки, поэтому преобразуем его в число.

    // parseFloat является безопасным способом преобразования.

    const rawValue = parseFloat(msg.payload);

    // Проверяем, удалось ли преобразование. Если нет - прерываем поток.

    // Это базовая валидация данных.

    if (isNaN(rawValue)) {

    node.warn("Не удалось преобразовать payload в число: " + msg.payload);

    return null; // Останавливаем дальнейшую обработку сообщения

    }

    // 1. Создаем новый объект msg.payload согласно нашему контракту.

    const eventPayload = {

    // 'ts': Временная метка в формате ISO 8601 (UTC)

    ts: new Date().toISOString(),

    // 'val': Числовое значение температуры

    val: rawValue,

    // 'src': Исходный топик для отладки

    src: sourceTopic,

    // 'unit' и 'type': Добавляем важные метаданные

    unit: "°C",

    type: "float"

    };

    // 2. Заменяем старый msg.payload на наш новый объект события.

    msg.payload = eventPayload;

    // 3. Устанавливаем новый топик для публикации события.

    msg.topic = "/events/lab/room1/climate/temperature";

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

    return msg;

    > ⚠️ Внимание: Всегда проверяйте и приводите типы данных. Значение, пришедшее как строка (`'25.5'`), должно быть преобразовано в число (`25.5`) с помощью функций `parseFloat()` или `parseInt()`. Иначе логика сценариев, ожидающая числовые операции (например, `> 25`), может работать некорректно или вызывать ошибки.

    Шаг 3: Настройка узла `MQTT Out`
    • Сервер: Выберите ваш MQTT-брокер.
    • Топик: Оставьте пустым. Узел автоматически использует `msg.topic`, который мы задали в узле `Function`.
    • QoS: Оставьте по умолчанию.
    • Retain: Оставьте по умолчанию (`false`).
    • Имя: `Publish Clean Event`
    Шаг 4: Настройка узлов `Debug`
  • Debug: Raw: Подключите к выходу `MQTT In`. Установите вывод на `полный объект сообщения`. Это позволит вам видеть исходный, сырой сигнал.
  • Debug: Event: Подключите к выходу `Function`. Установите вывод на `полный объект сообщения`. Это покажет вам результат работы — готовый объект события.
  • Разверните поток (Deploy). Теперь, когда в топик `/devices/wb-ms_v2_28/temperature` придет сообщение (например, `24.5`), вы увидите в панели отладки два вывода:

    • От "Raw Temp Signal": `msg.payload` будет строкой `"24.5"`.
    • От "Build Event Object": `msg.payload` будет полноценным JSON-объектом, соответствующим нашему контракту.

    ---

    Заключение: ценность контракта и следующие шаги

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

    Ключевые выводы:

    • Сигнал ограничен, событие — информативно.
    • Контракт сообщения на базе JSON — это фундамент для построения надежной, масштабируемой и легко поддерживаемой системы автоматизации.
    • Разделение топиков на `/devices` (сырые данные) и `/events` (очищенные данные) позволяет изолировать логику обработки от аппаратных драйверов.
    • Преобразование данных в единый формат происходит один раз на входе в систему, что кардинально упрощает все последующие сценарии.

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

    Что дальше?

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

    🔗 Следующий урок: COURSE-01-M03-L01 "Базовые сценарии: реакция на события", где мы научимся создавать логику, которая реагирует на наши структурированные события для управления освещением, климатом и другими системами.