Skip to content

wirenboard/wb-rules

Repository files navigation

wb-rules

Rule engine for Wiren Board, version 2.0

Движок правил для контроллеров Wiren Board, версия 2.0

Содержание

Правила

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

Правила пишутся на языке ECMAScript 5 (диалектом которого является Javascript) и загружаются в контроллер в папку /etc/wb-rules.

Если вы не писали на этом языке, то можете изучить синтаксис и принципы программирования с помощью учебника по JavaScript: https://learn.javascript.ru; но при этом учитавайте отличия и возможности языка ECMAScript 5: https://es5.javascript.ru/.

  • вместо alert() используйте log();
  • let не поддерживается, попробуйте использовать var;
  • не поддерживаются функции-стрелки — это когда пишут var sum = (a, b) => a + b; вместо var sum = function(a, b) {return a + b;};, второй вариант будет работать.

Определение правил

Правила определяются при помощи функции defineRule:

defineRule(name,{
  Тип_правила: function() {
    ...
  },
  then: function() {
    ...
  }
});

Параметр name — произвольное имя правила (не обязательный); Тип_правила — указывается один из существующих типов правил (читайте ниже), определяет условия для срабатывания правила; then — определяет функцию, которая выполняется при срабатывании правила. Например:

defineRule("Examle_rule", {
  whenChanged: "mydev/test",
  then: function() {
    log("mydev/test changed");
  }
});

Типы правил

whenChanged. При задании whenChanged правило срабатывает при любых изменениях значений параметров, указанных в массиве. Каждый параметр задаётся в виде "имя устройства/имя параметра":

// Правило выводит в лог состояние переключателей Devices → Discrete I/O → A1_OUT и Devices → Discrete I/O → A2_OUT
defineRule("test_whenChanged", {
  whenChanged: ["wb-gpio/A1_OUT", "wb-gpio/A2_OUT"], // топики, при изменении которых сработает правило
  then: function (newValue, devName, cellName) {
    log("devName:{}, cellName:{}, newValue:{}", devName, cellName, newValue); // вывод сообщения в лог
  }
});

Если нужно отслеживать один топик, то вместо массива можно просто задать строку "имя устройства/имя параметра":

// Правило выводит в лог состояние переключателя Devices → Discrete I/O → A1_OUT
defineRule("test_whenChanged", {
  whenChanged: "wb-gpio/A1_OUT", // топик, при изменении которого сработает правило
  then: function (newValue, devName, cellName) {
    log("devName:{}, cellName:{}, newValue:{}", devName, cellName, newValue); // вывод сообщения в лог
  }
});

В функцию, заданную в значении ключа then, передаются в качестве аргументов текущее значение параметра newValue, имя устройства devName и имя параметра cellName, изменение которого привело к срабатыванию правила.

В случае, если правило сработало из-за изменения функции, фигурирующей в whenChanged, в качестве единственного аргумента в then передаётся текущее значение этой функции.

Если срабатывание правила не связано непосредственно с изменением параметра (например, вызов при инициализации, по таймеру или через runRule()), then вызывается без аргументов, т.е. значением всех трёх аргументов будет undefined.

whenChanged — правила вызываются также и при первом просмотре правил, если фигурирующие непосредственно в списке или внутри вызываемых функций параметры определены среди retained-значений (см. подробности ниже в разделе Просмотр и выполнение правил).

whenChanged также следует использовать для параметров типа pushbutton — правила, в списке whenChanged которых фигурируют pushbutton-параметры, срабатывают каждый раз при нажатии на кнопку в пользовательском интерфейсе. При использовании whenChanged для кнопок не даётся никаких гарантий по поводу значения newValue, передаваемого в then.

asSoonAs. Правила, задаваемые при помощи asSoonAs, называются edge-triggered и срабатывают в случае, если значение функции, заданной в asSoonAs было ложным и стало истинным. Важно понимать, что функция, заданная в asSoonAs будет выполняться при каждом просмотре правил, поэтому перегружать её не надо.

// Правило сработает, когда переключатель Devices → Discrete I/O → A1_OUT будет включён и выключит его
defineRule({
  asSoonAs: function() {
    return dev["wb-gpio/A1_OUT"]; // правило сработает, когда значение параметра изменится на истинное
  },
  then: function (newValue, devName, cellName) {
    log("Переключатель {} включён! Выключаем…", cellName);
    dev["wb-gpio/A1_OUT"] = !newValue;
    log("Выключили.");
  }
});

when. Правила, задаваемые при помощи when, называются level-triggered, и срабатывают при каждом просмотре, при котором функция, заданная в when, возвращает истинное значение. При срабатывании правила выполняется функция, заданная в свойстве then.

// Правило сработает, когда переключатель Devices → Discrete I/O → A1_OUT будет включён
defineRule({
  when: function() {
    return dev["wb-gpio/A1_OUT"];
  },
  then: function (newValue, devName, cellName) {
    log("devName:{}, cellName:{}, newValue:{}", devName, cellName, newValue);
  }
});

cron-правила — это отдельный тип правил, которые задаются так:

defineRule("crontest_hourly", {
  when: cron("@hourly"),
  then: function () {
    log("@hourly rule fired");
  }
});

Вместо @hourly здесь можно задать любое выражение, допустимое в стандартном crontab, например, 00 00 20 * * (секунды минуты часы, выполнять правило каждый день в 20:00). Помимо стандартных выражений допускается использование ряда расширений, см. описание формата выражений используемой cron-библиотеки.

Объект dev

Объект 'dev' определяет MQTT-топик в правилах wb-rules.

Синтаксис:

dev["device/control"]

где, device — имя устройства в MQTT-топике, control — название контрола.

Параметры device и control содерджатся в полном адресе топика, который имеет вид /devices/device/controls/control.

Альтернативный синтаксис:

dev["device"]["control"] или, что то же самое, dev.device.control.

Значение параметра зависит от его типа: switch, alarm — булевский тип, text — строковый, остальные известные типы параметров, кроме уставок диммеров (тип rgb), считаются числовыми, уставки диммеров (тип rgb) и неизвестные типы параметров — строковыми. Полный список типов параметров в Wiren Board MQTT Conventions.

