Движок правил wb-rules
В контроллере Wiren Board есть движок правил, позволяющий писать как простые правила (например, "Если температура датчика больше 18С, выключи нагреватель"), так и сложные. В статье кратко описывается механизм работы правил и показываются примеры написания. Полная документация доступна на странице https://github.com/contactless/wb-rules.
Как создать правило
Подготовка
Правила имеют вид файлов, которые пишутся на языке Javascript, и находятся на контроллере в папке /etc/wb-rules
.
Правила редактируются в веб-интерфейсе в встроенном редакторе в разделе Rules.
Список типов контролов в интерфейсе: https://github.com/contactless/homeui/blob/contactless/conventions.md
Правила также можно редактировать, напрямую редактируя и создавая файлы в соответствующей папке. Про доступ смотрите в статье Просмотр файлов контроллера с компьютера.
После изменения файла в папке /etc/wb-rules
или создания нового скрипт автоматически начинает работать.
Примеры правил
Слежение за контролом
Это простейшее правило следит за контролом и устанавливает другой контрол в такое же состояние.
Например правило может включать сирену и лампу, если датчик движения заметил движение.
В примере датчик движения подключен к входу "сухой контакт", контрол типа "switch". Сирена подключена к встроеному реле Wiren Board, а лампа - через релейный блок по Modbus. Когда вход типа "сухой контакт" (выход датчика движения) замкнут, то на лампу и реле подаётся "1", когда выключен - "0".
Правило срабатывает каждый раз при изменении значения контрола "D1_IN" у устройства "wb-gpio". В код правила передаётся новое значение этого контрола в виде переменной newValue.
defineRule("motion_detector", {
whenChanged: "wb-gpio/D1_IN",
then: function (newValue, devName, cellName) {
dev["wb-gpio"]["Relay_2"] = newValue;
dev["wb-mrm2_6"]["Relay 1"] = newValue;
}
});
То же самое, но с виртуальным девайсом в качестве источника событий. Пример использования: сценарная кнопка, которая включает/выключает сирену и лампочку.
defineVirtualDevice("simple_test", {
title: "Simple switch",
cells: {
enabled: {
type: "switch",
value: false
},
}
});
defineRule("simple_switch", {
whenChanged: "simple_test/enabled",
then: function (newValue, devName, cellName) {
dev["wb-gpio"]["Relay_2"] = newValue;
dev["wb-mrm2_6"]["Relay 1"] = newValue;
}
});
Детектор движения c таймаутом
На вход D2 подключен детектор движения с выходом типа "сухой контакт", который замыкает D2 и GND при обнаружении движения. При этом, на канале "wb-gpio/D2_IN" появляется статус "1".
Правило включает свет при обнаружении движения и выключает свет, спустя 30 секунд после пропадания сигнала с датчика движения.
Освещение подключено через встроенное реле, канал wb-gpio/Relay_1.
var motion_timer_1_timeout_ms = 30 * 1000;
var motion_timer_1_id = null;
defineRule("motion_detector_1", {
whenChanged: "wb-gpio/D2_IN",
then: function (newValue, devName, cellName) {
if (newValue) {
dev["wb-gpio"]["Relay_1"] = 1;
if (motion_timer_1_id) {
clearTimeout(motion_timer_1_id);
}
motion_timer_1_id = setTimeout(function () {
dev["wb-gpio"]["Relay_1"] = 0;
}, motion_timer_1_timeout_ms);
}
}
});
Активация правила только в определённое время
Правило как в предыдущем разделе, но выполняется только с 9:30 до 17:10 по UTC.
var motion_timer_1_timeout_ms = 5 * 1000;
var motion_timer_1_id = null;
defineRule("motion_detector_1", {
whenChanged: "wb-gpio/A1_IN",
then: function (newValue, devName, cellName) {
var date = new Date();
// time point marking the beginning of the interval
// i.e. "today, at HH:MM". All dates are in UTC!
var date_start = new Date(date);
date_start.setHours(9);
date_start.setMinutes(30);
// time point marking the end of the interval
var date_end = new Date(date);
date_end.setHours(17);
date_end.setMinutes(10);
// if time is between 09:30 and 17:10 UTC
if ((date > date_start) && (date < date_end)) {
if (newValue) {
dev["wb-gpio"]["EXT1_R3A1"] = 1;
if (motion_timer_1_id) {
clearTimeout(motion_timer_1_id);
}
motion_timer_1_id = setTimeout(function () {
dev["wb-gpio"]["EXT1_R3A1"] = 0;
}, motion_timer_1_timeout_ms);
}
}
}
});
Роллеты
Одно реле включает двигатель, поднимающий шторы, второе реле - включает двигатель, опускающий шторы. Правило следит за тем, чтобы оба реле не были включены одновременно.
Кроме этого, правило отключает двигатели спустя заданное время после включения.
(function() { //don't touch this line
var suffix = "1"; // must be different in different JS files
var relay_up_device = "lc103_4";
var relay_up_control = "Relay 1";
var relay_down_device = "lc103_4";
var relay_down_control = "Relay 2";
var timeout_s = 15;
// End of settings
var relay_up_timer_id = null;
var relay_down_timer_id = null;
defineRule( "roller_shutter_up_on" + suffix, {
asSoonAs: function() {
return dev[relay_up_device][relay_up_control];
},
then: function () {
if (relay_up_timer_id) {
relay_up_timer_id = clearTimeout(relay_up_timer_id);
};
relay_up_timer_id = setTimeout(function() {
return dev[relay_up_device][relay_up_control] = 0;
}, timeout_s * 1000);
}
});
defineRule("roller_shutter_down_on" + suffix, {
asSoonAs: function() {
return dev[relay_down_device][relay_down_control];
},
then: function () {
if (relay_down_timer_id) {
relay_down_timer_id = clearTimeout(relay_down_timer_id);
};
relay_down_timer_id = setTimeout(function() {
dev[relay_down_device][relay_down_control] = 0;
}, timeout_s * 1000);
}
});
defineRule("roller_shutter_both_on" + suffix, {
asSoonAs: function() {
return dev[relay_up_device][relay_up_control] && dev[relay_down_device][relay_down_control];
},
then: function () {
if (relay_up_timer_id) {
relay_up_timer_id = clearTimeout(relay_up_timer_id);
};
if (relay_down_timer_id) {
relay_down_timer_id = clearTimeout(relay_down_timer_id);
};
dev[relay_up_device][relay_up_control] = 0;
dev[relay_down_device][relay_down_control] = 0;
log("Both roller shutter relays on, switching them off");
}
});
})();
Более старая версия того же сценария демонстрирует использование alias-ов:
(function() {
defineAlias("relay_up_1", "lc103_4/Relay 1");
defineAlias("relay_down_1", "lc103_4/Relay 2");
var timeout_s = 15;
defineRule("roller_shutter_1_up_on", {
asSoonAs: function() {
return relay_up_1;
},
then: function () {
setTimeout(function() {
relay_up_1 = 0;
}, timeout_s * 1000);
}
});
defineRule("roller_shutter_1_down_on", {
asSoonAs: function() {
return relay_down_1;
},
then: function () {
setTimeout(function() {
relay_down_1 = 0;
}, timeout_s * 1000);
}
});
defineRule("roller_shutter_1_both_on", {
asSoonAs: function() {
return relay_up_1 && relay_down_1;
},
then: function () {
relay_up_1 = 0;
relay_down_1 = 0;
log("Both roller shutter relays on, switching them off");
}
});
})();
Системные правила
Некторые правила поставляются с системой правил по-умолчанию в пакете wb-rules-system.
Полный список правил в репозитории.
Некоторые примеры:
Правило для пищалки
Правило создаёт виртуальное устройство buzzer с ползунками для регулировки громкости и частоты, а также кнопкой включения звука.
defineVirtualDevice("buzzer", {
title: "Buzzer", //
cells: {
frequency : {
type : "range",
value : 3000,
max : 7000,
},
volume : {
type : "range",
value : 10,
max : 100,
},
enabled : {
type : "switch",
value : false,
},
}
});
// setup pwm2
runShellCommand("echo 2 > /sys/class/pwm/pwmchip0/export");
function _buzzer_set_params() {
var period = parseInt(1.0 / dev.buzzer.frequency * 1E9);
var duty_cycle = parseInt(dev.buzzer.volume * 1.0 / 100 * period * 0.5);
runShellCommand("echo " + period + " > /sys/class/pwm/pwmchip0/pwm2/period");
runShellCommand("echo " + duty_cycle + " > /sys/class/pwm/pwmchip0/pwm2/duty_cycle");
};
defineRule("_system_buzzer_params", {
whenChanged: [
"buzzer/frequency",
"buzzer/volume",
],
then: function (newValue, devName, cellName) {
if ( dev.buzzer.enabled) {
_buzzer_set_params();
}
}
});
defineRule("_system_buzzer_onof", {
whenChanged: "buzzer/enabled",
then: function (newValue, devName, cellName) {
if ( dev.buzzer.enabled) {
_buzzer_set_params();
runShellCommand("echo 1 > /sys/class/pwm/pwmchip0/pwm2/enable");
} else {
runShellCommand("echo 0 > /sys/class/pwm/pwmchip0/pwm2/enable");
}
}
});
Правило для статуса питания
Правило создаёт виртуальное устройство, которое сообщает текущий статус питания. В качестве входных данных используется два канала АЦП: измерение напряжения на аккумуляторе и измерение входного напряжения.
Реализована следующая логика:
1. Если входное напряжение меньше напряжение на аккмуляторе, то значит плата питается от аккумулятора. В этом случае, также отображается 0V в качестве входного напряжения.
2. Если входное напряжение больше напряжения на аккумуляторе, то плата работает от внешнего источника питания. В качестве входонго напряжения отображается измерение с канала Vin.
Для иллюстрации правила используют два разных способа срабатывания: по изменению значения контрола (правило _system_track_vin) и по изменению значения выражения (два других).
defineVirtualDevice("power_status", {
title: "Power status", //
cells: {
'working on battery' : {
type : "switch",
value : false,
readonly : true
},
'Vin' : {
type : "voltage",
value : 0
}
}
});
defineRule("_system_track_vin", {
whenChanged: "wb-adc/Vin",
then: function() {
if (dev["wb-adc"]["Vin"] < dev["wb-adc"]["BAT"] ) {
dev["power_status"]["Vin"] = 0;
} else {
dev["power_status"]["Vin"] = dev["wb-adc"]["Vin"] ;
}
}
});
defineRule("_system_dc_on", {
asSoonAs: function () {
return dev["wb-adc"]["Vin"] > dev["wb-adc"]["BAT"];
},
then: function () {
dev["power_status"]["working on battery"] = false;
}
});
defineRule("_system_dc_off", {
asSoonAs: function () {
return dev["wb-adc"]["Vin"] <= dev["wb-adc"]["BAT"];
},
then: function () {
dev["power_status"]["working on battery"] = true;
}
});
Отправка команд по RS-485
Для примера отправим команду устройству на порт /dev/ttyNSC0 (соответствует аппаратному порту RS-485-ISO на Wiren Board 4). Для этого будем использовать движок правил и возможность выполнения произвольных shell-команд. Подробнее см. в документации.
С помощью движка правил создадим виртуальное устройство с контролом типа switch (переключатель).
При включении переключателя будем отправлять команду (уст. Яркость кан. 00=0xff) для Uniel UCH-M141:
FF FF 0A 01 FF 00 00 0A
При выключении переключателя будем отправлять команду (уст. Яркость кан. 00=0x00) для Uniel UCH-M141:
FF FF 0A 01 00 00 00 0B
1. Настройка порта
Для настройки порта /dev/ttyNSC0 на скорость 9600 надо выполнить следующую команду
stty -F /dev/ttyNSC0 ospeed 9600 ispeed 9600 raw clocal -crtscts -parenb -echo cs8
2. Отправка команды
Отправка данных делается следующей шелл-командой:
/usr/bin/printf '\xFF\xFF\x0A\x01\xD1\x06\x00\xE2' >/dev/ttyNSC0
где "\xFF\xFF\x0A\x01\xD1\x06\x00\xE2" - это запись команды "FF FF 0A 01 D1 06 00 E2".
3. Создадим в движке правил новый файл с правилами /etc/wb-rules/rs485_cmd.js
Файл можно редактировать с помощью vim, nano или mcedit в сеансе ssh на устройстве, либо залить его с помощью SCP.
root@wirenboard:~# mcedit /etc/wb-rules/rs485_cmd.js
4. Описываем в файле виртуальный девайс
defineVirtualDevice("rs485_cmd", {
title: "Send custom command to RS-485 port",
cells: {
enabled: {
type: "switch",
value: false
},
}
});
5. Перезапускаем wb-rules и проверяем работу
root@wirenboard:~# /etc/init.d/wb-rules restart root@wirenboard:~# tail -f /var/log/messages
В логе не должно быть сообщений об ошибке (выход через control-c)
В веб-интерфейсе в разделе Devices должно появиться новое устройство "Send custom command to RS-485 port".
6. Добавим функцию для конфигурирования порта.
function setup_port() {
runShellCommand("stty -F /dev/ttyNSC0 ospeed 9600 ispeed 9600 raw clocal -crtscts -parenb -echo cs8");
}
7. Опишем правила на включение и выключение переключателя
defineRule("_rs485_switch_on", {
asSoonAs: function () {
return dev.rs485_cmd.enabled;
},
then: function() {
runShellCommand("/usr/bin/printf '\\xff\\xff\\x0a\\x01\\xff\\x00\\x00\\x0a' > /dev/ttyNSC0");
}
});
defineRule("_rs485_switch_off", {
asSoonAs: function () {
return !dev.rs485_cmd.enabled;
},
then: function() {
runShellCommand("/usr/bin/printf '\\xff\\xff\\x0a\\x01\\x00\\x00\\x00\\x0b' >/dev/ttyNSC0");
}
});
Обратите внимание на двойное экранирование.
7. Собираем всё вместе
Полное содержимое файла с правилами:
defineVirtualDevice("rs485_cmd", {
title: "Send custom command to RS-485 port",
cells: {
enabled: {
type: "switch",
value: false
},
}
});
function setup_port() {
runShellCommand("stty -F /dev/ttyNSC0 ospeed 9600 ispeed 9600 raw clocal -crtscts -parenb -echo cs8");
}
defineRule("_rs485_switch_on", {
asSoonAs: function () {
return dev.rs485_cmd.enabled;
},
then: function() {
runShellCommand("/usr/bin/printf '\\xff\\xff\\x0a\\x01\\xff\\x00\\x00\\x0a' > /dev/ttyNSC0");
}
});
defineRule("_rs485_switch_off", {
asSoonAs: function () {
return !dev.rs485_cmd.enabled;
},
then: function() {
runShellCommand("/usr/bin/printf '\\xff\\xff\\x0a\\x01\\x00\\x00\\x00\\x0b' >/dev/ttyNSC0");
}
});
setTimeout(setup_port, 1000); // запланировать выполненеи setup_port() через 1 секунду после старта правил.