Практика: 5 примеров JSONata (выборка, фильтрация, агрегация)
Введение: от теории к практике JSONata
В предыдущем уроке мы познакомились с теоретическими основами JSONata — мощного языка запросов и преобразований для JSON-данных. Мы выяснили, что его основная роль в экосистеме контроллеров HI — это декларативная трансформация данных без необходимости написания сложного кода в ноде `Function`. Это позволяет инженерам автоматизации создавать более читаемые, надежные и легко поддерживаемые потоки (flows).
Этот урок полностью посвящен практике. Мы разберем пять реальных сценариев, с которыми вы гарантированно столкнетесь при работе на объектах. Каждый пример будет пошагово демонстрировать, как с помощью одного JSONata-выражения решить конкретную прикладную задачу:
> 🔗 Связанный материал: Прежде чем продолжить, убедитесь, что вы изучили основы синтаксиса в уроке `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`
* Выражение: `payload.data.temperature / 10`
* Результат: `24.5`
{
"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 }
]
* Выражение: `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 }
]
* Выражение: `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()`.
* Выражение: `$sum(payload.meters.power_W)`
* Результат: `6920.7` (2100.5 + 1570.2 + 3250.0)
* Выражение: `$average(payload.meters.power_W)`
* Результат: `2306.9` (6920.7 / 3)
* Выражение: `$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"`
{
"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-выражений.
- Работа с переменными и контекстом для выполнения еще более сложных преобразований.