Не следует использовать объект dev вне кода правил. Не следует присваивать значения параметрам через dev вне then-функций правил и функций обработки таймеров (коллбэки setInterval / setTimeout). В обоих случаях последствия не определены.

Операция присваивания dev[...] = ... в then всегда приводит к публикации MQTT-сообщения, даже если значение параметра не изменилось. В случае виртуальных устройств новое значение публикуется в топике /devices/.../controls/..., и соответствующее значение dev[...] изменяется сразу:

defineVirtualDevice("virtdev", {
  // ...
});

defineRule("someRule", {
  when: ...,
  then: function () {
    dev["virtdev/someparam"] = 42; // публикация 42 -> /devices/virtdev/controls/someparam
    log("v={}", dev["virtdev/someparam"]); // всегда выдаёт v=42
  }
});

В случае внешних устройств новое значение публикуется в топике /devices/.../controls/.../on, а соответствующее значение dev[...] изменится только после получения ответного значения в топике /devices/.../controls/... от драйвера устройства:

defineRule("anotherRule", {
  when: ...,
  then: function () {
    dev["extdev/someparam"] = 42; // публикация 42 -> /devices/extdev/controls/someparam
    log("v={}", dev["extdev/someparam"]); // выдаёт старое значение
  }
});

Виртуальные устройства

С помощью виртуальных устройств можно создать управляющий элемент с внутренним набором функций, например, термостат.

Виртуальное устройство задаётся так:

defineVirtualDevice('my-virtual-device', {
    title: 'MyVirtualDeviceTitle' ,
    cells: {
      ControlName1: {
        title: "Name 1",
        type: "switch",
        value: false
      },
      ControlName2: {
        title: "Name 2",
        type: "range",
        value: 25,
        max: 100,
        min: 1
      },
    }
});

Описание параметров — ECMAScript-объект, ключами которого являются имена параметров, а значениями — описания параметров.

Поля объекта:

  • title — имя, публикуемое в MQTT-топике /devices/.../controls/.../meta/title для данного параметра.
  • type — тип, публикуемый в MQTT-топике /devices/.../controls/.../meta/type для данного параметра. Список доступных типов смотрите в Wiren Board MQTT Conventions.
  • value — значение параметра по умолчанию (топик /devices/.../controls/...).
  • forceDefault — когда задано истинное значение, при запуске контроллера параметр всегда устанавливается в значение по умолчанию. Иначе он будет установлен в последнее сохранённое значение.
  • lazyInit — когда задано истинное значение, при описании контрола в коде фактическое создание его в mqtt происходить не будет до тех пор, пока этому контролу не будет присвоено какое-то значение (например dev[deviceID][controlID] = "string")
  • max для параметра типа range может задавать его максимально допустимое значение.
  • min для параметра типа range может задавать его минимально допустимое значение.
  • readonly — когда задано истинное значение, параметр объявляется read-only (публикуется 1 в /devices/.../controls/.../meta/readonly).

По умолчанию forceDefault == false, т.е. если флаг не задан явно, при запуске параметр примет предыдущее сохранённое значение (если оно существует и lazyInit != true; для новых виртуальных устройств будет записано значение по умолчанию при условии lazyInit != true). Для того, чтобы вернуть старое поведение wb-rules (не использовать сохранённое значение при запуске), задайте явно forceDefault = true.

По умолчанию lazyInit == false, т.е. в этом случае при запуске контрол примет предыдущее сохранённое значение (при условии, что оно существует и forceDefault != true). Если же задать lazyInit = true, то в этом случае хранилище значений не будет использоваться для этого контрола ни для чтения, ни для записи, а сам контрол отобразится в mqtt только после присвоения ему значения в первый раз.

Таймеры

Однократные

setTimeout(callback, milliseconds) запускает однократный таймер, вызывающий при срабатывании функцию, переданную в качестве аргумента callback. Возвращает положительный целочисленный идентификатор таймера, который может быть использован в качестве аргумента функции clearTimeout().

defineVirtualDevice("test_buzzer", {
title: "Test Buzzer",
  cells: {
    enabled: {
      type: "pushbutton",
      value: false
    }
  }
});

defineRule({
  whenChanged: "test_buzzer/enabled",
    then: function (newValue, devName, cellName) {
    dev["buzzer/enabled"] = true;
    setTimeout(function () {
      dev["buzzer/enabled"] = false;
    }, 2000);
  }
});

startTimer(name, milliseconds) запускает однократный таймер с указанным именем.

Таймер становится доступным как timers.<name>. При срабатывании таймера происходит просмотр правил, при этом timers.<name>.firing для этого таймера становится истинным на время этого просмотра.

defineVirtualDevice("test_buzzer", {
  title: "Test Buzzer",
  cells: {
    enabled: {
      type: "switch",
      value: false
    }
  }
});

defineRule("1",{
  asSoonAs: function () {
    return dev["test_buzzer/enabled"];
  },
  then: function () {
    startTimer("one_second", 1000);
    dev["buzzer/enabled"] = true; // включаем пищалку
  }
});

defineRule("2",{
  when: function () {
    return timers.one_second.firing;
  },
  then: function () {
    dev["buzzer/enabled"] = false; // выключаем пищалку
    dev["test_buzzer/enabled"] = false;
  }
});

Периодические

setInterval(callback, milliseconds) запускает периодический таймер, вызывающий при срабатывании функцию, переданную в качестве аргумента callback. Возвращает положительный целочисленный идентификатор таймера, который может быть использован в качестве аргумента функции clearTimeout().

clearTimeout(id) останавливает таймер с указанным идентификатором. Функция clearInterval(id) является alias'ом clearTimeout().

defineVirtualDevice("test_buzzer", {
  title: "Test Buzzer",
  cells: {
    enabled: {
      type: "pushbutton",
      value: false
    }
  }
});

var test_interval = null;

