ГлавнаяАкадемияNode-RED: установка, flows, msg/JSON, отладка → Паттерн 'Rules Engine'

Паттерн 'Rules Engine'

Урок 5 · Node-RED: установка, flows, msg/JSON, отладка · 30 мин · theory

LESS-06-M05-L06: Введение в паттерн 'Rules Engine'

Паттерн Rules Engine (или Движок Правил) — это мощный архитектурный подход, который позволяет отделить и централизовать логику принятия решений от основного потока выполнения программы. Вместо того чтобы "размазывать" бизнес-логику по множеству узлов `switch` и `function`, мы создаем единый механизм, который декларативно, то есть на основе описания, определяет, какое действие необходимо выполнить в ответ на определенный набор входящих данных.

Представьте себе работу диспетчерского центра на промышленном объекте. Диспетчер не принимает решения хаотично. У него есть четкий регламент — набор правил:

В этой аналогии:

Основное преимущество такого подхода — централизация. Когда вся логика принятия решений собрана в одном месте и описана в виде структурированного набора правил, систему становится значительно проще понимать, поддерживать и масштабировать. Если завтра изменится регламент, инженеру не придется искать нужный узел `function` в десятках потоков. Ему достаточно будет изменить или добавить правило в централизованном движке. Это кардинально снижает риск внесения ошибок и ускоряет модификацию системы.

В контексте Node-RED на контроллере HI, классический подход с разветвленными потоками, где каждый `switch` узел проверяет одно условие, быстро приводит к так называемой "лапше" (Spaghetti Flow). Движок правил позволяет превратить сложную, запутанную схему в линейный, понятный и легко читаемый поток:

| Подход с разрозненной логикой | Подход с 'Rules Engine' |

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

| Множество узлов `switch` и `function` по всему потоку. | Один или несколько централизованных узлов движка правил. |

| Логика принятия решений смешана с логикой преобразования данных. | Логика принятия решений отделена (декларативна). |

| Изменение одного правила может потребовать правки нескольких узлов. | Изменение правила затрагивает только его описание. |

| Сложно понять общую картину автоматизации. | Вся логика видна в одном месте (в наборе правил). |

| Высокий риск логических ошибок при модификации. | Низкий риск, так как структура потока не меняется. |

Таким образом, паттерн 'Rules Engine' — это переход от императивного стиля ("как делать") к декларативному ("что делать"), что является признаком зрелого и профессионального подхода к проектированию систем автоматизации.

---

Компоненты 'Rules Engine': Факты, Правила и Действия

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

> 🔗 Связанный материал: Факты часто представляют собой состояние системы. Рекомендуем повторить урок COURSE-06-M05-L03 'Паттерн State Machine' для лучшего понимания управления состоянием.

### Факты (Facts)

Факты — это атомарные единицы информации, которые служат "сырьем" для движка правил. Это объективные данные о текущем состоянии системы и окружающей среды, на основе которых будут приниматься решения.

В системе на базе контроллера HI фактами могут быть:

Факты должны быть представлены в простом и понятном формате "ключ-значение". Например, для нашего движка мы можем собрать все факты в один объект:

{

"temperature_living_room": 24.5,

"humidity_living_room": 55,

"motion_detected": true,

"illuminance": 350,

"window_open": false,

"active_scene": "День",

"time_of_day": "14:30"

}

### Правила (Rules)

Правила — это сердце движка. Каждое правило представляет собой формализованную логическую конструкцию "Если (условие), то (действие)".

### Действия (Actions)

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

Примеры действий:

### Инструменты для реализации в Node-RED

