Rule engine for Wiren Board, version 2.0
Движок правил для контроллеров Wiren Board, версия 2.0
Содержание
- Правила
- Определение правил
- Типы правил
- Объект dev
- Виртуальные устройства
- Таймеры
- Просмотр и выполнение правил
- Управление правилами
- Пример скрипта
- Доступ к топикам meta
- API создания/управления устройств
- Встроенные функции и переменные
- Модули
- Сервис оповещений
- Сервис алармов
- Постоянное хранилище
- Изоляция сценариев
- Автоматическая перезагрузка сценариев
- Управление логированием
- Установка
- Ограничения
Правила — специальные скрипты, предназначенные для программирования контроллеров 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
также следует использовать для параметров типа 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("Выключили.
10000
");
}
});
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
здесь можно задать любое выражение в формате <second> <minute> <hour> <day of month> <month> [<day of week>]
,
например, 00 00 20 * *
(выполнять правило каждый день в 20:00).
Помимо стандартных выражений допускается использование ряда расширений,
см. описание
формата выражений используемой cron-библиотеки.
Объект '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"]); // выдаёт старое значение
}
});
С помощью виртуальных устройств можно создать управляющий элемент с внутренним набором функций, например, термостат.
Виртуальным устройствам и контролам можно присваивать русские имена, задавая title
в виде title: { en: ’Title’, ru: ’Заголовок’ }
, или через setTitle
у контрола: setTitle({ en: ’Title’, ru: ’Заголовок’ })
.
Для значений параметров с типом value
и text
можно использовать перечисления enum в виде набора именованных констант. Перечисления удобно использовать, когда значение параметра может принимать ограниченное количество значений, например, дни недели.
Чтобы задать перечисление используйте для нужного контрола параметр enum
с набором пар “ключ”: “значение”
. Для значений можно использвоать переводы в формате “ключ”: {en: 'Value', ru: 'Значение'}
.
Если параметр имеет тип value
каждый ключ должен быть строковым числом в десятичном или шестнадцатеричном формате.
Виртуальное устройство задаётся так:
defineVirtualDevice('my-virtual-device', {
title: {en: 'My Virtual Device', ru: 'Мое виртуальное устройство'} ,
cells: {
ControlName1: {
title: "Name 1",
type: "switch",
value: false
},
ControlName2: {
title: "Name 2",
type: "range",
value: 25,
max: 100,
min: 1
},
state: {
title: {en: 'State', ru: 'Состояние'},
type: "value",
value: 1,
enum: {
1: {en: 'Normal', ru: 'В норме'},
2: {en: 'Warning', ru: 'Внимание'},
3: {en: 'Crash', ru: 'Авария'}
}
},
}
});
Описание параметров — 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
для параметра типаvalue
/range
может задавать его максимально допустимое значение.min
для параметра типаvalue
/range
может задавать его минимально допустимое значение.precision
для параметра типаvalue
/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
) происходит следующим образом.
При получении 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));
}
});
Предусмотрен доступ к топкам /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");
}
}
});
Если у контрола отсутствует определенный meta-топик, то при попытке чтения будет возвращено undefined
. При попытке чтения meta-топика несуществующего контрола будет возвращено null
.
Функция 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
setError(string)
getError() => string
Контролам можно устанавливать значения мета-полей при помощи сеттеров.
Например, установить description
можно при помощи метода setDescription(string)
, units
— setUnits(string)
и т.д.
Аналогично можно и получать значения этих полей геттерами, например, для description
— getDescription()
Полный список методов объекта контрола смотрите ниже.
Setters:
setTitle(string)
илиsetTitle({ en: string, ru: string })
setEnumTitles(object)
(параметр вида{'val1': {'en': 'Title1', 'ru': 'Заголовок1'}, ...}
)setDescription(string)
setType(string)
setUnits(string)
setReadonly(string)
setMax(number)
setMin(number)
setPrecision(number)
setError(string)
setOrder(number)
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
getPrecision() => number
getError() => string
getOrder() => number
getValue() => any
global
- глобальный объект ECMAScript (в браузерном JavaScript
глобальный объект доступен, как window)
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(name, "device/param")
задаёт альтернативное имя для параметра.
Например, после выполнения defineAlias("heaterRelayOn", "Relays/Relay 1");
выражение
heaterRelayOn = true
означает то же самое, что dev["Relays/Relay 1"] = true
.
"...".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(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.{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()
;
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));
});
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(cmd, args, options)
запускает внешний процесс, определяемый
cmd
. Необязательный параметр options
- объект, который может
содержать следующие поля:
captureOutput
- еслиtrue
, захватить stdout процесса и передать его в виде строки вexitCallback
captureErrorOutput
- еслиtrue
, захватить stderr процесса и передать его в виде строки вexitCallback
. Если данный параметр не задан, то stderr дочернего процесса направляется в stderr процесса wb-rulesinput
- строка, которую следует использовать в качестве содержимого 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, но с некоторыми особенностями.
Поиск модулей происходит по следующим путям (в заданном порядке):
/etc/wb-rules-modules
— сюда можно складывать пользовательские модули, они сохранятся при обновлении контролера./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.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.filename содержит полный путь до файла модуля. Например, для модуля, сохранённого в /etc/wb-rules-modules/myModule.js
:
log(module.filename); // выведет /etc/wb-rules-modules/myModule.js
module.static хранит данные, общие для всех экземпляров данного модуля. Его следуе F438 т использовать для тех данных, которые должны быть доступны сразу во всех сценариях, использующих данный модуль. Смотрите примеры ниже.
Файл модуля /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, тем не менее, будет содержать именно имя файла сценария — вершины дерева зависимостей.
Файл /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"] = StorableObject({ 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 }
.
При записи null
или undefined
, значение удаляется из хранилища, последущие чтения будут возвращать undefined
.
Каждый файл сценария запускается в своём отдельном пространстве имён — контексте. Таким образом, каждый сценарий может определять свои функции и глобальные переменные без риска изменить поведение других сценариев.
В качестве примера приведём два сценария, одновременно запускаемых в движке правил. Каждый сценарий определяет глобальные переменные и функции.
Поведение 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);
Правило поиска переменной в первом случае будет выглядеть так:
- Проверяем, есть ли myVar среди локальных переменных (определённой как var
myVar = ...
). - Если нет, проверяем, есть ли myVar в глобальном объекте (определённой как
myVar = ...
). - Если нет, проверяем, есть ли myVar в прототипе глобального объекта (определённой как
global.__proto__.myVar
).
Поиск останавливается, как только переменная найдена.
Таким образом, первый способ обращения будет работать только в том случае, если myVar не определена в верхних областях видимости.
При внесении изменений в файлы (или изменение времени модификации файла) с правилами происходит автоматическая
перезагрузка изменённых файлов. При перезагрузке глобальное состояние
ECMAScript-движка сохраняется, т.е., например, если глобальная
переменная определена в файле a.js
, то при изменении файла b.js
её
значение не изменится. Глобальные переменные и функции, определения
которых удалены из правил, также не удаляются до перезагрузки движка
правил (systemctl restart wb-rules
). В то же время удаление
определений правил и виртуальных устройств отслеживается и
обрабатываются, т.е. если, например, удалить правило из .js-файла, то
это правило более срабатывать не будет.
Для включения отладочного режима задать порт и опцию -debug
в /etc/default/wb-rules
:
WB_RULES_OPTIONS="-debug
9309
"
Ещё отладку можно включить в веб-интерфейсе контроллера:
- переключатель Devices → Rule Engine Settings → Rule debugging;
- флажок Enable debug в консоли отладки правил.
Сообщения об ошибках записываются в syslog.
wb-rules уже установлен контроллеры Wiren Board, но если у вас его не оказалось, используйте инструкции ниже.
Пакет wb-rules есть в репозитории, для установки и обновления надо выполнить:
apt-get update
apt-get install wb-rules
Правила находятся в каталоге /etc/wb-rules/
Публикация более 100 топиков в секунду может вызвать повышенное потребление CPU и проблемы с производительностью. Рекомендуется оптимизировать частоту публикации топиков для обеспечения стабильной работы.