defineRule({
  whenChanged: "test_buzzer/enabled",
  then: function (newValue, devName, cellName) {
    var n = 0;
    if (dev["test_buzzer/enabled"]) {
      test_interval = setInterval(function () {
        dev["buzzer/enabled"] = !dev["buzzer/enabled"];
        n = n + 1;
        if (n >= 10){
          clearInterval(test_interval);
        }
      }, 500);
    }
  }
});

startTicker(name, milliseconds) запускает периодический таймер с указанным интервалом, который также становится доступным как timers.<name>.

Метод stop() таймера (обычного или периодического) приводит к его останову.

Объект timers устроен таким образом, что timers.<name> для любого произвольного <name> всегда возвращает "таймероподобный" объект, т.е. объект с методом stop() и свойством firing. Для неактивных таймеров firing всегда содержит false, а метод stop() ничего не делает.

defineVirtualDevice("test_buzzer", {
  title: "Test Buzzer",
  cells: {
    enabled: {
      type: "switch",
      value: false
    }
  }
});

defineRule("1",{
  asSoonAs: function () {
    return dev["test_buzzer/enabled"];
  },
  then: function () {
    startTicker("one_second", 1000);
  }
});
defineRule("2",{
  when: function () { return timers.one_second.firing; },
  then: function () {
    if (dev["test_buzzer/enabled"] == true) {
      dev["buzzer/enabled"] = !dev["buzzer/enabled"];
    } else {
      timers.one_second.stop();
      dev["buzzer/enabled"] = false;
    }
  }
});

Просмотр и выполнение правил

Здесь мы рассмотрим подробно механизм просмотра и выполнения правил. Рекомендуем внимательно прочитать — это поможет в случае возникновения непонятных ситуаций с несрабатывающими правилами.

Правила просматриваются:

  • при инициализации rule engine после получения всех retained-значений из MQTT;
  • при изменении метаданных устройств (добавлении и переименовании устройств);
  • при изменении любого параметра, доступного в MQTT (/devices/+/controls/+). В данном случае в целях оптимизации правила просматриваются избирательно (см. ниже);
  • при срабатывании таймера, запущенного при помощи startTimer() или startTicker(). В данном случае правила также просматриваются избирательно (см. ниже);
  • при явном вызове runRule() из обработчика таймера, заданного по setTimeout() или setInterval().

Для просмотра правил важным является понятие полного (complete) параметра. Параметр считается полным, когда для него по MQTT получены как значение, так и тип (.../meta/type). В отладочном режиме попытки обращения к неполным параметрам в функциях, фигурирующих в when, asSoonAs и whenChanged приводят к записи в лог сообщения skipping rule due to incomplete cell.

Ниже описаны способы просмотра правил различного типа. Обратите внимание на оптимизацию просмотра правил при получении MQTT-значений и срабатывании таймеров, запущенных через startTimer() или startTicker(). Эта оптимизация может привести к нежелательным результатам, если в условиях правила фигурируют изменяемые пользовательские глобальные переменные, т.к. факт доступа к этим переменным не фиксируется и их изменение может не повлечь за собой просмотр правила при последующих срабатываниях таймера или получении MQTT-значений. Поэтому вместо изменяемых пользовательских глобальных переменных в условиях правил рекомендуем использовать параметры виртуальных устройств.

Срабатывание правила означает вызов then-функции этого правила.

when (level-triggered). Просмотр level-triggered правил (when) осуществляется следующим образом: вызывается функция, заданная в when. Если функция обращается хотя бы к одному неполному параметру, правило не выполняется. Если функция не обращалась к неполным параметрам и вернула истинное значение, правило выполняется.

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

Аналогичным образом фиксируется доступ к объекту timers - при срабатывании таймеров, запущенных через startTimer() или startTicker(), правило просматривается только в том случае, если его when-функция хотя бы раз обращалась к данному конкретному правилу.

asSoonAs (edge-triggered). Просмотр edge-triggered правил (asSoonAs) осуществляется следующим образом: вызывается функция, заданная в asSoonAs. Если функция обращается хотя бы к одному неполному параметру, правило не выполняется. Если функция не обращалась к неполным параметрам и вернула истинное значение, и при этом правило просматривается первый раз, либо при предыдущем просмотре значение функции было ложным, правило выполняется.

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

Аналогичным образом фиксируется доступ к объекту timers - при срабатывании таймеров, запущенных через startTimer() или startTicker(), правило просматривается только в том случае, если его asSoonAs-функция хотя бы раз обращалась к данному конкретному правилу.

whenChanged. Просмотр правил, срабатывающих на изменение значения (whenChanged) происходит следующим образом. При просмотре во время инициализации правило срабатывает, если хотя бы один из параметров, непосредственно перечисленных в whenChanged, является полным, либо если хотя бы одна из функций, перечисленных в whenChanged, при вызове не обращается к неполным параметрам.

При получении MQTT-значений параметров правило срабатывает, в случае, если выполнено хотя бы одно из следующих условий:

  • после прихода сообщения соответствующий параметр является полным, изменил своё значение с момента прошлого просмотра и непосредственно упомянут в whenChanged;
  • после прихода сообщения соответствующий параметр является полным, имеет тип pushbutton и непосредственно упомянут в whenChanged;
  • хотя бы одна из функций, фигурирующих в whenChanged, не обращается к неполным параметрам и возвращает значение, отличное от того, которое она вернула при предшествующем просмотре.

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

При срабатывании таймеров, запущенных через startTimer() или startTicker(), whenChanged-правила не просматриваются.

Cron-правила обрабатываются отдельно от остальных правил при наступлении времени, удовлетворяющего заданному в определении правила cron-выражению.

Важно! Чтобы избежать труднопредсказуемое поведение в функциях, фигурирующих в when, asSoonAs и whenChanged не рекомендуем использовать side effects, т.е. менять состояние программы (изменять значение глобальных переменных, значений параметров, запускать таймеры и т.д.) Важно понимать, что система не даёт никаких гарантий по тому, сколько раз будут вызываться эти функции при просмотрах правил.

Управление правилами

В wb-rules 2.0 появилась возможность управлять выполнением правил. Теперь функция defineRule() возвращает идентификатор созданного правила (аналогично setTimeout()/setInterval()), который можно использовать позже для выключения/включения отработки правила или принудительного запуска тела правила.