В Node-RED для создания движка правил можно использовать инструменты разной степени сложности и гибкости:

  • Стандартный узел `switch`: Идеален для простых движков с небольшим количеством статичных правил (до 5-10). Он позволяет проверять несколько условий для одного входящего сообщения и направлять его на разные выходы.
  • Стандартный узел `function`: Подходит для реализации сложной, кастомной логики правил, которая не укладывается в возможности узла `switch`. Однако это императивный подход, и при большом количестве правил код может стать запутанным.
  • Специализированные узлы (`node-red-contrib-json-rules` и аналоги): Лучший выбор для декларативных и динамических движков правил. В этом случае правила описываются в виде структуры данных (обычно JSON), которую можно хранить во внешнем файле, в базе данных или в контексте. Это позволяет изменять логику работы системы без необходимости редактировать и развертывать сам поток.
  • Выбор инструмента зависит от задачи. Для простого управления светом по датчику движения достаточно узла `switch`. Для сложной системы управления климатом в отеле, где правила могут настраиваться для каждого номера, необходим полноценный декларативный движок.

    ---

    Практическая реализация: узел 'switch' как простой движок правил

    Рассмотрим, как создать простой, но эффективный движок правил для сценария "Умное освещение" с помощью стандартного узла `switch`. Этот узел идеально подходит для задач, где правила статичны и их количество невелико.

    Задача: Автоматизировать освещение в гостиной. Логика должна следовать трем правилам:
  • Включить свет: Если обнаружено движение И уровень естественной освещенности ниже 50 люкс.
  • Выключить свет: Если движение не обнаруживалось в течение 5 минут.
  • Блокировка включения: Не включать свет автоматически, если в системе активен режим 'Ночь', даже при выполнении условия #1.
  • Для реализации нам понадобятся датчики, подключенные к универсальным входам контроллера HI, которые передают данные в Node-RED. Предположим, у нас есть поток, который собирает факты в одно сообщение.

    Пример входящего сообщения (факт):
    {
    

    "payload": {

    "device_id": "sensor_living_room",

    "motion": true,

    "illuminance": 45

    },

    "topic": "telemetry/living_room"

    }

    Также предположим, что текущий режим сцены хранится в контексте потока: `flow.get('scene_mode')`, который может принимать значения 'День', 'Вечер', 'Ночь'.

    ### Построение потока

    Логика выключения по отсутствию движения в течение 5 минут удобно реализуется с помощью узла `trigger`. Он будет отправлять сообщение "выключить" спустя 5 минут после последнего сообщения о наличии движения. Наш движок правил на `switch` сосредоточится на логике включения.

    ASCII-схема потока:
                                    +---------------------------+
    

    [Датчик движения/освещенности]-->| Function: "Собрать факты" |---+

    +---------------------------+ |

    |

    +----------------------------------------------------+

    |

    v

    +-----------------------------+

    | Switch: "Движок правил освещения" |

    +-----------------------------+

    | (Выход 1: ВКЛ Свет) | (Выход 2: Игнор)

    | |

    v v

    +--------------------------+ +----------------+

    | Function: "Форм. команду ON"| | Comment: "Ничего не делать" |

    +--------------------------+ +----------------+

    |

    v

    +--------------------+

    | MQTT Out: Команда реле |

    +--------------------+

    ### Настройка узла `switch`

    Узел `switch` — это наш мини-движок правил. Мы настроим его на проверку нескольких свойств одновременно, используя его способность работать с JSONata и переменными контекста.

  • Откройте настройки узла `switch`.
  • В поле "Property" оставьте `msg.payload`.
  • Добавьте правило (одно, но сложное) типа "Expression" (JSONata). Это выражение проверит все наши условия одновременно.
  • Выражение JSONata для правила включения света (Выход 1):
    payload.motion = true and payload.illuminance < 50 and $flowContext('scene_mode') != 'Ночь'
    
    Разбор выражения:
  • Настройте узел так, чтобы он останавливался после первого совпадения.
  • Добавьте второе правило "otherwise" (иначе), которое будет ловить все остальные сообщения и направлять их на второй выход (игнорирование).
  • ### Результат работы

        { "payload": { "motion": true, "illuminance": 45 } }
    

    и в контексте `flow.scene_mode` установлено значение 'Вечер', то JSONata-выражение вернет `true`. Сообщение пойдет на Выход 1. Узел `Function` за ним сформирует команду для реле, и свет включится.

        // Код в узле "Форм. команду ON"

    msg.payload = "ON";

    msg.topic = "hi/commands/living_room/light_main/set";

    return msg;

    Этот пример демонстрирует, как даже один узел `switch` с правильно составленным выражением может служить компактным и читаемым движком правил для несложных сценариев автоматизации. Главное — правильно подготовить факты перед подачей на вход движка.

    ---

    Продвинутый подход: динамические правила с node-red-contrib-json-rules

    Когда логика становится сложнее, а правила должны быть гибкими и изменяемыми без перезагрузки потоков, стандартные узлы достигают своего предела. Здесь на помощь приходят специализированные решения, такие как `node-red-contrib-json-rules`. Этот узел реализует полноценный декларативный движок правил, где сами правила являются данными в формате JSON.

    > 💡 Подсказка: Динамическое управление правилами идеально подходит для кастомизации автоматизаций конечным пользователем через UI. Вы можете создать в интерфейсе редактор правил, который будет сохранять JSON в `global` контекст, а движок будет их подхватывать.

    ### Установка и принцип работы

    Для начала установите узел через `Manage palette` в интерфейсе Node-RED:

    # В терминале контроллера HI в директории ~/.node-red
    

    npm install node-red-contrib-json-rules

    После перезапуска Node-RED у вас появится узел `rules`.

    Принцип его работы следующий:

  • На вход №1 (Facts) подается объект с фактами.
  • На вход №2 (Rules) подается массив с правилами в формате JSON. Это можно сделать один раз при старте потока.
  • Узел обрабатывает каждый факт, применяя к нему все правила.
  • На выходе генерируется сообщение для каждого сработавшего правила, содержащее результат (действие).
  • Ключевое преимущество в том, что правила — это не код, а данные. Их можно хранить в файле, в базе данных MySQL на контроллере или в глобальном контексте, что позволяет изменять их "на лету".

    ### Практический пример: Управление климатом

    Задача: Создать гибкую систему управления кондиционером в офисном помещении. Правила должны быть легко изменяемы. Факты, которые мы будем собирать: JSON-структура с правилами:

    Мы можем сохранить этот JSON в глобальном контексте с помощью узла `Inject` при старте.

    [
    

    {

    "if": [

    { "fact": "temperature", "operator": ">=", "value": 25 },

    { "fact": "presence", "operator": "==", "value": true },

    { "fact": "window_open", "operator": "==", "value": false }

    ],

    "then": [

    {

    "action": "set_ac",

    "params": { "state": "cool", "temperature": 22, "fan_speed": "auto" }

    }

    ]

    },

    {

    "if": [

    { "fact": "presence", "operator": "==", "value": false }

    ],

    "then": [

    {

    "action": "set_ac",

    "params": { "state": "off" }

    }

    ]

    },

    {

    "if": [

    { "fact": "window_open", "operator": "==", "value": true }

    ],

    "then": [

    {

    "action": "set_ac",

    "params": { "state": "off" }

    },

    {

    "action": "notify_user",

    "params": { "message": "Кондиционер выключен, так как открыто окно." }

    }

    ]

    }

    ]

    Разбор правила: - `"fact"`: Имя факта, который мы проверяем.

    - `"operator"`: Оператор сравнения (`>`, `<`, `==`, `!=`, etc.).

    - `"value"`: Значение для сравнения.

    - `"action"`: Имя действия, которое мы хотим сгенерировать.

    - `"params"`: Параметры этого действия.

    ### Построение потока и обработка результатов

    ASCII-схема:
    [Inject: правила] --(2)-->+-----------------+
    

    | rules |

    [Сбор фактов] ----(1)-->| (Движок правил) | --(результат)-->[Switch: `action`]

    +-----------------+ |

    +--(set_ac)-->[Формирование команды AC]

    |

    +--(notify_user)-->[Отправка уведомления]

  • При старте Node-RED узел `Inject` отправляет JSON с правилами на второй вход узла `rules`. Движок загружает и запоминает их.
  • Поток сбора фактов периодически (например, раз в минуту) формирует объект с текущими данными:
  •     {

    "temperature": 26,

    "humidity": 60,

    "window_open": false,

    "presence": true,

    "work_hours": true

    }

    и подает его на первый вход узла `rules`.

  • Движок `rules` применяет правила. В данном случае сработает первое правило (`temperature >= 25` и `presence == true` и `window_open == false`).
  • На выходе узла `rules` появится сообщение:
  •     {

    "action": "set_ac",

    "params": {

    "state": "cool",

    "temperature": 22,

    "fan_speed": "auto"

    },

    "if": [ / ...условия, которые сработали... / ]

    }

  • Далее узел `switch`, настроенный на проверку `msg.action`, направляет это сообщение в соответствующую ветку потока, которая уже отвечает за формирование и отправку конкретной Modbus- или MQTT-команды на кондиционер.
  • Используя `node-red-contrib-json-rules`, мы получаем невероятно гибкую систему. Добавить новое правило или изменить порог температуры теперь можно, просто отредактировав JSON-объект в глобальном контексте через UI Dashboard, без единого изменения в логике потока Node-RED.

    ---

    Интеграция с другими паттернами и антипаттерны

    Паттерн 'Rules Engine' не существует в вакууме. Наибольшую эффективность он показывает в комбинации с другими паттернами проектирования, рассмотренными нами ранее. Однако, как и любой мощный инструмент, его можно использовать неправильно.

    > ⚠️ Внимание: Слишком большое количество сложных правил (>50-100), выполняемых на высокой частоте, может привести к заметной нагрузке на CPU контроллера HI. Профилируйте производительность при построении комплексных систем.

    ### Интеграция с другими паттернами

  • Rules Engine + Scheduler:
  • Эта комбинация позволяет выполнять проверку правил не по каждому изменению фактов, а по расписанию. Это крайне полезно для некритичных систем, чтобы снизить нагрузку.

    * Пример: Система управления отоплением. Нет смысла пересчитывать логику каждую секунду. Достаточно запускать `Rules Engine` раз в 5-10 минут с помощью узла `Inject`, настроенного как Scheduler. Он инициирует сбор свежих фактов (температуры, уставки) и подает их на вход движка.

  • Rules Engine + State Machine:
  • Движок правил отлично подходит для определения переходов в конечном автомате.

    * Пример: Система управления поливом.

    * Состояния: `Idle`, `Watering`, `Paused_Rain`.

    * Факты: `soil_moisture`, `rain_sensor_active`, `time_of_day`.

    * Правило: `IF state is 'Idle' AND soil_moisture < 30% AND rain_sensor_active = false THEN action = 'change_state_to_watering'`.

    * Движок правил определяет, что нужно сменить состояние, а сам механизм смены состояния и хранения (`flow.context`) реализуется, как описано в уроке про `State Machine`.

  • Rules Engine + Command/Acknowledge (Cmd/Ack):
  • Это одна из самых важных комбинаций для создания надежных систем. Движок правил не должен напрямую управлять оборудованием. Его задача — выразить намерение.

    * Пример: `Rules Engine` решает, что нужно включить насос (`action: 'set_pump_on'`). Это действие отправляется в виде MQTT-сообщения `.../set` с командой. Отдельный субпоток, реализующий паттерн `Cmd/Ack`, принимает эту команду, отправляет ее на Modbus-устройство, ждет подтверждения `.../state` и сообщает об успешном выполнении. Таким образом, сложная логика правил отделена от низкоуровневой логики взаимодействия с оборудованием.

    ### Распространенные антипаттерны

  • Антипаттерн "Божественный узел" (God Node):
  • * Проблема: Попытка реализовать всю логику системы, включая десятки сложных правил, внутри одного гигантского узла `function`. Такой код становится абсолютно нечитаемым, его невозможно отлаживать и поддерживать. Любое изменение превращается в испытание.

    * Решение: Разделяй и властвуй. Если правила можно сгруппировать по функциональности (управление светом, управление климатом), создайте для каждой группы свой, отдельный движок правил. Если правила сложны, но их можно описать декларативно — используйте специализированные узлы вроде `node-red-contrib-json-rules`.

  • Антипаттерн "Жестко закодированные правила" (Hard-coded Rules):
  • * Проблема: Множество "магических чисел" и условий, зашитых прямо в код узлов `function` или `switch`. Например, `if (msg.payload.temp > 22.5 && msg.payload.hour > 8) ...`. Чтобы изменить порог `22.5` на `23`, нужно найти этот узел, отредактировать код и заново развернуть поток.

    * Решение: Выносите все пороговые значения и параметры в переменные контекста (`flow.get('heating_threshold')`) или используйте декларативный подход, где правила являются данными (JSON). Это позволит менять логику работы системы через панель управления или конфигурационный файл, что особенно важно для систем, которые будет обслуживать не только разработчик, но и конечный пользователь.

    ### Что дальше

    В этом уроке мы рассмотрели паттерн 'Rules Engine' как мощный инструмент для централизации и декларативного описания логики автоматизации. Мы научились строить простые движки на базе узла `switch` и продвинутые, динамические системы с помощью `node-red-contrib-json-rules`.

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