Главная → Академия → Node-RED: установка, flows, msg/JSON, отладка → Практика: 5 примеров JSONata (выборка, фильтрация, агрегация)

Практика: 5 примеров JSONata (выборка, фильтрация, агрегация)

Введение: от теории к практике JSONata

В предыдущем уроке мы познакомились с теоретическими основами JSONata — мощного языка запросов и преобразований для JSON-данных. Мы выяснили, что его основная роль в экосистеме контроллеров HI — это декларативная трансформация данных без необходимости написания сложного кода в ноде `Function`. Это позволяет инженерам автоматизации создавать более читаемые, надежные и легко поддерживаемые потоки (flows).

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

  • Выборка и преобразование: извлечение и форматирование данных из вложенной структуры MQTT-сообщения.
  • Фильтрация: отбор нужных устройств из массива по заданным критериям (например, из списка светильников DALI).
  • Агрегация: вычисление суммарных и средних значений по группе устройств (например, счетчиков с шины Modbus).
  • Условная логика: создание человекочитаемых статусов на основе машинных данных (`true`/`false`).
  • Комплексное форматирование: объединение нескольких техник для генерации сложных выходных структур.
  • > 🔗 Связанный материал: Прежде чем продолжить, убедитесь, что вы изучили основы синтаксиса в уроке `COURSE-06-M03-L05 "Введение в JSONata: что это и зачем нужно"`. Понимание базовых операторов является обязательным для освоения материала этого урока.

    Для выполнения практических заданий подготовьте ваше рабочее пространство в Node-RED. Создайте новую вкладку и для каждого примера используйте ноду `Inject` для эмуляции входящего сообщения. В поле `msg.payload` ноды `Inject` вставляйте JSON-структуры, которые будут приведены в каждом примере. Результат работы JSONata-выражения, которое мы поместим в ноду `Change`, отслеживайте с помощью ноды `Debug`.

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

    ---

    Пример 1: Выборка и трансформация данных с MQTT-датчика

    Это одна из самых частых задач. Умные устройства, особенно работающие по протоколам Zigbee, Z-Wave или кастомным MQTT-протоколам, часто отправляют данные в виде сложного вложенного JSON-объекта, содержащего показания сразу нескольких сенсоров. Наша цель — извлечь из этого объекта только нужные нам данные и привести их к стандартному контракту сообщения, принятому в нашей академии.

    Анализ входящей структуры

    Предположим, наш контроллер HI получает по MQTT топику `telemetry/room-101/multisensor` следующее сообщение:

    {
    

    "device_id": "zigbee-0A1B-3C4D",

    "model": "Aqua-Comfort-3000",

    "rssi": -78,

    "battery_level": 92,

    "data": {

    "temperature": 245,

    "humidity": 48.2,

    "co2": 915

    },

    "timestamp": 1678886400000

    }

    Здесь мы видим несколько особенностей:

    • Температура (`temperature`) передается как целое число `245`, что на самом деле означает `24.5 °C`. Это распространенная практика для избежания проблем с числами с плавающей запятой.
    • Полезные нам данные находятся внутри вложенного объекта `data`.
    • Сообщение содержит много метаинформации, которая нам не нужна для простой логики управления климатом.

    Задача

    Необходимо преобразовать входящий `msg.payload` в новую структуру, содержащую только значение температуры в градусах Цельсия, идентификатор источника и временную метку.

    Решение с помощью JSONata

    Мы будем использовать ноду `Change` с режимом "Set" и типом выражения "JSONata".

  • Простая выборка: Чтобы просто извлечь значение температуры, мы используем путь к свойству.
  • * Выражение: `payload.data.temperature`

    * Результат: `245`

  • Математическая трансформация: Теперь разделим полученное значение на 10, чтобы получить реальную температуру.
  • * Выражение: `payload.data.temperature / 10`

    * Результат: `24.5`

  • Формирование нового объекта (итоговое решение): Создадим новый JSON-объект, который соответствует нашему стандартному контракту. JSONata позволяет конструировать JSON "на лету".
  •     {

    "value": payload.data.temperature / 10,

    "unit": "°C",

    "source": payload.device_id,

    "ts": payload.timestamp

    }

    Поместив это выражение в ноду `Change`, мы получим на выходе `msg` со следующим `payload`:

        {

    "value": 24.5,

    "unit": "°C",

    "source": "zigbee-0A1B-3C4D",

    "ts": 1678886400000

    }

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

    > 💡 Подсказка: Используйте встроенный в Node-RED редактор выражений (Expression Editor) в ноде `Change`. При нажатии на кнопку `...` открывается окно, где можно тестировать JSONata в реальном времени. Во вкладке "Example" можно вставить пример `msg`, и редактор сразу покажет результат работы вашего выражения, подсвечивая синтаксические ошибки. Это значительно ускоряет отладку.

    ---

    Пример 2: Фильтрация массива устройств (KNX/DALI)

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

    Анализ входящей структуры

    Предположим, у нас есть поток, который опрашивает состояние всех светильников в офисном опенспейсе. В `msg.payload` приходит следующий массив:

    [
    

    { "id": "dali-light-01", "name": "Desk 1-2", "state": true, "brightness": 80 },

    { "id": "dali-light-02", "name": "Desk 3-4", "state": true, "brightness": 80 },

    { "id": "dali-light-03", "name": "Corridor", "state": false, "brightness": 0 },

    { "id": "dali-light-04", "name": "Meeting", "state": true, "brightness": 45 },

    { "id": "dali-light-05", "name": "Lounge", "state": true, "brightness": 100 }

    ]

    Это список из пяти светильников с их состоянием (`state`) и текущей яркостью (`brightness`).

    Задача

    Нужно выполнить две задачи:

    • А) Получить список только тех светильников, которые в данный момент включены (`state` равно `true`).
    • Б) Получить список светильников, которые включены и яркость которых установлена выше 50%.

    Решение с помощью JSONata

    Для фильтрации массивов в JSONata используется синтаксис предиката `[<условие>]`. Условие пишется внутри квадратных скобок и применяется к каждому элементу массива.

  • Фильтрация по одному условию (задача А):
  • * Выражение: `payload[state = true]`

    * Результат: JSONata вернет новый массив, содержащий все объекты, у которых поле `state` равно `true`. Светильник `dali-light-03` будет отфильтрован.

        [

    { "id": "dali-light-01", "name": "Desk 1-2", "state": true, "brightness": 80 },

    { "id": "dali-light-02", "name": "Desk 3-4", "state": true, "brightness": 80 },

    { "id": "dali-light-04", "name": "Meeting", "state": true, "brightness": 45 },

    { "id": "dali-light-05", "name": "Lounge", "state": true, "brightness": 100 }

    ]

  • Фильтрация по нескольким условиям (задача Б): Для комбинирования условий используются логические операторы `and` и `or`.
  • * Выражение: `payload[state = true and brightness > 50]`

    * Результат: Это выражение оставит в массиве только те светильники, которые одновременно включены И имеют яркость больше 50.

        [

    { "id": "dali-light-01", "name": "Desk 1-2", "state": true, "brightness": 80 },

    { "id": "dali-light-02", "name": "Desk 3-4", "state": true, "brightness": 80 },

    { "id": "dali-light-05", "name": "Lounge", "state": true, "brightness": 100 }

    ]

  • Проекция результата: Часто после фильтрации нам нужен не весь объект, а только некоторые его поля. JSONata позволяет комбинировать фильтрацию с созданием новой структуры.
  • * Выражение: `payload[state = true and brightness > 50].{"device_id": id, "level": brightness}`

    * Результат: Сначала будет выполнен отбор, а затем для каждого элемента в отфильтрованном массиве будет создан новый объект указанной структуры.

        [

    { "device_id": "dali-light-01", "level": 80 },

    { "device_id": "dali-light-02", "level": 80 },

    { "device_id": "dali-light-05", "level": 100 }

    ]

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

    ---

    Пример 3: Агрегация данных с Modbus-счетчиков

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

    > ⚠️ Внимание: Агрегирующие функции могут создавать заметную нагрузку на CPU контроллера при работе с очень большими массивами данных (>1000 элементов). В таких сценариях рассмотрите возможность предварительной обработки, разделения данных на части или выполнения расчетов в базе данных MySQL, если данные там хранятся.

    Анализ входящей структуры

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

    {
    

    "poll_timestamp": 1678890000000,

    "source_bus": "RS485-1",

    "meters": [

    { "slave_id": 5, "power_W": 2100.5, "status": "ok" },

    { "slave_id": 6, "power_W": 1570.2, "status": "ok" },

    { "slave_id": 7, "power_W": null, "status": "timeout" },

    { "slave_id": 8, "power_W": 3250.0, "status": "ok" }

    ]

    }

    Здесь `meters` — это массив объектов, каждый из которых содержит `slave_id` устройства по Modbus и его текущую мощность `power_W`. Обратите внимание, что одно из устройств (`slave_id: 7`) не ответило, и его мощность равна `null`.

    Задача

    Необходимо рассчитать:

    • А) Общую потребляемую мощность по всем работающим счетчикам.
    • Б) Среднюю нагрузку на один работающий счетчик.
    • В) Количество счетчиков, которые успешно ответили на запрос.

    Решение с помощью JSONata

    JSONata предоставляет встроенные агрегирующие функции, которые идеально подходят для этой задачи: `$sum()`, `$average()`, `$count()`.

  • Подготовка массива данных: Сначала нам нужно извлечь только значения мощности из массива объектов. Это делается путем указания пути `payload.meters.power_W`. JSONata автоматически создаст простой массив из значений этого поля: `[2100.5, 1570.2, null, 3250.0]`. Важно, что агрегирующие функции по умолчанию игнорируют `null` значения.
  • Вычисление суммы (задача А):
  • * Выражение: `$sum(payload.meters.power_W)`

    * Результат: `6920.7` (2100.5 + 1570.2 + 3250.0)

  • Вычисление среднего (задача Б):
  • * Выражение: `$average(payload.meters.power_W)`

    * Результат: `2306.9` (6920.7 / 3)

  • Подсчет количества (задача В): Функция `$count()` подсчитывает количество элементов в массиве.
  • * Выражение: `$count(payload.meters.power_W)`

    * Результат: `3` (подсчитываются только не-null значения, что в данном случае эквивалентно количеству ответивших счетчиков).

  • Комплексный отчет: Мы можем объединить все вычисления в один выходной объект.
  •     {

    "total_power": $sum(payload.meters.power_W),

    "average_power": $average(payload.meters.power_W),

    "online_meters": $count(payload.meters.power_W),

    "total_meters": $count(payload.meters)

    }

    Результат:

        {

    "total_power": 6920.7,

    "average_power": 2306.9,

    "online_meters": 3,

    "total_meters": 4

    }

    Этот подход позволяет создавать сводные отчеты и вычислять ключевые показатели эффективности (KPI) прямо на уровне контроллера HI, без привлечения внешних систем.

    ---

    Пример 4: Условная логика и форматирование статуса

    Системы автоматизации оперируют двоичными состояниями: `true`/`false`, `0`/`1`, `ON`/`OFF`. Однако для пользователя (в интерфейсе, в push-уведомлениях или отчетах) эти состояния нужно преобразовывать в понятный текст.

    Анализ входящей структуры

    Рассмотрим простой пример от датчика протечки. При срабатывании он отправляет сообщение:

    {
    

    "sensor_id": "leak-detector-kitchen-01",

    "payload": {

    "leak_detected": true

    },

    "location": "Kitchen, under sink"

    }

    Если протечки нет, поле `leak_detected` будет равно `false`.

    Задача

    Создать человекочитаемое сообщение для отправки администратору объекта через Telegram или MQTT. Сообщение должно быть разным в зависимости от состояния датчика.

    Решение с помощью JSONata

    Для реализации условной логики в JSONata используется условный (тернарный) оператор: `<условие> ? <выражение_если_true> : <выражение_если_false>`.

  • Базовое использование: Преобразуем булево значение в строку.
  • * Выражение: `payload.payload.leak_detected ? "Обнаружена протечка!" : "Система в норме"`

    * Результат, если `leak_detected` это `true`: `"Обнаружена протечка!"`

    * Результат, если `leak_detected` это `false`: `"Система в норме"`

  • Объединение со строками: Тернарный оператор можно использовать внутри более сложных выражений, например, для конкатенации строк (оператор `&`).
  •     "Статус объекта: " & (payload.payload.leak_detected ? "ТРЕВОГА" : "НОРМА") & ". Место: " & location

    * Результат (при `true`): `"Статус объекта: ТРЕВОГА. Место: Kitchen, under sink"`

  • Формирование объекта для отправки в мессенджер: Мы можем использовать эту логику, чтобы сформировать целый объект, например, для бота в Telegram, который может содержать не только текст, но и другие параметры.
  •     {

    "chat_id": "admin_group",

    "text": "Датчик " & sensor_id & " сообщает: " & (payload.payload.leak_detected ? "🚨 ВНИМАНИЕ, ПРОТЕЧКА! 🚨" : "✅ Все в порядке."),

    "priority": payload.payload.leak_detected ? "high" : "low"

    }

    Результат (при `true`):

        {

    "chat_id": "admin_group",

    "text": "Датчик leak-detector-kitchen-01 сообщает: 🚨 ВНИМАНИЕ, ПРОТЕЧКА! 🚨",

    "priority": "high"

    }

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

    ---

    Заключение: Итоги и лучшие практики

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

    • Выборка по пути: `payload.data.value` для доступа к данным во вложенных структурах.
    • Фильтрация массивов: `payload[state=true and brightness > 50]` для отбора элементов по одному или нескольким условиям.
    • Агрегация данных: `$sum()`, `$average()`, `$count()` для выполнения математических расчетов над наборами данных.
    • Условная логика: тернарный оператор `<условие> ? <да> : <нет>` для динамического преобразования значений.
    • Конструирование объектов: `{ "key": value }` для создания новых JSON-структур "на лету".

    Освоение этих техник позволяет значительно сократить использование ноды `Function` для задач трансформации данных, делая потоки более читаемыми, надежными и стандартизированными. JSONata становится вашим "швейцарским ножом" для быстрой и эффективной обработки JSON на контроллерах HI.

    > 💡 Лучшая практика: Всегда документируйте сложные JSONata-выражения с помощью комментариев прямо в описании ноды `Change`. Напишите, какую задачу решает выражение и что ожидается на входе и выходе. Это сэкономит огромное количество времени вам и вашим коллегам в будущем при поддержке проекта.

    Что дальше?

    Мы рассмотрели базовые, но самые востребованные возможности JSONata. В следующих уроках мы углубимся в более сложные темы:

    • Использование встроенных функций для работы со строками, массивами и числами.
    • Создание и вызов собственных функций внутри JSONata-выражений.
    • Работа с переменными и контекстом для выполнения еще более сложных преобразований.