По умолчанию, все правила включены.

var myRule = defineRule({
  whenChanged: "mydev/test",
  then: function() {
    log("mydev/test changed");
  }
});

// ...

disableRule(myRule); // отключить проверку и выполнение правила
enableRule(myRule); // разрешить выполнение правила

runRule(myRule); // принудительно запустить тело правила (функцию then)
// на текущий момент не поддерживается передача аргументов в then

Пример скрипта

Пример файла с правилами (sample1.js):

// Определяем виртуальное устройство relayClicker
// с параметром enabled типа switch. MQTT-topic параметра —
// /devices/relayClicker/controls/enabled
defineVirtualDevice("relayClicker", {
  title: "Relay Clicker", // Название устройства /devices/relayClicker/meta/name
  cells: {
    // параметры
    enabled: { // /devices/relayClicker/controls/enabled
      type: "switch",  // тип (.../meta/type)
      value: false     // значение по умолчанию
    }
  }
});

// правило с именем startClicking
defineRule("startClicking", {
  asSoonAs: function () {
    // edge-triggered-правило - выполняется, только когда значение
    // данной функции меняется и при этом становится истинным
    return dev["relayClicker/enabled"] && (dev["uchm121rx/Input 0"] == "0");
  },
  then: function () {
    // выполняется при срабатывании правила
    startTicker("clickTimer", 1000);
  }
});

defineRule("stopClicking", {
  asSoonAs: function () {
    return !dev["relayClicker/enabled"] || dev["uchm121rx/Input 0"] != "0";
  },
  then: function () {
    timers.clickTimer.stop();
  }
});

defineRule("doClick", {
  when: function () {
    // level-triggered правило - срабатывает каждый раз при
    // просмотре данного правила, когда timers.clickTimer.firing
    // истинно. Такое происходит при просмотре правила
    // вследствие срабатывании таймера timers.clickTimer.firing
    return timers.clickTimer.firing;
  },
  then: function () {
    // отправляем значение в /devices/uchm121rx/controls/Relay 0/on
    dev["uchm121rx/Relay 0"] = !dev["uchm121rx/Relay 0"];
  }
});

defineRule("echo", {
  // Срабатывание при изменения значения параметра.
  // Вызывается также при первоначальном просмотре
  // правил, если /devices/wb-w1/controls/00042d40ffff
  // и /devices/wb-w1/controls/00042d40ffff/meta/type
  // были среди retained-значений
  whenChanged: "wb-w1/00042d40ffff",
  then: function (newValue, devName, cellName) {
    // Запуск shell-команды
    runShellCommand("echo " + devName + "/" + cellName + "=" + newValue, {
      captureOutput: true,
      exitCallback: function (exitCode, capturedOutput) {
        log("cmd output: " + capturedOutput);
      }
    });
  }
});

// при необходимости можно определять глобальные функции
function cellSpec(devName, cellName) {
  // используем форматирование строк
  return devName === undefined ? "(no cell)" : "{}/{}".format(devName, cellName);
}

// пример правила, срабатывающего по изменению значений функции
defineRule("funcValueChange2", {
  whenChanged: [
    // Правило срабатывает, когда изменяется значение
    // /devices/somedev/controls/cellforfunc1 или
    // меняется значение выражения dev["somedev/cellforfunc2"] > 3.
    // Также оно срабатывает при первоначальном просмотре
    // правил если хотя бы один из используемых в
    // whenChanged параметров находится среди retained-значений.
    "somedev/cellforfunc1",
    function () {
      return dev["somedev/cellforfunc2"] > 3;
    }
  ],
  then: function (newValue, devName, cellName) {
    // при использовании whenChanged в then-функцию
    // передаётся newValue - значение изменившегося
    // параметра или функции, упомянутой в whenChanged.
    // В случае, когда правило срабатывает
    log("funcValueChange2: {}: {} ({})", cellSpec(devName, cellName),
        newValue, typeof(newValue));
  }
});

Доступ к топикам meta

Предусмотрен доступ к топкам /devices/.../controls/.../meta/... как внешних устройств (только чтение), так и локально определённых виртуальных (чтение и запись).

Получить значение meta-топика: dev["wb-mr3_48/K1#error"] или dev["wb-mr3_48/K1#readonly"]

Установить значение meta-топика виртуального устройства: dev["virDev1/cell1#error"] = "some error" или dev["virDev1/cell2#max"] = 255

Значения meta-топиков можно использовать в правилах как триггеры. Например, можно отслеживать когда теряется связь с устройством и каким-либо образом на это реагировать:

// отправим смс каждый раз, когда первое реле на модуле WB-MR3 станет недоступно
defineRule("onRelayLost", {
  asSoonAs: function () { // также возможно использовать параметр when
    return (dev["wb-mr3_48/K1#error"]);
  },
  then: function () {
    log("ERROR: " + dev["wb-mr3_48/K1#error"]);
    Notify.sendSMS(...);
  }
});

Для отслеживания изменения значений также доступен триггер whenChanged:

// отправим смс как при потере так и восстановлении связи с реле
defineRule("onChange", {
  whenChanged: "wb-mr3_48/K1#error",
  then: function (newValue, devName, cellName) {
    if(newValue !== "") {
      Notify.sendSMS("...", "relay is broken");
    } else {
      Notify.sendSMS("...", "relay is OK");
    }
  }
});

API создания/управления устройств

Функция defineVirtualDevice() возвращает объект, представляющей собой виртуальное устройство. Также этот объект можно получить с помощью глобальной функции getDevice(<id девайса>). Аналогично, можно получить объект контрола при помощи глобальной функции getControl(<id девайса>/<id контрола>), т.е. для получения контрола ctrlID на девайсе deviceID нужно вызвать getControl("deviceID/ctrlID").

К девайсу можно добавлять котролы динамически при помощи метода addControl(<id контрола>, {описание параметров}), удалять — removeControl(<id контрола>).

