Rule Examples/ru: различия между версиями
FuzzyBot (обсуждение | вклад) (Обновление для соответствия новой версии исходной страницы.) |
FuzzyBot (обсуждение | вклад) (Обновление для соответствия новой версии исходной страницы.) |
||
(не показана 1 промежуточная версия этого же участника) | |||
Строка 1: | Строка 1: | ||
{{DISPLAYTITLE: Примеры правил}} | |||
<languages/> | <languages/> | ||
=== Слежение за контролом === | === Слежение за контролом === | ||
Строка 8: | Строка 7: | ||
Например правило может включать сирену и лампу, если датчик движения заметил движение. | Например правило может включать сирену и лампу, если датчик движения заметил движение. | ||
В примере датчик движения подключен к входу | В примере датчик движения подключен к входу "сухой контакт", контрол типа "switch". Сирена подключена к встроеному реле Wiren Board, а лампа - через релейный блок по Modbus. Когда вход типа "сухой контакт" (выход датчика движения) замкнут, то на лампу и реле подаётся "1", когда выключен - "0". | ||
Правило срабатывает каждый раз при изменении значения контрола | Правило срабатывает каждый раз при изменении значения контрола "D1_IN" у устройства "wb-gpio". В код правила передаётся новое значение этого контрола в виде переменной newValue. | ||
<syntaxhighlight lang="ecmascript"> | <syntaxhighlight lang="ecmascript"> | ||
Строка 17: | Строка 16: | ||
whenChanged: "wb-gpio/D1_IN", | whenChanged: "wb-gpio/D1_IN", | ||
then: function (newValue, devName, cellName) { | then: function (newValue, devName, cellName) { | ||
dev["wb-gpio | dev["wb-gpio"]["Relay_2"] = newValue; | ||
dev["wb-mrm2_6 | dev["wb-mrm2_6"]["Relay 1"] = newValue; | ||
} | } | ||
Строка 41: | Строка 40: | ||
whenChanged: "simple_test/enabled", | whenChanged: "simple_test/enabled", | ||
then: function (newValue, devName, cellName) { | then: function (newValue, devName, cellName) { | ||
dev["wb-gpio | dev["wb-gpio"]["Relay_2"] = newValue; | ||
dev["wb-mrm2_6 | dev["wb-mrm2_6"]["Relay 1"] = newValue; | ||
} | } | ||
Строка 50: | Строка 49: | ||
=== Детектор движения c таймаутом === | === Детектор движения c таймаутом === | ||
На вход D2 подключен детектор движения с выходом | На вход D2 подключен детектор движения с выходом "сухой контакт". При обнаружении движения он замыкает D2 и GND, и на соответствующем канале <code>wb-gpio/D2_IN</code> появляется статус "1". | ||
Освещение подключено через встроенное реле, соответствующий канал <code>wb-gpio/Relay_1</code>. | Освещение подключено через встроенное реле, соответствующий канал <code>wb-gpio/Relay_1</code>. | ||
Правило работает так: | Правило работает так: | ||
* когда движение появляется, свет включается. Если ранее был запущен тридцатисекундный таймер | * когда движение появляется, свет включается. Если ранее был запущен тридцатисекундный таймер "на выключение", этот таймер отключается; | ||
* когда движение пропадает, запускается тридцатисекундный таймер | * когда движение пропадает, запускается тридцатисекундный таймер "на выключение". Если ему удаётся дойти до конца, свет выключается. | ||
<syntaxhighlight lang="ecmascript"> | <syntaxhighlight lang="ecmascript"> | ||
Строка 66: | Строка 65: | ||
then: function (newValue, devName, cellName) { | then: function (newValue, devName, cellName) { | ||
if (newValue) { | if (newValue) { | ||
dev["wb-gpio | dev["wb-gpio"]["Relay_1"] = true; | ||
if (motion_timer_1_id) { | if (motion_timer_1_id) { | ||
clearTimeout(motion_timer_1_id); | clearTimeout(motion_timer_1_id); | ||
} | } | ||
motion_timer_1_id = setTimeout(function () { | motion_timer_1_id = setTimeout(function () { | ||
dev["wb-gpio | dev["wb-gpio"]["Relay_1"] = false; | ||
motion_timer_1_id = null; | motion_timer_1_id = null; | ||
}, motion_timer_1_timeout_ms); | }, motion_timer_1_timeout_ms); | ||
Строка 90: | Строка 89: | ||
then: function(newValue, devName, cellName) { | then: function(newValue, devName, cellName) { | ||
if (!newValue) { | if (!newValue) { | ||
dev["wb-gpio | dev["wb-gpio"][relay_control] = true; | ||
if (motion_timer_id) { | if (motion_timer_id) { | ||
clearTimeout(motion_timer_id); | clearTimeout(motion_timer_id); | ||
Строка 96: | Строка 95: | ||
motion_timer_id = setTimeout(function() { | motion_timer_id = setTimeout(function() { | ||
dev["wb-gpio | dev["wb-gpio"][relay_control] = false; | ||
motion_timer_id = null; | motion_timer_id = null; | ||
}, timeout_ms); | }, timeout_ms); | ||
Строка 136: | Строка 135: | ||
if ((date > date_start) && (date < date_end)) { | if ((date > date_start) && (date < date_end)) { | ||
if (newValue) { | if (newValue) { | ||
dev["wb-gpio | dev["wb-gpio"]["EXT1_R3A1"] = 1; | ||
if (motion_timer_1_id) { | if (motion_timer_1_id) { | ||
Строка 143: | Строка 142: | ||
motion_timer_1_id = setTimeout(function () { | motion_timer_1_id = setTimeout(function () { | ||
dev["wb-gpio | dev["wb-gpio"]["EXT1_R3A1"] = 0; | ||
motion_timer_1_id = null; | motion_timer_1_id = null; | ||
}, motion_timer_1_timeout_ms); | }, motion_timer_1_timeout_ms); | ||
Строка 180: | Строка 179: | ||
defineRule( "roller_shutter_up_on" + suffix, { | defineRule( "roller_shutter_up_on" + suffix, { | ||
asSoonAs: function() { | asSoonAs: function() { | ||
return dev[ | return dev[relay_up_device][relay_up_control]; | ||
}, | }, | ||
then: function () { | then: function () { | ||
Строка 188: | Строка 187: | ||
relay_up_timer_id = setTimeout(function() { | relay_up_timer_id = setTimeout(function() { | ||
return dev[ | return dev[relay_up_device][relay_up_control] = 0; | ||
}, timeout_s * 1000); | }, timeout_s * 1000); | ||
} | } | ||
Строка 195: | Строка 194: | ||
defineRule("roller_shutter_down_on" + suffix, { | defineRule("roller_shutter_down_on" + suffix, { | ||
asSoonAs: function() { | asSoonAs: function() { | ||
return dev[ | return dev[relay_down_device][relay_down_control]; | ||
}, | }, | ||
then: function () { | then: function () { | ||
Строка 203: | Строка 202: | ||
relay_down_timer_id = setTimeout(function() { | relay_down_timer_id = setTimeout(function() { | ||
dev[ | dev[relay_down_device][relay_down_control] = 0; | ||
}, timeout_s * 1000); | }, timeout_s * 1000); | ||
} | } | ||
Строка 210: | Строка 209: | ||
defineRule("roller_shutter_both_on" + suffix, { | defineRule("roller_shutter_both_on" + suffix, { | ||
asSoonAs: function() { | asSoonAs: function() { | ||
return dev[ | return dev[relay_up_device][relay_up_control] && dev[relay_down_device][relay_down_control]; | ||
}, | }, | ||
then: function () { | then: function () { | ||
Строка 223: | Строка 222: | ||
dev[relay_up_device][relay_up_control] = 0; | dev[relay_up_device][relay_up_control] = 0; | ||
dev[ | dev[relay_down_device][relay_down_control] = 0; | ||
log("Both roller shutter relays on, switching them off"); | log("Both roller shutter relays on, switching them off"); | ||
} | } | ||
Строка 296: | Строка 295: | ||
then: function(newValue, devName, cellName) { | then: function(newValue, devName, cellName) { | ||
if(newValue){ | if(newValue){ | ||
dev["water_meters | dev["water_meters"]["water_meter_1"] = ((parseInt(newValue) - counterCorrection) * inpulseValue) + meterCorrection; // Умножаем значение счетчика на количество литров/импульс и прибавляем корректировочное значение. | ||
} | } | ||
} | } | ||
Строка 429: | Строка 428: | ||
if (co2_good) { | if (co2_good) { | ||
dev[" | dev[devName]["Green LED"] = true; | ||
dev[" | dev[devName]["Red LED"] = false; | ||
dev[" | dev[devName]["LED Period (s)"] = 10; | ||
} | } | ||
if (co2_middle) { | if (co2_middle) { | ||
dev[" | dev[devName]["Green LED"] = true; | ||
dev[" | dev[devName]["Red LED"] = true; | ||
dev[" | dev[devName]["LED Period (s)"] = 5; | ||
} | } | ||
if (co2_bad) { | if (co2_bad) { | ||
dev[" | dev[devName]["Green LED"] = false; | ||
dev[" | dev[devName]["Red LED"] = true; | ||
dev[" | dev[devName]["LED Period (s)"] = 1; | ||
} | } | ||
} | } | ||
Строка 457: | Строка 456: | ||
then: function(newValue, devName, cellName) { | then: function(newValue, devName, cellName) { | ||
if (newValue > 50) { | if (newValue > 50) { | ||
if (dev["wb-msw-v3_97 | if (dev["wb-msw-v3_97"]["Illuminance"] < 50) { | ||
dev["wb-mr3_11 | dev["wb-mr3_11"]["K1"] = true; | ||
} | } | ||
} else { | } else { | ||
dev["wb-mr3_11 | dev["wb-mr3_11"]["K1"] = false; | ||
} | } | ||
} | } | ||
Строка 581: | Строка 580: | ||
whenChanged: "wb-adc/Vin", | whenChanged: "wb-adc/Vin", | ||
then: function() { | then: function() { | ||
if (dev["wb-adc"]["Vin"] < dev["wb-adc | if (dev["wb-adc"]["Vin"] < dev["wb-adc"]["BAT"] ) { | ||
dev["power_status | dev["power_status"]["Vin"] = 0; | ||
} else { | } else { | ||
dev["power_status | dev["power_status"]["Vin"] = dev["wb-adc"]["Vin"] ; | ||
} | } | ||
} | } | ||
Строка 593: | Строка 592: | ||
defineRule("_system_dc_on", { | defineRule("_system_dc_on", { | ||
asSoonAs: function () { | asSoonAs: function () { | ||
return dev["wb-adc | return dev["wb-adc"]["Vin"] > dev["wb-adc"]["BAT"]; | ||
}, | }, | ||
then: function () { | then: function () { | ||
dev["power_status | dev["power_status"]["working on battery"] = false; | ||
} | } | ||
}); | }); | ||
Строка 602: | Строка 601: | ||
defineRule("_system_dc_off", { | defineRule("_system_dc_off", { | ||
asSoonAs: function () { | asSoonAs: function () { | ||
return dev["wb-adc | return dev["wb-adc"]["Vin"] <= dev["wb-adc"]["BAT"]; | ||
}, | }, | ||
then: function () { | then: function () { | ||
dev["power_status | dev["power_status"]["working on battery"] = true; | ||
} | } | ||
}); | }); | ||
Строка 929: | Строка 928: | ||
Для расписаний используются не cron-правила, а обёртка над ними. Обёртка включает и выключает правила, которые, в отличие от cron-правил, выполняются постоянно, будучи включенными. | Для расписаний используются не cron-правила, а обёртка над ними. Обёртка включает и выключает правила, которые, в отличие от cron-правил, выполняются постоянно, будучи включенными. | ||
Например, мы хотим, чтобы освещение было включено с 10 до 17ч. Обёртка (libschedule) будет выполнять правило | Например, мы хотим, чтобы освещение было включено с 10 до 17ч. Обёртка (libschedule) будет выполнять правило "включить освещение" раз в минуту с 10 утра до 17 вечера. | ||
Это значит, что даже если контроллер работает с перерывами и пропустил время перехода между расписаниями (10 утра), то контроллер всё равно включит освещение при первой возможности. | Это значит, что даже если контроллер работает с перерывами и пропустил время перехода между расписаниями (10 утра), то контроллер всё равно включит освещение при первой возможности. |
Версия 20:26, 21 сентября 2022
Слежение за контролом
Это простейшее правило следит за контролом и устанавливает другой контрол в такое же состояние.
Например правило может включать сирену и лампу, если датчик движения заметил движение.
В примере датчик движения подключен к входу "сухой контакт", контрол типа "switch". Сирена подключена к встроеному реле Wiren Board, а лампа - через релейный блок по Modbus. Когда вход типа "сухой контакт" (выход датчика движения) замкнут, то на лампу и реле подаётся "1", когда выключен - "0".
Правило срабатывает каждый раз при изменении значения контрола "D1_IN" у устройства "wb-gpio". В код правила передаётся новое значение этого контрола в виде переменной newValue.
defineRule({
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".
Освещение подключено через встроенное реле, соответствующий канал 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"] = true;
if (motion_timer_1_id) {
clearTimeout(motion_timer_1_id);
}
motion_timer_1_id = setTimeout(function () {
dev["wb-gpio"]["Relay_1"] = false;
motion_timer_1_id = null;
}, motion_timer_1_timeout_ms);
}
},
});
Создание однотипных правил
Если таких детекторов движения нужно несколько, то, чтобы не копировать код, можно обернуть создание правила и переменных в функцию:
function makeMotionDetector(name, timeout_ms, detector_control, relay_control) {
var motion_timer_id = null;
defineRule(name, {
whenChanged: "wb-gpio/" + detector_control,
then: function(newValue, devName, cellName) {
if (!newValue) {
dev["wb-gpio"][relay_control] = true;
if (motion_timer_id) {
clearTimeout(motion_timer_id);
}
motion_timer_id = setTimeout(function() {
dev["wb-gpio"][relay_control] = false;
motion_timer_id = null;
}, timeout_ms);
}
}
});
}
makeMotionDetector("motion_detector_1", 20000, "EXT1_DR1", "EXT2_R3A1");
makeMotionDetector("motion_detector_2", 10000, "EXT1_DR2", "EXT2_R3A2");
makeMotionDetector("motion_detector_3", 10000, "EXT1_DR3", "EXT2_R3A3");
Активация правила только в определённое время
Правило как в предыдущем разделе, но выполняется только с 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_id = null;
}, 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-MCM8. Выдает 1 импульс на 10 литров воды. При подключении на счетчике были показания 123.120 м³, что равно 123120 литрам воды. У WB-MCM8 при подключении было насчитано 7 импульсов.
var meterCorrection = 123120 // Корректировочное значение счетчика в литрах
var counterCorrection = 7 // Корректировочное значение WB-MCM8 в импульсах
var inpulseValue = 10 // Количество литров на один импульс
defineVirtualDevice("water_meters", { // Создаем виртуальный девайс для отображения в веб интерфейсе.
title: "Счетчики воды",
cells: {
water_meter_1: {
type: "value",
value: 0
},
}
});
defineRule("water_meter_1", {
whenChanged: "wb-mcm8_29/Input 1 counter",
then: function(newValue, devName, cellName) {
if(newValue){
dev["water_meters"]["water_meter_1"] = ((parseInt(newValue) - counterCorrection) * inpulseValue) + meterCorrection; // Умножаем значение счетчика на количество литров/импульс и прибавляем корректировочное значение.
}
}
});
Обработка счётчиков нажатий
Описание
Последние версии прошивок устройств Wiren Board могут распознавать типы нажатий подключённых к входам кнопок и транслировать их по Modbus на контроллер Wiren Board. О том, как устройство распознаёт типы нажатий, читайте в его документации.
Для обработки нажатий нужно отслеживать на контроллере состояние счётчика нужного типа нажатия и, при его изменении, выполнять действие.
Обработку счётчиков удобно делать на wb-rules, но вы можете использовать любой инструмент для автоматизации, например, Node-RED. Чтобы ускорить опрос счетчиков, настройте период опроса.
Примеры
В примере мы используем модуль WB-MCM8 для управления первым каналом диммера WB-MDM3:
- Короткое нажатие включает канал.
- Двойное — выключает канал.
- Длинное — увеличивает яркость.
- Короткое, а затем длинное — уменьшает яркость.
Так как изменение яркости требует растянутое во времени действие, то мы используем таймер. Также мы контролируем состояние входа с кнопкой и прекращаем действие при отпускании кнопки.
/* ---------------------------- */
/* 1. Single Press Counter: On action*/
/* ---------------------------- */
defineRule({
whenChanged: "wb-mcm8_20/Input 1 Single Press Counter",
then: function (newValue, devName, cellName) {
dev["wb-mdm3_58/K1"] = true;
}
});
/* ---------------------------- */
/* 2. Double Press Counter: Off action*/
/* ---------------------------- */
defineRule({
whenChanged: "wb-mcm8_20/Input 1 Double Press Counter",
then: function (newValue, devName, cellName) {
dev["wb-mdm3_58/K1"] = false;
}
});
/* --------------------------------------- */
/* 3. Long Press Counter: Increase brightness */
/* --------------------------------------- */
defineRule({
whenChanged: "wb-mcm8_20/Input 1 Long Press Counter",
then: function (newValue, devName, cellName) {
// Start a timer that will increase the value of the control
startTicker("input1_long_press", 75);
}
});
// A rule that will increase the brightness on a timer
defineRule({
when: function () { return timers["input1_long_press"].firing; },
then: function () {
var i = dev["wb-mdm3_58/Channel 1"];
if (i < 100 && dev["wb-mcm8_20/Input 1"]) {
i++
dev["wb-mdm3_58/Channel 1"] = i
} else {
timers["input1_long_press"].stop();
}
}
});
/* -------------------------------------------- */
/* 4. Shortlong Press Counter: Decrease brightness */
/* -------------------------------------------- */
defineRule({
whenChanged: "wb-mcm8_20/Input 1 Shortlong Press Counter",
then: function (newValue, devName, cellName) {
// Start a timer that will decrease the value of the control
startTicker("input1_shortlong_press", 75);
}
});
// A rule that will decrease the brightness on a timer
defineRule({
when: function () { return timers["input1_shortlong_press"].firing; },
then: function () {
var i = dev["wb-mdm3_58/Channel 1"];
if (i > 0 && dev["wb-mcm8_20/Input 1"]) {
i--
dev["wb-mdm3_58/Channel 1"] = i
} else {
timers["input1_shortlong_press"].stop();
}
}
});
Универсальный модуль для wb-rules
Мы написали модуль для wb-rules wb-press-actions, который облегчает обработку нажатий в ваших скриптах.
Датчик MSW v.3
При подключении датчика WB-MSW v.3 к контроллеру Wiren Board есть возможность создавать интересные сценарии, используя данные с датчика. На пример Включать свет по движению, сигнализировать светодиодами о превышении значения СО2 или VOC, Включать Кондиционер, если жарко или увлажнитель воздуха, если воздух слишком сухой. Правила создаются индивидуально под задачи. Здесь мы приведем несколько примеров для понимания принципа работы с датчиком. Больше примеров написания правил можно найти в документации Движок правил wb-rules.
CO2
При концентрации CO2 меньше 650 - раз в 10 секунд мигаем зеленым.
При концентрации CO2 свыше 651, но меньше 1000 - раз в 5 секунд мигаем желтым.
При концентрации CO2 свыше 1001 - раз в секунду мигаем красным.
Max Motion
"Max Motion" - максимальное значение датчика движения за N время. Время от 1 до 60 секунд можно выставить в 282 регистре. По умолчанию 10 секунд. При достижении Max Motion значения 50 проверяем достаточно ли освещена комната, если нет - включаем свет. Как только значение Max Motion упадет ниже 50 свет выключаем.
defineRule("msw3_Motion", {
whenChanged: "wb-msw-v3_97/Max Motion",
then: function(newValue, devName, cellName) {
if (newValue > 50) {
if (dev["wb-msw-v3_97"]["Illuminance"] < 50) {
dev["wb-mr3_11"]["K1"] = true;
}
} else {
dev["wb-mr3_11"]["K1"] = false;
}
}
});
Системные правила
Многие показания, которые видны в веб-интерфейсе контроллера из коробки, тоже создаются правилами на движке wb-rules. Их код находится здесь: https://github.com/wirenboard/wb-rules-system. Системные правила собраны в пакет wb-rules-system
, сами файлы скриптов на контроллере находятся в папке /usr/share/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;
}
});
Термостат
Пример простого термостата из темы на портале поддержки.
defineVirtualDevice("Termostat", {
title: "Termostat",
cells: {
// =============== Прихожая теплый пол
"R01-TS16-1-mode": {//режим 0-ручной 1-по расписанию
type: "switch",
value: false,
},
"R01-TS16-1-setpoint": {//уставка
type: "range",
value: 25,
max: 30,
readonly: false
},
"R01-TS16-1-lock": {//блокировка в визуализации 0-снята 1-заблокирована
type: "switch",
value: false,
},.......
var hysteresis = 0.5;
function Termostat(name, temp, setpoint, TS, TS_onoff) {
defineRule(name, {
whenChanged: temp, //при изменении состояния датчика
then: function (newValue, devName, cellName) { //выполняй следующие действия
if (dev[TS_onoff]) {
if ( newValue < dev[setpoint] - hysteresis) { //если температура датчика меньше уставки - гистерезис
dev[TS] = true;
}
if ( newValue > dev[setpoint] + hysteresis) { //если температура датчика больше виртуальной уставки + гистерезис
dev[TS] = false;
}
}
else dev[TS] = false;
}
});
}
Termostat("R01-TS16-1", "A60-M1W3/External Sensor 1", "Termostat/R01-TS16-1-setpoint", "wb-gpio/EXT4_R3A1", "Termostat/R01-TS16-1-onoff"); // Прихожая теплый пол
Отправка команд по 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 секунду после старта правил.
Отправка сообщения через Telegram-бота
Сообщения отправляются с использованием Telegram API через curl
.
var message = "Text"; // напишите свой текст сообщения
var token = "TOKEN"; // замените на токен бота
var chat_id = CHATID; // замените на свой chat_id
var command = 'curl -s -X POST https://api.telegram.org/bot{}/sendMessage -d chat_id={} -d text="{}"'.format(token, chat_id, message);
runShellCommand(command);
Обработка ошибок в работе с serial-устройствами
Реализована через подписку на все топики meta/error.
defineVirtualDevice("meta_error_test", {
title: "Metaerordisplay",
cells: {
topic: {
type: "text",
value: "",
readonly: true
},
value: {
type: "text",
value: "",
readonly: true
},
}
});
trackMqtt("/devices/+/controls/+/meta/error", function(message){
log.info("name: {}, value: {}".format(message.topic, message.value))
if (message.value=="r"){
dev["meta_error_test/topic"] = message.topic;
dev["meta_error_test/value"] = message.value;
}
});
Пользовательские поля в веб-интерфейсе
Задача - надо в веб-интерфейсе контроллера Wiren Board вводить уставки температуры и влажности.
Простой способ, это сделать в defineVirtualDevice() поле, ему сделать readonly: false. И оно появится в веб-интерфейсе в Devices как редактируемое, а значение будет сохраняться в движке правил. Но сложную настройку с меню и вариантами так не сделать.
Правильный, но сложный способ — создать новую вкладку в разделе Configs с редактируемыми полями параметров установок .
Потребуются три файла:
1. Схема вывода html странички в разделе Configs : /usr/share/wb-mqtt-confed/schemas/test-config.schema.json
{
"type":"object",
"title":"Test configuration",
"description":"Long description configuration",
"configFile": {
"path":"/etc/test-config.conf",
"service":"wb-rules"
},
"properties": {
"temperature_setpoint": {
"type":"number",
"title":"Temperature Setpoint (Degrees C)",
"default": 25,
"propertyOrder": 1,
"minimum": 5,
"maximum": 40
},
"humidity_setpoint": {
"type":"number",
"title":"Humidity Setpoint (RH, %)",
"default": 60,
"propertyOrder": 2,
"minimum": 10,
"maximum": 95
}
},
"required": ["temperature_setpoint", "humidity_setpoint"]
}
2. Описание конфигурации по умолчанию (при сохранении формы в веб интерфейсе, значения запишутся в этот файл) : /etc/test-config.conf
{
"temperature_setpoint": 60,
"humidity_setpoint": 14
}
3. Скрипт, обновляющий конфиг : /mnt/data/etc/wb-rules/test-config-script.js
var config = readConfig("/etc/test-config.conf");
log("temperature setpoint is: {}".format(config.temperature_setpoint));
Последний файл можно в том числе редактировать из веб-интерфейса на вкладке Scripts.
В json файлах описаны схемы вывода html странички браузером, по общепринятому стандарту отображения. Описание ключей тут: json-schema.org.
После создания файлов, нужно выполнить рестарт сервисов
service wb-mqtt-confed restart
service wb-rules restart
При нажатии кнопки Save в веб-интерфейсе, будет перезапускаться сервис wb-rules, а значения установок - записываться в правила.
Сложные правила с расписаниями
Объект - продуктовый магазин. Различные системы магазина управляются по обратной связи от датчиков температуры и с учётом расписания работы магазина.
Для расписаний используются не cron-правила, а обёртка над ними. Обёртка включает и выключает правила, которые, в отличие от cron-правил, выполняются постоянно, будучи включенными.
Например, мы хотим, чтобы освещение было включено с 10 до 17ч. Обёртка (libschedule) будет выполнять правило "включить освещение" раз в минуту с 10 утра до 17 вечера.
Это значит, что даже если контроллер работает с перерывами и пропустил время перехода между расписаниями (10 утра), то контроллер всё равно включит освещение при первой возможности.
lib_schedules.js:
global.__proto__.Schedules = {};
(function(Schedules) { // замыкание
function todayAt(now, hours, minutes) {
var date = new Date(now);
// i.e. "today, at HH:MM". All dates are in UTC!
date.setHours(hours);
date.setMinutes(minutes);
return date;
}
function checkScheduleInterval(now, start_time, end_time) {
var start_date = todayAt(now, start_time[0], start_time[1]);
var end_date = todayAt(now, end_time[0], end_time[1]);
log("checkScheduleInterval {} {} {}".format(now, start_date, end_date));
if (end_date >= start_date) {
if ((now >= start_date) && (now < end_date)) {
return true;
}
} else {
// end date is less than start date,
// assuming they belong to a different days (e.g. today and tomorrow)
// option 1: what if it's now the day of "end" date?
// in this case the following is enough:
if (now < end_date) {
return true;
}
// well, that seems not to be the case. ok,
// option 2: it's the day of "start" date:
if (now >= start_date) {
return true;
}
}
return false;
}
function checkSchedule(schedule, now) {
if (now == undefined) {
now = new Date();
}
for (var i = 0; i < schedule.intervals.length; ++i) {
var item = schedule.intervals[i];
if (checkScheduleInterval(now, item[0], item[1])) {
log("found matching schedule interval at {}".format(item));
return true;
}
}
return false;
}
function updateSingleScheduleDevStatus(schedule) {
log("updateSingleScheduleDevStatus {}".format(schedule.name));
dev["_schedules"][schedule.name] = checkSchedule(schedule);
};
function addScheduleDevCronTasks(schedule) {
for (var i = 0; i < schedule.intervals.length; ++i) {
var interval = schedule.intervals[i];
for (var j = 0; j < 2; ++j) { // either start or end of the interval
var hours = interval[j][0];
var minutes = interval[j][1];
log("cron at " + "0 " + minutes + " " + hours + " * * *");
defineRule("_schedule_dev_{}_{}_{}".format(schedule.name, i, j), {
when: cron("0 " + minutes + " " + hours + " * * *"),
then: function () {
log("_schedule_dev_ {}_{}_{}".format(schedule.name, i, j));
updateSingleScheduleDevStatus(schedule);
}
});
}
}
}
function addScheduleAutoUpdCronTask(schedule) {
defineRule("_schedule_auto_upd_{}".format(schedule.name), {
when: cron("@every " + schedule.autoUpdate),
then: function() {
dev._schedules[schedule.name] = dev._schedules[schedule.name];
}
});
}
var _schedules = {};
Schedules.registerSchedule = function(schedule) {
_schedules[schedule.name] = schedule;
};
Schedules.initSchedules = function() {
var params = {
title: "Schedule Status",
cells: {}
};
for (var schedule_name in _schedules) {
if (_schedules.hasOwnProperty(schedule_name)) {
var schedule = _schedules[schedule_name];
params.cells[schedule_name] = {type: "switch", value: false, readonly: true};
}
};
defineVirtualDevice("_schedules", params);
for (var schedule_name in _schedules) {
if (_schedules.hasOwnProperty(schedule_name)) {
var schedule = _schedules[schedule_name];
// setup cron tasks which updates the schedule dev status at schedule
// interval beginings and ends
addScheduleDevCronTasks(schedule);
// if needed, setup periodic task to trigger rules which use this schedule
if (schedule.autoUpdate) {
addScheduleAutoUpdCronTask(schedule);
}
// set schedule dev status as soon as possible at startup
(function(schedule) {
setTimeout(function() {
updateSingleScheduleDevStatus(schedule);
}, 1);
})(schedule);
};
};
};
})(Schedules);
Пример правил, с использованием Schedules:
(function() { // замыкание
defineAlias("countersTemperature", "wb-msw2_30/Temperature");
defineAlias("vegetablesTemperature", "wb-msw2_31/Temperature");
defineAlias("heater1EnableInverted", "wb-mrm2-old_70/Relay 1");
defineAlias("frontshopVentInverted", "wb-gpio/EXT1_R3A3");
Schedules.registerSchedule({
"name" : "signboard", // вывеска
"autoUpdate" : "1m",
"intervals" : [
[ [12, 30], [20, 30] ], // в UTC, 15:30 - 23:30 MSK
[ [3, 30], [5, 20] ], // в UTC, 6:30 - 8:20 MSK
]
});
Schedules.registerSchedule({
"name" : "ext_working_hours_15m",
"autoUpdate" : "1m",
"intervals" : [
[ [4, 45], [20, 15] ], // всё ещё UTC, 7:45 - 23:15 MSK
]
});
Schedules.registerSchedule({
"name" : "working_hours",
"autoUpdate" : "1m",
"intervals" : [
[ [5, 0], [19, 0] ], // всё ещё UTC, 8:00 - 22:00 MSK
]
});
Schedules.registerSchedule({
"name" : "working_hours_15m",
"autoUpdate" : "1m",
"intervals" : [
[ [4, 45], [19, 15] ], // всё ещё UTC, 7:45 - 22:15 MSK
]
});
Schedules.registerSchedule({
"name" : "frontshop_lighting",
"autoUpdate" : "1m",
"intervals" : [
[ [4, 20], [20, 45] ], // всё ещё UTC, 7:20 -23:45 MSK
]
});
Schedules.registerSchedule({
"name" : "heaters_schedule",
"intervals" : [
[ [4, 0], [17, 0] ], // всё ещё UTC, 07:00 - 20:00 MSK дневной режим
]
});
Schedules.initSchedules();
// Вывеска и фасадное освещение
defineRule("signboardOnOff", {
when: function() {
return dev._schedules.signboard || true;
},
then: function (newValue, devName, cellName) {
log("signboardOnOff newValue={}, devName={}, cellName={}", newValue, devName, cellName);
var on = dev._schedules.signboard; //
dev["wb-mr6c_80/K2"] = !on;
dev["wb-mr6c_80/K1"] = !on;
dev["wb-mr6c_80/K3"] = !on;
}
});
// Освещение торгового зала
defineRule("lightingFrontshopOnOff", {
when: function() {
return dev._schedules.frontshop_lighting || true;
},
then: function (newValue, devName, cellName) {
log("lightingFrontshopOnOff newValue={}, devName={}, cellName={}", newValue, devName, cellName);
dev["wb-gpio/EXT1_R3A1"] = ! dev._schedules.frontshop_lighting; //инвертированный контактор
}
});
// Вентиляция подсобного помещения
defineRule("ventBackstoreOnOff", {
when: function() {
return dev._schedules.ext_working_hours_15m || true;
},
then: function (newValue, devName, cellName) {
log("ventBackstoreOnOff newValue={}, devName={}, cellName={}", newValue, devName, cellName);
var on = dev._schedules.ext_working_hours_15m;
dev["wb-mr6c_81/K1"] = ! on; //инвертированный контактор
dev["wb-mr6c_81/K5"] = ! on; //инвертированный контактор
}
});
// Освещение холодильных горок
defineRule("lightingCoolingshelfsOnOff", {
when: function() {
return dev._schedules.frontshop_lighting || true;
},
then: function (newValue, devName, cellName) {
log("lightingCoolingshelfsOnOff newValue={}, devName={}, cellName={}", newValue, devName, cellName);
var on = dev._schedules.working_hours_15m;
// освещение в горках через нормально-закрытые реле (инвертировано)
dev["wb-mrm2-old_60/Relay 1"] = !on;
dev["wb-mrm2-old_61/Relay 1"] = !on;
dev["wb-mrm2-old_62/Relay 1"] = !on;
dev["wb-mrm2-old_63/Relay 1"] = !on;
dev["wb-mrm2-old_64/Relay 1"] = !on;
dev["wb-mrm2-old_65/Relay 1"] = !on;
dev["wb-mrm2-old_66/Relay 1"] = !on;
dev["wb-mrm2-old_67/Relay 1"] = !on;
}
});
//Брендовые холодильники (пиво, лимонады)
defineRule("powerBrandFridgesOnOff", {
when: function() {
return dev._schedules.working_hours || true;
},
then: function (newValue, devName, cellName) {
log("powerBrandFridgesOnOff newValue={}, devName={}, cellName={}", newValue, devName, cellName);
var on = dev._schedules.working_hours;
dev["wb-gpio/EXT1_R3A5"] = !on; // инвертировано
}
});
// ========= Котлы и приточная вентиляция ТЗ ===========
// обратная связь по температуре овощной зоны
// днём работает позиционный регулятор
defineRule("heatersDayOff", {
when: function() {
return (dev._schedules.heaters_schedule) && (vegetablesTemperature > 17.0);
},
then: function (newValue, devName, cellName) {
log("heatersDayOff newValue={}, devName={}, cellName={}", newValue, devName, cellName);
heater1EnableInverted = !false; // инвертировано
}
});
defineRule("heatersDayOn", {
when: function() {
return (dev._schedules.heaters_schedule) && (vegetablesTemperature < 16.7);
},
then: function (newValue, devName, cellName) {
log("heatersDayOn newValue={}, devName={}, cellName={}", newValue, devName, cellName);
heater1EnableInverted = !true; // инвертировано
}
});
// ночью работает позиционный регулятор
defineRule("heatersNightOff", {
when: function() {
return (!dev._schedules.heaters_schedule) && (vegetablesTemperature > 11.6);
},
then: function (newValue, devName, cellName) {
log("heatersNightOff newValue={}, devName={}, cellName={}", newValue, devName, cellName);
heater1EnableInverted = !false; // инвертировано
}
});
defineRule("heatersNightOn", {
when: function() {
return (!dev._schedules.heaters_schedule) && (vegetablesTemperature < 11.3);
},
then: function (newValue, devName, cellName) {
log("heatersNightOn newValue={}, devName={}, cellName={}", newValue, devName, cellName);
heater1EnableInverted = !true; // инвертировано
}
});
// приточная и вытяжная вентиляция принудительно выключены
defineRule("ventFrontshopAlwaysOff", {
when: cron("@every 1m"),
then: function() {
dev["wb-gpio/EXT1_R3A3"] = !false;
dev["wb-gpio/EXT1_R3A4"] = !false;
}
});
// ================== Кассовая зона =================
// в кассовой зоне в рабочее время температура поддерживается кондиционерами (позиционный регулятор)
defineRule("countersACOn", {
when: function() {
return (dev._schedules.working_hours_15m) && (countersTemperature < 17.7);
},
then: function (newValue, devName, cellName) {
log("countersACOn newValue={}, devName={}, cellName={}", newValue, devName, cellName);
dev["wb-mir_75/Play from ROM7"] = true; // кондиционер кассовой зоны на нагрев
}
});
// в нерабочее время кондиционер выключен
defineRule("countersACOff", {
when: function() {
return (!dev._schedules.working_hours_15m) || (countersTemperature > 18.0);
},
then: function (newValue, devName, cellName) {
log("countersACOff newValue={}, devName={}, cellName={}", newValue, devName, cellName);
dev["wb-mir_75/Play from ROM2"] = true; // кондиционер кассовой зоны выключить
}
});
// =============== Овощная зона ==============
// Охлаждение овощей кондиционером только при температуре воздуха выше 18.5C
defineRule("acVegOn", {
when: function() {
return vegetablesTemperature >= 18.5
},
then: function (newValue, devName, cellName) {
log("acVegOn newValue={}, devName={}, cellName={}", newValue, devName, cellName);
dev["wb-mir_76/Play from ROM3"] = true; // Охлаждение +18
}
});
defineRule("acVegOff", {
when: function() {
return vegetablesTemperature < 17.8
},
then: function (newValue, devName, cellName) {
log("acVegOff newValue={}, devName={}, cellName={}", newValue, devName, cellName);
dev["wb-mir_76/Play from ROM2"] = true; // выключить
}
});
})()