Для проверки контрола на существование можно воспользоваться функцией isControlExists(<id контрола>). Так как при попытке установить значения контролов не виртуальных (внешних) девайсов возникает исключение — для проверки на принадлежность девайса можно использовать метод isVirtual().

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

getDevice("deviceID").controlsList().forEach(function(ctrl) {
  ...
});

Полный список методов объекта девайса:

  • getId() => string
  • getDeviceId() => string - deprecated, используйте getId()
  • getCellId(string) => string
  • addControl(string, {описание параметров})
  • removeControl(string)
  • getControl(string) => __wbVdevCellPrototype
  • isControlExists(string) => boolean
  • controlsList() => []__wbVdevCellPrototype
  • isVirtual() => boolean

Контролам можно устанавливать значения мета-полей при помощи сеттеров. Например, установить description можно при помощи метода setDescription(string), unitssetUnits(string) и т.д. Аналогично можно и получать значения этих полей геттерами, например, для descriptiongetDescription()

Полный список методов объекта контрола смотрите ниже.

Setters:

  • setTitle(string) или setTitle({ en: string, ru: string })
  • setEnumTitles(object) (параметр вида {'val1': {'en': 'Title1', 'ru': 'Заголовок1'}, ...})
  • setDescription(string)
  • setType(string)
  • setUnits(string)
  • setReadonly(string)
  • setMax(string)
  • setMin(string)
  • setError(string)
  • setOrder(string)
  • setValue(any) или setValue({ value: any, notify: bool })

Getters:

  • getId() => string
  • getTitle(string?) => string (опциональный параметр - язык заголовка, "en" по умолчанию)
  • getDescription() => string
  • getType() => string
  • getUnits() => string
  • getReadonly() => boolean
  • getMax() => number
  • getMin() => number
  • getError() => string
  • getOrder() => number
  • getValue() => any

Встроенные функции и переменные

global

global - глобальный объект ECMAScript (в браузерном JavaScript глобальный объект доступен, как window)

readConfig()

readConfig(path) считывает конфигурационный файл в формате JSON, находящийся по указанному пути. Генерирует исключение, если файл не найден, не может быть прочитан или разобран. Ожидается что корневой элемент JSON'а является объектом. Поддерживаются однострочные // и многострочные /* ... */ комментарии. Для чтения массива, вместо:

$ cat rules.js
var conf = readConfig("test.conf");
$ cat test.conf
[
  ...
]

Используйте, например:

$ cat rules.js
var conf = readConfig("test.conf").config;
$ cat test.conf
{
  "config": [
    ...
  ]
}

Алиасы defineAlias()

defineAlias(name, "device/param") задаёт альтернативное имя для параметра. Например, после выполнения defineAlias("heaterRelayOn", "Relays/Relay 1"); выражение heaterRelayOn = true означает то же самое, что dev["Relays/Relay 1"] = true.

Форматирование строки format()

"...".format(arg1, arg2, ...) осуществляет последовательную замену подстрок {} в указанной строке на строковые представления своих аргументов и возвращает результирующую строку. Например, "a={} b={}".format("q", 42) даёт "a=q b=42". Для включения символа { в строку формата следует использовать {{: "a={} {{}".format("q") даёт "a=q {}". Если в списке аргументов format() присутствуют лишние аргументы, они добавляются в конец строки через пробел: "abc {}:".format(1, 42) даёт "abc 1: 42".

Форматирование строки xformat()

"...".xformat(arg1, arg2, ...) осуществляет последовательную замену подстрок {} в указанной строке на строковые представления своих аргументов и возвращает результирующую строку. Например, "a={} b={}".xformat("q", 42) даёт "a=q b=42". Для включения символа { в строку формата следует использовать \{ (\\{ внутри строковой константы ECMAScript): "a={} \\{}".xformat("q") даёт "a=q {}" (важно! в format(), в отличие от xformat(), для escape используется две фигурные скобки). Кроме того, xformat() позволяет включать в текст результат выполнения произвольных ECMAScript-выражений: "Some value: {{dev["abc/def"]}}". В этой связи xformat() следует использовать с осторожностью в тех случаях, когда непривелегированный пользователь может влиять на содержимое строки формата.

Вывод сообщений в лог log.

log.{debug,info,warning,error}(fmt, [arg1 [, ...]]) выводит сообщение в лог. В зависимости от функции сообщение классифицируется:

  • debug — отладочное, выводится только при включённой отладке;
  • info — информационное;
  • warning — предупреждение;
  • error — ошибка.

Сообщения можно посмотреть через journalctl:

journalctl -u wb-rules -f

Используется форматированный вывод, как в случае "...".format(...), при этом аргумент fmt выступает в качестве строки формата, т.е. log.info("a={}", 42) выводит в лог строку a=42.

Помимо syslog, сообщение дублируется в зависимости от функции в виде MQTT-сообщения в топике /wbrules/log/debug, /wbrules/log/info, /wbrules/log/warning, /wbrules/log/error. debug-сообщения отправляются в MQTT только в том случае, если включён вывод отладочных сообщений установкой в 1 параметра /devices/wbrules/controls/Rule debugging.

Указанные log-топики используются пользовательским интерфейсом для консоли сообщений.

Для сообщений типа log и debug доступны сокращения:

log(fmt, [arg1 [, ...]]) // сокращение для log.info(...)
debug(fmt, [arg1 [, ...]]) // сокращение для log.debug(...)

Подписка на MQTT-топики trackMqtt()

Если вам нужно следить за изменением произвольных MQTT-топиков, используйте trackMqtt();

trackMqtt(topic, callback()) подписывается на MQTT с указанным topic'ом, допустимы символы # и + значения передаются в функцию объектом message состоящим из: .topic — путь к топику, значение которого изменилось и .value — новое значение топика:

trackMqtt("/devices/wb-adc/controls/Vin", function(message) {
  log.info("name: {}, value: {}".format(message.topic, message.value));
});

Публикация сообщений в MQTT publish()

publish(topic, payload, [QoS [, retain]]) публикует MQTT-сообщение с указанными topic'ом, содержимым, QoS и значением флага retained.

Важно: не используйте publish() для изменения значения параметров устройств. Для этих целей есть объект dev о котором рассказано выше.

Пример:

// Публикация non-retained сообщения с содержимым "0" (без кавычек)
// в топике /abc/def/ghi с QoS = 0
publish("/abc/def/ghi", "0");
// То же самое с явным заданием QoS
publish("/abc/def/ghi", "0", 0);
// То же самой с QoS=2
publish("/abc/def/ghi", "0", 2);
// То же самое с retained-флагом
publish("/abc/def/ghi", "0", 2, true);

Запуск внешних процессов spawn()

spawn(cmd, args, options) запускает внешний процесс, определяемый cmd. Необязательный параметр options - объект, который может содержать следующие поля:

  • captureOutput - если true, захватить stdout процесса и передать его в виде строки в exitCallback
  • captureErrorOutput - если true, захватить stderr процесса и передать его в виде строки в exitCallback. Если данный параметр не задан, то stderr дочернего процесса направляется в stderr процесса wb-rules
  • input - строка, которую следует использовать в качестве содержимого stdin процесса
  • exitCallback - функция, вызываемая при завершении процесса. Аргументы функции: exitCode - код возврата процесса, capturedOutput - захваченный stdout процесса в виде строки в случае, когда задана опция captureOutput, capturedErrorOutput - захваченный stderr процсса в виде строки в случае, когда задана опция captureErrorOutput

runShellCommand(cmd, options) вызывает /bin/sh с указанной командой следующим образом: spawn("/bin/sh", ["-c", cmd], options).

defineRule({
  asSoonAs: function() {
    return true;
  },
  then: function () {
    runShellCommand("uname -a", {
      captureOutput: true,
      exitCallback: function(exitCode, capturedOutput) {
        log(exitCode);
        if (exitCode === 0) {
          log(capturedOutput);
          return;
        }
      }
    });
  }
});

Модули

Начиная с версии 2.0, в движке правил wb-rules появилась поддержка подключаемых JS-модулей. Поддержка похожа по поведению на аналогичную в Node.js, но с некоторыми особенностями.

Расположение

Поиск модулей происходит по следующим путям (в заданном порядке):

  1. /etc/wb-rules-modules — сюда можно складывать пользовательские модули, они сохранятся при обновлении контролера.
  2. /usr/share/wb-rules-modules — папка с системными модулями.

Таким образом, пользовательские модули удобно складывать в /etc/wb-rules-modules.

Добавить свои пути можно редактированием /etc/default/wb-rules добавлением путей к переменной WB_RULES_MODULES через разделитель ::

...
WB_RULES_MODULES="/etc/wb-rules-modules:/usr/share/wb-rules-modules"
...

Создание модуля

Для создания модуля достаточно создать файл с именем, соответствующим имени модуля (с расширением .js) в директории /etc/wb-rules-modules.

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

Объект exports

С помощью объекта exports можно передавать пользовательскому сценарию параметры и методы.

Файл модуля:

exports.hello = function(text) {
  log("Hello from module, {}", text);
};

exports.answer = 42;

Файл сценария, в который подключается модуль:

var m = require("myModule");
m.hello("world"); // выведет в лог "Hello from module, world"
log("The answer is {}", m.answer); // выведет в лог "The answer is 42"

Важно! Объект exports можно только дополнять значениями, но не переопределять:

exports = function(text) {
  log("Hello from module, {}", text);
};

// Ожидание:
var m = require("my-module");
m("world"); // не работает

// На практике m будет пустым объектом.
// Та же проблема произойдёт при использовании такой конструкции:
exports = {
  hello: function(text) {
    log("Hello from module, {}", world);
  },
  answer: 42
};

Объект module

Объект module содержит параметры, относящиеся непосредственно к файлу модуля.

module.filename содержит полный путь до файла модуля. Например, для модуля, сохранённого в /etc/wb-rules-modules/myModule.js:

log(module.filename); // выведет /etc/wb-rules-modules/myModule.js

module.static хранит данные, общие для всех экземпляров данного модуля. Его следует использовать для тех данных, которые должны быть доступны сразу во всех сценариях, использующих данный модуль. Смотрите примеры ниже.

Файл модуля /etc/wb-rules-modules/myModule.js:

exports.counter = function() {
  if (module.static.count === undefined) {
    module.static.count = 1;
  }
  log("Number of calls: {}", module.static.count);
  module.static.count++;
};

Файл сценария scenario1.js:

var m = require("myModule");
m.counter();
m.counter();

Файл сценария scenario2.js:

var m = require("myModule");
m.counter();
m.counter();
m.counter();

В результате работы двух скриптов в логе окажется 5 сообщений:

Number of calls: 1
Number of calls: 2
Number of calls: 3
Number of calls: 4
Number of calls: 5

__filename

Переменная __filename берётся из глобального объекта сценария, к которому подключается модуль, и содержит имя файла сценария.

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

Файл /etc/wb-rules-modules/myModule.js:

exports.hello = function() {
  log(__filename);
};

Файл сценария /etc/wb-rules/scenario1.js:

var m = require("myModule");
m.hello(); // выведет scenario1.js

Подключение модуля к сценарию

Подключение модуля происходит с помощью функции require(). Она возвращает объект, экспортированный модулем (exports).

...
var myModule = require("myModule");
...

При этом движок правил будет искать файл myModule.js по очереди в директориях поиска (см. Расположение).

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

...
var myModule = require("path/to/myModule");
...

После того, как файл будет найден, его содержимое будет выполнено, и из файла будет передан объект exports.

Особенности:

  • Если модуль был подключен в одном сценарии несколько раз (несколько вызовов require("myModule")), содержимое файла модуля будет выполнено только в первый раз, а при повторных вызовах будет возвращаться сохранённый объект exports.
  • Если модуль подключается в разных сценариях, для каждого сценария будет создан свой объект модуля и заново выполнен весь код модуля. Если модулю требуется использовать данные, общие для всех файлов сценариев, для хранения данных следует использовать объект module.static.

Сервис оповещений

С помощью сервиса оповещений можно отправлять сообщение на электронную почту или через SMS.

Notify.sendEmail(to, subject, text) отправляет почту указанному адресату (to), с указанной темой (subject) и содержимым (text).

Notify.sendSMS(to, text, command) отправляет SMS на указанный номер (to) с указанным содержимым (text), используя команду (command) (необязательный аргумент).

Для отправки SMS используется ModemManager, а если он не установлен, то gammu.

Сервис алармов

Основная функция:

Alarms.load(spec) - загружает блок алармов. spec может задавать либо непосредственно блок алармов в виде JavaScript-объекта, либо указывать путь к JSON-файлу, содержащему описание алармов.

Каждому блоку алармов соответсвует виртуальное устройство, содержащее по контролу на каждый аларм, отражающему состояние аларма: 0 = не активен, 1 = активен. Также в устройстве присутствует дополнительный контрол log, используемый для логирования работы службы алармов.

Загружаемый по умолчанию блок алармов находится в файле /etc/wb-rules/alarms.conf. Этот файл доступен для редактирования через веб-редактор конфигов.

Пример блока алармов с описанием:

{
  // Название MQTT-устройства блока алармов
  "deviceName": "sampleAlarms",

  // Отображаемое название устройства блока алармов
  "deviceTitle": "Sample Alarms",

  // Описание получателей
  "recipients": [
    {
      // Тип получателя - e-mail
      "type": "email",

      // E-mail адрес получателя
      "to": "someone@example.com",

      // Тема письма (необязательное поле)
      "subject": "alarm!"
    },
    {
      // Ещё один e-mail-получатель
      "type": "email",

      // E-mail адрес получателя
      "to": "anotherone@example.com",

      // Тема письма. {} заменяется на текст сообщения
      "subject": "Alarm: {}"
    },
    {
      // Тип получателя - SMS
      "type": "sms",

      // Номер телефона получателя
      "to": "+78122128506",

      // Команда для отправки SMS. Поле можно оставить пустым, чтобы использовать
      // gammu. В команде нужно указать как минимум один плейсхолдер {} - для номера. Тогда
      // текст будет отправлен в stdin. Если указать 2 плейсхолдера - то в первый запишется
      // номер, во второй - текст.
      // Примеры:
      // /path/to/sender.py --number {}
      // /path/to/sender.py --number {} --text "{}"
      "command": ""
    }
  ],

  // Описание алармов
  "alarms": [
    {
      // Название аларма
      "name": "importantDeviceIsOff",

      // Наблюдаемые устройство и контрол
      "cell": "somedev/importantDevicePower",

      // Ожидаемое значение. Аларм срабатывает, если значение контрола становится
      // отличным от expectedValue. Когда значение снова становится равным
      // expectedValue, аларм деактивируется.
      "expectedValue": 1,

      // Сообщение, отправляемое при срабатываении аларма.
      // Если сообщение не указано, оно генерируется автоматически на основе
      // текущего значения контрола.
      "alarmMessage": "Important device is off",

      // Сообщение, отправляемое при деактивации аларма.
      // Если сообщение не указано, оно генерируется автоматически на основе
      // текущего значения контрола.
      "noAlarmMessage": "Important device is back on",

      // Интервал (в секундах) отправки сообщений во время активности аларма.
      // Если это поле не указано, то сообщения отправляются только
      // при активации и деактивации аларма.
      "interval": 200,

      // Задержка срабатывания аларма.
      // Если поле присутствует, то аларм сработает только когда условие срабатывания
      // будет непрерывно выполнятся в течение заданного интервала (в миллисекундах).
      "alarmDelayMs" : 10000,

      // Задержка сброса аларма.
      // Если поле присутствует, то аларм сбросится только когда условие срабатывания
      // не будет непрерывно выполнятся в течение заданного интервала (в миллисекундах).
      "noAlarmDelayMs" : 3000
    },
    {
      // Название аларма
      "name": "temperatureOutOfBounds",

      // Наблюдаемые устройство и контрол
      "cell": "somedev/devTemp",

      // Вместо expectedValue можно указать minValue, maxValue либо и minValue, и maxValue.
      // Если значение наблюдаемого контрола становится меньше minValue или больше maxValue,
      // происходит срабатывание аларма. Когда значение возвращается в указанный диапазон,
      // аларм деактивируется.
      "minValue": 10,
      "maxValue": 15,

      // Сообщение, отправляемое при срабатываении аларма. {} Заменяется
      // на текущее значение контрола. Возможно использование {{ expr }}
      // для вычисления произвольного JS-выражения (см. "...".xformat(...)).
      "alarmMessage": "Temperature out of bounds, value = {{dev['somedev']['devTemp']}}",

      // Сообщение, отправляемое при деактивации аларма. {} Заменяется
      // на текущее значение контрола. Возможно использование {{ expr }}
      // для вычисления произвольного JS-выражения (см. "...".xformat(...)).
      "noAlarmMessage": "Temperature is within bounds again, value = {}",

      // Интервал (в секундах) отправки сообщений во время активности аларма.
      "interval": 10,

      // Максимальное количество отправляемых сообщений.
      // За каждый период активности аларма отправляется не больше
      // указанного количества сообщений.
      "maxCount": 5
    }
  ]
}

Постоянное хранилище

В wb-rules есть возможность использовать постоянное хранилище. Переменные в постоянном хранилище сохраняются на flash, таким образом, остаются доступными после перезагрузки контроллера.

Пример использования постоянного хранилища:

defineRule("myRule", {
  ...
  then: function() {
    // здесь "my-storage" - имя хранилища
    var ps = new PersistentStorage("my-storage", {global: true});

    // в постоянное хранилище можно записывать значения любого типа
    ps["var1"] = 42;
    ps["var2"] = "foo";
    ps["var3"] = { name: "Temperature", value: 26.3 };

    // чтение из хранилища
    log("Value of var1: " + ps["var1"]);
  }
}

Можно создавать несколько постоянных хранилищ с разными именами; каждое из них будет иметь свой набор значений.

...
var ps1 = new PersistentStorage("storage1", {global: true});
var ps2 = new PersistentStorage("storage2", {global: true});

ps1["key"] = 42;

ps2["key"] = 84;
ps2["foo"] = "bar";

log(ps1["key"]); // выведет 42
log(ps1["foo"]); // undefined

log(ps2["key"]); // выведет 84
log(ps2["foo"]); // выведет bar
...

Примечание: второй аргумент { global: true } означает, что хранилище является глобальным для всех правил. Это значит, что если создать хранилища с одинаковыми именами в разных файлах правил, они получат доступ к одному и тому же хранилищу. Порядок доступа при этом не определён.

rules1.js:

...
var ps = new PersistentStorage("global-storage", {global: true});
ps["foo"] = "bar";
...

rules2.js:

...
var ps = new PersistentStorage("global-storage", {global: true});

// выведет bar после того, как это значение будет записано в rules1.js
log(ps["foo"]);

Поддержка локальных хранилищ (для избежания нежелательных конфликтов имён хранилищ между файлами) должна появиться в будущих версиях wb-rules. Пока что обязательно нужно указывать аргумент { global: true }.

Изоляция сценариев

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

В качестве примера приведём два сценария, одновременно запускаемых в движке правил. Каждый сценарий определяет глобальные переменные и функции.

Поведение wb-rules при обращении к глобальной переменной, изменяемой в нескольких файлах сценариев строго определено и такое же, как будто сценарий единственный в системе.

Пример правила для вывода сообщения в log.

Сценарий 1 (rules1.js):

test1 = 42;

setTimeout(function myFuncOne() {
  log("myFuncOne called");
  log("test1: {}, test2: {}", test1, test2);
  test1: 42, test2: (undefined)
  // (будет выведена ошибка выполнения: ReferenceError: identifier 'test2' undefined)
}, 1000);

Сценарий 2 (rules2.js):

test1 = 84;
test2 = "Hello";

setTimeout(function myFuncTwo() {
  log("myFuncTwo called");
  log("test1: {}, test2: {}", test1, test2);
  // раньше: test1: [либо 42, либо 84], test2: Hello
}, 1000);

В версии 1.7 для изоляции правил рекомендовалось использовать рекомендовалось использовать замыкание, т.е. оборачивание кода сценария в конструкцию:

(function() {
  // код сценария идёт здесь
})();

Начиная с версии 2.0, в подобной конструкции нет необходимости. Тем не менее, старые сценарии, использующие эту конструкцию, продолжат работу без изменений в поведении.

Обходные пути

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

Использование модулей. Можно написать модуль для организации взаимодействия. У модулей есть статическое хранилище, общее для всех файлов, импортировавших модуль. (см. module.static)

Постоянное хранилище. Для обмена данными также можно использовать глобальные постоянные хранилища (PersistentStorage):

var ps = new PersistentStorage("my-global-storage", {global: true});

/// ...

ps.myvar = "value"; // это значение доступно для всех пользователей хранилища с именем "my-global-storage"

Имейте ввиду, что при использовании глобальных постоянных хранилищ может произойти совпадение имён, в этом случае возможно нарушение поведения, которое трудно обнаружить.

Прототип глобального объекта. Метод считается «грязным», т.к. все переменные и функции, опубликованные таким образом, становятся доступными всем сценариям в системе. Старайтесь избегать этого способа. За неопределённое поведение при использовании этого метода несёт ответственность сам программист.

Глобальные объекты всех сценариев имеют общий объект — прототип, в котором определены стандартные функции wb-rules (такие, как defineRule, setTimeout и т.д.). Через него можно передавать переменные или функции в общую область видимости.

global.__proto__.myVar = 42; // теперь myVar — общая переменная для всех сценариев

// из других сценариев к переменной можно обращаться так
log("shared myVar: {}", myVar);

// или вот так, что чуть более аккуратно, т.к. однозначно показывает, где определена переменная
log("shared myVar: {}", global.__proto__.myVar);

Правило поиска переменной в первом случае будет выглядеть так:

  1. Проверяем, есть ли myVar среди локальных переменных (определённой как var myVar = ...).
  2. Если нет, проверяем, есть ли myVar в глобальном объекте (определённой как myVar = ...).
  3. Если нет, проверяем, есть ли myVar в прототипе глобального объекта (определённой как global.__proto__.myVar).

Поиск останавливается, как только переменная найдена.

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

Автоматическая перезагрузка сценариев

При внесении изменений в файлы с правилами происходит автоматическая перезагрузка изменённых файлов. При перезагрузке глобальное состояние ECMAScript-движка сохраняется, т.е., например, если глобальная переменная определена в файле a.js, то при изменении файла b.js её значение не изменится. Глобальные переменные и функции, определения которых удалены из правил, также не удаляются до перезагрузки движка правил (service wb-rules restart). В то же время удаление определений правил и виртуальных устройств отслеживается и обрабатываются, т.е. если, например, удалить правило из .js-файла, то это правило более срабатывать не будет.

Управление логированием

Для включения отладочного режима задать порт и опцию -debug в /etc/default/wb-rules:

WB_RULES_OPTIONS="-debug"

Ещё отладку можно включить в веб-интерфейсе контроллера:

  • переключатель Devices → Rule Engine Settings → Rule debugging;
  • флажок Enable debug в консоли отладки правил.

Сообщения об ошибках записываются в syslog.

Установка

wb-rules уже установлен контроллеры Wiren Board, но если у вас его не оказалось, используйте инструкции ниже.

Через apt-get

Пакет wb-rules есть в репозитории, для установки и обновления надо выполнить:

apt-get update
apt-get install wb-rules

Правила находятся в каталоге /etc/wb-rules/

Сборка из исходников

Сборка из исходников может понадобиться, если вы хотите внести изменения в wb-rules.

Работу с исходными текстами необходимо производить внутри wbdev workspace (создаётся командой wbdev update-workspace).

Сборка исполняемого файла для arm:

wbdev hmake clean && wbdev hmake

Сборка исполняемого файла для x86_64:

wbdev hmake clean && wbdev hmake amd64

После выполнения этих команд в папке проекта появляется исполняемый файл wb-rules.

Сборка пакета для Wiren Board:

wbdev gdeb