Движок правил wb-rules: различия между версиями

Отметить эту версию для перевода
(Подготовка страницы к переводу)
(Отметить эту версию для перевода)
Строка 1: Строка 1:
<languages/>
<languages/>
<translate>
<translate>
<!--T:1-->
[[File:Wb rules demo.png|400px|thumb|right|Редактирование правил в веб-интерфейсе]]
[[File:Wb rules demo.png|400px|thumb|right|Редактирование правил в веб-интерфейсе]]
Для контроллера можно писать правила, например: "Если температура датчика меньше 18°С, включи нагреватель". Правила создаются через [[Special:MyLanguage/Веб-интерфейс Wiren Board|веб-интерфейс]] и пишутся на простом языке, похожем на Javascript.
Для контроллера можно писать правила, например: "Если температура датчика меньше 18°С, включи нагреватель". Правила создаются через [[Special:MyLanguage/Веб-интерфейс Wiren Board|веб-интерфейс]] и пишутся на простом языке, похожем на Javascript.


<!--T:2-->
Самое полное описание движка правил: https://github.com/contactless/wb-rules
Самое полное описание движка правил: https://github.com/contactless/wb-rules






== Как создавать и редактировать правила ==
== Как создавать и редактировать правила == <!--T:3-->


<!--T:4-->
*Список файлов с правилами находится на странице ''Scripts'' веб-интерфейса.
*Список файлов с правилами находится на странице ''Scripts'' веб-интерфейса.
*Нажмите на название файла, чтобы открыть его для редактирования.
*Нажмите на название файла, чтобы открыть его для редактирования.
Строка 20: Строка 23:




== Пишем первое правило ==
== Пишем первое правило == <!--T:5-->


<!--T:6-->
[[File:Web-scripts-rule1.png|400px|thumb|Правило для управления обогревателем, записанное через веб-интерфейс]]
[[File:Web-scripts-rule1.png|400px|thumb|Правило для управления обогревателем, записанное через веб-интерфейс]]


<!--T:7-->
Правила бывают двух типов - непосредственно правила (начинаются со слов ''defineRule'') и виртуальные устройства (начинаются со слов ''defineVirtualDevice''). Виртуальные устройства - это появляющиеся в веб-интерфейсе новые элементы управления - например, кнопка-выключатель, которая на самом деле выключает два устройства одновременно. Она не привязана напрямую ни к какому физическому устройству, а действия при её нажатии определяются написанным вами скриптом.
Правила бывают двух типов - непосредственно правила (начинаются со слов ''defineRule'') и виртуальные устройства (начинаются со слов ''defineVirtualDevice''). Виртуальные устройства - это появляющиеся в веб-интерфейсе новые элементы управления - например, кнопка-выключатель, которая на самом деле выключает два устройства одновременно. Она не привязана напрямую ни к какому физическому устройству, а действия при её нажатии определяются написанным вами скриптом.


<!--T:8-->
Любое количество разных правил можно хранить в одном файле. Обычно в одном файле хранятся правила, отвечающие за близкие функции.
Любое количество разных правил можно хранить в одном файле. Обычно в одном файле хранятся правила, отвечающие за близкие функции.






=== Первое правило ===
=== Первое правило === <!--T:9-->


<!--T:10-->
Для начала разберём простое правило - при превышении температуры выключи обогреватель. Температуру получаем с датчика [[Special:MyLanguage/1-Wire|1-Wire]], обогреватель подключён к Реле 1 внешнего релейного модуля [[Special:MyLanguage/WB-MRM2|WB-MRM2]].
Для начала разберём простое правило - при превышении температуры выключи обогреватель. Температуру получаем с датчика [[Special:MyLanguage/1-Wire|1-Wire]], обогреватель подключён к Реле 1 внешнего релейного модуля [[Special:MyLanguage/WB-MRM2|WB-MRM2]].
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">


<!--T:11-->
defineRule("heater_control", { //название правила - "контроль обогревателя", может быть произвольным
defineRule("heater_control", { //название правила - "контроль обогревателя", может быть произвольным
   whenChanged: "wb-w1/28-0115a48fcfff", //при изменении состояния датчика 1-Wire с идентификатором 28-0115a48fcfff
   whenChanged: "wb-w1/28-0115a48fcfff", //при изменении состояния датчика 1-Wire с идентификатором 28-0115a48fcfff
Строка 46: Строка 54:
});
});


<!--T:12-->
</syntaxhighlight>
</syntaxhighlight>
*Первая строка - кодовое слово ''defineRule'' и название правила
*Первая строка - кодовое слово ''defineRule'' и название правила
Строка 55: Строка 64:




=== Первое правило с виртуальным устройством ===
=== Первое правило с виртуальным устройством === <!--T:13-->


<!--T:14-->
Создаём виртуальный переключатель, при нажатии на который переключаются сразу два реле.
Создаём виртуальный переключатель, при нажатии на который переключаются сразу два реле.


<!--T:15-->
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">
defineVirtualDevice("switch_both", {
defineVirtualDevice("switch_both", {
Строка 70: Строка 81:
});
});


<!--T:16-->
defineRule("control_both", {
defineRule("control_both", {
   whenChanged: "switch_both/enabled",
   whenChanged: "switch_both/enabled",
Строка 81: Строка 93:




=== Пишем сложные правила ===
=== Пишем сложные правила === <!--T:17-->


<!--T:18-->
Чтобы начать писать сложные правила, нужно посмотреть примеры правил и полную документацию по движку правил:
Чтобы начать писать сложные правила, нужно посмотреть примеры правил и полную документацию по движку правил:
#Примеры правил:
#Примеры правил:
Строка 91: Строка 104:




== Примеры правил ==
== Примеры правил == <!--T:19-->




=== Слежение за контролом ===
=== Слежение за контролом === <!--T:20-->


<!--T:21-->
Это простейшее правило следит за контролом и устанавливает другой контрол в такое же состояние.
Это простейшее правило следит за контролом и устанавливает другой контрол в такое же состояние.


<!--T:22-->
Например правило может включать сирену и лампу, если датчик движения заметил движение.  
Например правило может включать сирену и лампу, если датчик движения заметил движение.  


<!--T:23-->
В примере датчик движения подключен к входу "сухой контакт", контрол типа "switch". Сирена подключена к встроеному реле Wiren Board, а лампа - через релейный блок по Modbus.  Когда вход типа "сухой контакт" (выход датчика движения) замкнут, то на лампу и реле подаётся "1", когда выключен - "0".
В примере датчик движения подключен к входу "сухой контакт", контрол типа "switch". Сирена подключена к встроеному реле Wiren Board, а лампа - через релейный блок по Modbus.  Когда вход типа "сухой контакт" (выход датчика движения) замкнут, то на лампу и реле подаётся "1", когда выключен - "0".




<!--T:24-->
Правило срабатывает каждый раз при изменении значения контрола "D1_IN" у устройства "wb-gpio".  В код правила передаётся новое значение этого контрола в виде переменной newValue.
Правило срабатывает каждый раз при изменении значения контрола "D1_IN" у устройства "wb-gpio".  В код правила передаётся новое значение этого контрола в виде переменной newValue.


<!--T:25-->
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">


<!--T:26-->
defineRule("motion_detector", {
defineRule("motion_detector", {
   whenChanged: "wb-gpio/D1_IN",
   whenChanged: "wb-gpio/D1_IN",
Строка 113: Строка 132:
dev["wb-mrm2_6"]["Relay 1"] = newValue;
dev["wb-mrm2_6"]["Relay 1"] = newValue;


   }
   <!--T:27-->
}
});
});


<!--T:28-->
</syntaxhighlight>
</syntaxhighlight>




<!--T:29-->
То же самое, но с виртуальным девайсом в качестве источника событий. Пример использования: сценарная кнопка, которая включает/выключает сирену и лампочку.
То же самое, но с виртуальным девайсом в качестве источника событий. Пример использования: сценарная кнопка, которая включает/выключает сирену и лампочку.


<!--T:30-->
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">
defineVirtualDevice("simple_test", {
defineVirtualDevice("simple_test", {
Строка 133: Строка 156:




<!--T:31-->
defineRule("simple_switch", {
defineRule("simple_switch", {
   whenChanged: "simple_test/enabled",
   whenChanged: "simple_test/enabled",
Строка 139: Строка 163:
dev["wb-mrm2_6"]["Relay 1"] = newValue;
dev["wb-mrm2_6"]["Relay 1"] = newValue;


   }
   <!--T:32-->
}
});
});


<!--T:33-->
</syntaxhighlight>
</syntaxhighlight>






=== Детектор движения c таймаутом ===
=== Детектор движения c таймаутом === <!--T:34-->


<!--T:35-->
На вход D2 подключен детектор движения с выходом типа "сухой контакт", который замыкает D2 и GND при обнаружении движения.
На вход D2 подключен детектор движения с выходом типа "сухой контакт", который замыкает D2 и GND при обнаружении движения.
При этом, на канале "wb-gpio/D2_IN" появляется статус "1".
При этом, на канале "wb-gpio/D2_IN" появляется статус "1".


<!--T:36-->
Правило включает свет при обнаружении движения и выключает свет, спустя 30 секунд после пропадания сигнала с датчика движения.
Правило включает свет при обнаружении движения и выключает свет, спустя 30 секунд после пропадания сигнала с датчика движения.


<!--T:37-->
Освещение подключено через встроенное реле, канал wb-gpio/Relay_1.
Освещение подключено через встроенное реле, канал wb-gpio/Relay_1.


<!--T:38-->
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">


<!--T:39-->
var motion_timer_1_timeout_ms = 30 * 1000;
var motion_timer_1_timeout_ms = 30 * 1000;
var motion_timer_1_id = null;
var motion_timer_1_id = null;


<!--T:40-->
defineRule("motion_detector_1", {
defineRule("motion_detector_1", {
   whenChanged: "wb-gpio/D2_IN",
   whenChanged: "wb-gpio/D2_IN",
Строка 166: Строка 198:
         dev["wb-gpio"]["Relay_1"] = 1;
         dev["wb-gpio"]["Relay_1"] = 1;


       if (motion_timer_1_id) {
       <!--T:41-->
if (motion_timer_1_id) {
           clearTimeout(motion_timer_1_id);
           clearTimeout(motion_timer_1_id);
       }
       }
Строка 178: Строка 211:
});
});


<!--T:42-->
</syntaxhighlight>
</syntaxhighlight>






=== Создание однотипных правил ===
=== Создание однотипных правил === <!--T:43-->


<!--T:44-->
Если таких детекторов движения нужно несколько, то, чтобы не копировать код, можно обернуть создание правила и переменных в функцию:
Если таких детекторов движения нужно несколько, то, чтобы не копировать код, можно обернуть создание правила и переменных в функцию:


<!--T:45-->
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">
  function makeMotionDetector(name, timeout_ms, detector_control, relay_control) {
  function makeMotionDetector(name, timeout_ms, detector_control, relay_control) {
Строка 198: Строка 234:
               }
               }


               motion_timer_id = setTimeout(function() {
               <!--T:46-->
motion_timer_id = setTimeout(function() {
                   dev["wb-gpio"][relay_control] = 0;
                   dev["wb-gpio"][relay_control] = 0;
                   motion_timer_id = null;
                   motion_timer_id = null;
Строка 207: Строка 244:
}
}


<!--T:47-->
makeMotionDetector("motion_detector_1", 20000, "EXT1_DR1", "EXT2_R3A1");
makeMotionDetector("motion_detector_1", 20000, "EXT1_DR1", "EXT2_R3A1");
makeMotionDetector("motion_detector_2", 10000, "EXT1_DR2", "EXT2_R3A2");
makeMotionDetector("motion_detector_2", 10000, "EXT1_DR2", "EXT2_R3A2");
Строка 214: Строка 252:




=== Активация правила только в определённое время ===
=== Активация правила только в определённое время === <!--T:48-->


<!--T:49-->
Правило как в предыдущем разделе, но выполняется только с 9:30 до 17:10 по UTC.
Правило как в предыдущем разделе, но выполняется только с 9:30 до 17:10 по UTC.


<!--T:50-->
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">
var motion_timer_1_timeout_ms = 5 * 1000;
var motion_timer_1_timeout_ms = 5 * 1000;
Строка 227: Строка 267:
     var date = new Date();
     var date = new Date();


     // time point marking the beginning of the interval
     <!--T:51-->
// time point marking the beginning of the interval
     // i.e. "today, at HH:MM". All dates are in UTC!
     // i.e. "today, at HH:MM". All dates are in UTC!
     var date_start = new Date(date);
     var date_start = new Date(date);
Строка 233: Строка 274:
     date_start.setMinutes(30);
     date_start.setMinutes(30);


     // time point marking the end of the interval
     <!--T:52-->
// time point marking the end of the interval
     var date_end = new Date(date);
     var date_end = new Date(date);
     date_end.setHours(17);
     date_end.setHours(17);
Строка 259: Строка 301:




=== Роллеты ===
=== Роллеты === <!--T:53-->


<!--T:54-->
Одно реле включает двигатель, поднимающий шторы, второе реле - включает двигатель, опускающий шторы.
Одно реле включает двигатель, поднимающий шторы, второе реле - включает двигатель, опускающий шторы.
Правило следит за тем, чтобы оба реле не были включены одновременно.
Правило следит за тем, чтобы оба реле не были включены одновременно.


<!--T:55-->
Кроме этого, правило отключает двигатели спустя заданное время после включения.
Кроме этого, правило отключает двигатели спустя заданное время после включения.


<!--T:56-->
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">
(function() { //don't touch this line
(function() { //don't touch this line
Строка 274: Строка 319:
   var relay_up_control = "Relay 1";
   var relay_up_control = "Relay 1";


   var relay_down_device = "lc103_4";
   <!--T:57-->
var relay_down_device = "lc103_4";
   var relay_down_control = "Relay 2";
   var relay_down_control = "Relay 2";


   var timeout_s = 15;
   <!--T:58-->
var timeout_s = 15;
    
    
   // End of settings
   // End of settings
Строка 294: Строка 341:
       };
       };


       relay_up_timer_id = setTimeout(function() {
       <!--T:59-->
relay_up_timer_id = setTimeout(function() {
         return dev[relay_up_device][relay_up_control] = 0;
         return dev[relay_up_device][relay_up_control] = 0;
       }, timeout_s * 1000);
       }, timeout_s * 1000);
Строка 300: Строка 348:
   });
   });


   defineRule("roller_shutter_down_on" + suffix, {
   <!--T:60-->
defineRule("roller_shutter_down_on" + suffix, {
     asSoonAs: function() {
     asSoonAs: function() {
       return dev[relay_down_device][relay_down_control];
       return dev[relay_down_device][relay_down_control];
Строка 315: Строка 364:
   });
   });


   defineRule("roller_shutter_both_on" + suffix, {
   <!--T:61-->
defineRule("roller_shutter_both_on" + suffix, {
     asSoonAs: function() {
     asSoonAs: function() {
       return dev[relay_up_device][relay_up_control] && dev[relay_down_device][relay_down_control];
       return dev[relay_up_device][relay_up_control] && dev[relay_down_device][relay_down_control];
Строка 324: Строка 374:
       };
       };


       if (relay_down_timer_id) {
       <!--T:62-->
if (relay_down_timer_id) {
         relay_down_timer_id = clearTimeout(relay_down_timer_id);  
         relay_down_timer_id = clearTimeout(relay_down_timer_id);  
       };
       };


        
        
       dev[relay_up_device][relay_up_control] = 0;
       <!--T:63-->
dev[relay_up_device][relay_up_control] = 0;
       dev[relay_down_device][relay_down_control] = 0;
       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");
Строка 339: Строка 391:




<!--T:64-->
Более старая версия того же сценария демонстрирует использование alias-ов:
Более старая версия того же сценария демонстрирует использование alias-ов:
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">


<!--T:65-->
(function() {
(function() {
   defineAlias("relay_up_1", "lc103_4/Relay 1");
   defineAlias("relay_up_1", "lc103_4/Relay 1");
Строка 347: Строка 401:
   var timeout_s = 15;
   var timeout_s = 15;


   defineRule("roller_shutter_1_up_on", {
   <!--T:66-->
defineRule("roller_shutter_1_up_on", {
   asSoonAs: function() {
   asSoonAs: function() {
     return relay_up_1;
     return relay_up_1;
Строка 358: Строка 413:
   });
   });


   defineRule("roller_shutter_1_down_on", {
   <!--T:67-->
defineRule("roller_shutter_1_down_on", {
     asSoonAs: function() {
     asSoonAs: function() {
       return relay_down_1;
       return relay_down_1;
Строка 369: Строка 425:
   });
   });


   defineRule("roller_shutter_1_both_on", {
   <!--T:68-->
defineRule("roller_shutter_1_both_on", {
     asSoonAs: function() {
     asSoonAs: function() {
       return relay_up_1 && relay_down_1;
       return relay_up_1 && relay_down_1;
Строка 381: Строка 438:
})();
})();


<!--T:69-->
</syntaxhighlight>
</syntaxhighlight>






=== Системные правила ===
=== Системные правила === <!--T:70-->


<!--T:71-->
Некоторые правила поставляются с системой правил по умолчанию в пакете wb-rules-system.  
Некоторые правила поставляются с системой правил по умолчанию в пакете wb-rules-system.  


<!--T:72-->
Полный список правил [https://github.com/contactless/wb-rules-system/tree/master/rules в репозитории].
Полный список правил [https://github.com/contactless/wb-rules-system/tree/master/rules в репозитории].


<!--T:73-->
Некоторые примеры:
Некоторые примеры:






==== Правило для пищалки ====
==== Правило для пищалки ==== <!--T:74-->


<!--T:75-->
[https://github.com/contactless/wb-rules-system/blob/master/rules/buzzer.js Правило] создаёт виртуальное устройство buzzer с ползунками для регулировки громкости и частоты, а также кнопкой включения звука.
[https://github.com/contactless/wb-rules-system/blob/master/rules/buzzer.js Правило] создаёт виртуальное устройство buzzer с ползунками для регулировки громкости и частоты, а также кнопкой включения звука.




<!--T:76-->
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">
defineVirtualDevice("buzzer", {
defineVirtualDevice("buzzer", {
   title: "Buzzer", //
   title: "Buzzer", //


   cells: {
   <!--T:77-->
cells: {
     frequency : {
     frequency : {
         type : "range",
         type : "range",
Строка 423: Строка 487:




<!--T:78-->
// setup pwm2
// setup pwm2
runShellCommand("echo 2 > /sys/class/pwm/pwmchip0/export");
runShellCommand("echo 2 > /sys/class/pwm/pwmchip0/export");
Строка 428: Строка 493:




<!--T:79-->
function _buzzer_set_params() {
function _buzzer_set_params() {
         var period = parseInt(1.0 / dev.buzzer.frequency * 1E9);
         var period = parseInt(1.0 / dev.buzzer.frequency * 1E9);
Строка 433: Строка 499:




         runShellCommand("echo " + period + " > /sys/class/pwm/pwmchip0/pwm2/period");
         <!--T:80-->
runShellCommand("echo " + period + " > /sys/class/pwm/pwmchip0/pwm2/period");
         runShellCommand("echo " + duty_cycle + " > /sys/class/pwm/pwmchip0/pwm2/duty_cycle");
         runShellCommand("echo " + duty_cycle + " > /sys/class/pwm/pwmchip0/pwm2/duty_cycle");
};
};




<!--T:81-->
defineRule("_system_buzzer_params", {
defineRule("_system_buzzer_params", {
   whenChanged: [
   whenChanged: [
Строка 444: Строка 512:
     ],
     ],


   then: function (newValue, devName, cellName) {
   <!--T:82-->
then: function (newValue, devName, cellName) {
     if ( dev.buzzer.enabled) {
     if ( dev.buzzer.enabled) {
         _buzzer_set_params();
         _buzzer_set_params();
Строка 452: Строка 521:




<!--T:83-->
defineRule("_system_buzzer_onof", {
defineRule("_system_buzzer_onof", {
   whenChanged: "buzzer/enabled",
   whenChanged: "buzzer/enabled",
Строка 466: Строка 536:




<!--T:84-->
</syntaxhighlight>
</syntaxhighlight>






==== Правило для статуса питания ====
==== Правило для статуса питания ==== <!--T:85-->


<!--T:86-->
[https://github.com/contactless/wb-rules-system/blob/master/rules/power_status.js Правило] создаёт виртуальное устройство, которое сообщает текущий статус питания. В качестве входных данных используется два канала АЦП: измерение напряжения на аккумуляторе и измерение входного напряжения.
[https://github.com/contactless/wb-rules-system/blob/master/rules/power_status.js Правило] создаёт виртуальное устройство, которое сообщает текущий статус питания. В качестве входных данных используется два канала АЦП: измерение напряжения на аккумуляторе и измерение входного напряжения.


<!--T:87-->
Реализована следующая логика:
Реализована следующая логика:


<!--T:88-->
1. Если входное напряжение меньше напряжение на аккумуляторе, то значит плата питается от аккумулятора. В этом случае, также отображается 0V в качестве входного напряжения.
1. Если входное напряжение меньше напряжение на аккумуляторе, то значит плата питается от аккумулятора. В этом случае, также отображается 0V в качестве входного напряжения.


<!--T:89-->
2. Если входное напряжение  больше напряжения на аккумуляторе, то плата работает от внешнего источника питания. В качестве входонго напряжения отображается измерение с канала Vin.
2. Если входное напряжение  больше напряжения на аккумуляторе, то плата работает от внешнего источника питания. В качестве входонго напряжения отображается измерение с канала Vin.




<!--T:90-->
Для иллюстрации правила используют два разных способа срабатывания: по изменению значения контрола (правило _system_track_vin) и по изменению значения выражения (два других).
Для иллюстрации правила используют два разных способа срабатывания: по изменению значения контрола (правило _system_track_vin) и по изменению значения выражения (два других).


<!--T:91-->
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">


<!--T:92-->
defineVirtualDevice("power_status", {
defineVirtualDevice("power_status", {
   title: "Power status", //
   title: "Power status", //


   cells: {
   <!--T:93-->
cells: {
     'working on battery' : {
     'working on battery' : {
         type : "switch",
         type : "switch",
Строка 500: Строка 579:




   }
   <!--T:94-->
}
});
});






<!--T:95-->
defineRule("_system_track_vin", {
defineRule("_system_track_vin", {
     whenChanged: "wb-adc/Vin",
     whenChanged: "wb-adc/Vin",
Строка 518: Строка 599:




<!--T:96-->
defineRule("_system_dc_on", {
defineRule("_system_dc_on", {
   asSoonAs: function () {
   asSoonAs: function () {
Строка 527: Строка 609:
});
});


<!--T:97-->
defineRule("_system_dc_off", {
defineRule("_system_dc_off", {
   asSoonAs: function () {
   asSoonAs: function () {
Строка 536: Строка 619:
});
});


<!--T:98-->
</syntaxhighlight>
</syntaxhighlight>






=== Отправка команд по RS-485 ===
=== Отправка команд по RS-485 === <!--T:99-->


<!--T:100-->
Для примера отправим команду устройству на порт ''/dev/ttyNSC0'' (соответствует аппаратному порту RS-485-ISO на [[Special:MyLanguage/Wiren Board 4|Wiren Board 4]]).
Для примера отправим команду устройству на порт ''/dev/ttyNSC0'' (соответствует аппаратному порту RS-485-ISO на [[Special:MyLanguage/Wiren Board 4|Wiren Board 4]]).
Для этого будем использовать движок правил и возможность выполнения произвольных shell-команд. Подробнее см. [https://github.com/contactless/wb-rules#%D0%94%D1%80%D1%83%D0%B3%D0%B8%D0%B5-%D0%BF%D1%80%D0%B5%D0%B4%D0%BE%D0%BF%D1%80%D0%B5%D0%B4%D0%B5%D0%BB%D1%91%D0%BD%D0%BD%D1%8B%D0%B5-%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8-%D0%B8-%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%BD%D1%8B%D0%B5 в документации].
Для этого будем использовать движок правил и возможность выполнения произвольных shell-команд. Подробнее см. [https://github.com/contactless/wb-rules#%D0%94%D1%80%D1%83%D0%B3%D0%B8%D0%B5-%D0%BF%D1%80%D0%B5%D0%B4%D0%BE%D0%BF%D1%80%D0%B5%D0%B4%D0%B5%D0%BB%D1%91%D0%BD%D0%BD%D1%8B%D0%B5-%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8-%D0%B8-%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%BD%D1%8B%D0%B5 в документации].


<!--T:101-->
С помощью движка правил создадим виртуальное устройство с контролом типа switch (переключатель).  
С помощью движка правил создадим виртуальное устройство с контролом типа switch (переключатель).  


<!--T:102-->
При включении переключателя будем отправлять команду (уст. Яркость кан. 00=0xff) для Uniel UCH-M141:
При включении переключателя будем отправлять команду (уст. Яркость кан. 00=0xff) для Uniel UCH-M141:
<pre>
<pre>
Строка 553: Строка 640:




<!--T:103-->
При выключении переключателя будем отправлять команду (уст. Яркость кан. 00=0x00) для Uniel UCH-M141:
При выключении переключателя будем отправлять команду (уст. Яркость кан. 00=0x00) для Uniel UCH-M141:
<pre>
<pre>
Строка 562: Строка 650:




<!--T:104-->
1. Настройка порта  
1. Настройка порта  


<!--T:105-->
Для настройки порта /dev/ttyNSC0 на скорость 9600 надо выполнить следующую команду
Для настройки порта /dev/ttyNSC0 на скорость 9600 надо выполнить следующую команду


<!--T:106-->
<pre>
<pre>
stty -F /dev/ttyNSC0 ospeed 9600 ispeed 9600 raw clocal -crtscts -parenb -echo cs8
stty -F /dev/ttyNSC0 ospeed 9600 ispeed 9600 raw clocal -crtscts -parenb -echo cs8
</pre>
</pre>


<!--T:107-->
2. Отправка команды  
2. Отправка команды  


<!--T:108-->
Отправка данных делается следующей шелл-командой:
Отправка данных делается следующей шелл-командой:


<!--T:109-->
<pre>
<pre>
/usr/bin/printf '\xFF\xFF\x0A\x01\xD1\x06\x00\xE2' >/dev/ttyNSC0
/usr/bin/printf '\xFF\xFF\x0A\x01\xD1\x06\x00\xE2' >/dev/ttyNSC0
Строка 580: Строка 674:




<!--T:110-->
3. Создадим в движке правил новый файл с правилами <code>/etc/wb-rules/rs485_cmd.js</code>
3. Создадим в движке правил новый файл с правилами <code>/etc/wb-rules/rs485_cmd.js</code>


<!--T:111-->
Файл можно редактировать с помощью vim, nano или mcedit в сеансе ssh на устройстве, либо залить его с помощью SCP.
Файл можно редактировать с помощью vim, nano или mcedit в сеансе ssh на устройстве, либо залить его с помощью SCP.


<!--T:112-->
<pre>
<pre>
root@wirenboard:~# mcedit  /etc/wb-rules/rs485_cmd.js
root@wirenboard:~# mcedit  /etc/wb-rules/rs485_cmd.js
Строка 589: Строка 686:




<!--T:113-->
4. Описываем в файле виртуальный девайс
4. Описываем в файле виртуальный девайс


<!--T:114-->
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">
defineVirtualDevice("rs485_cmd", {
defineVirtualDevice("rs485_cmd", {
Строка 604: Строка 703:




<!--T:115-->
5. Перезапускаем wb-rules и проверяем работу
5. Перезапускаем wb-rules и проверяем работу


<!--T:116-->
<pre>
<pre>
root@wirenboard:~# /etc/init.d/wb-rules restart
root@wirenboard:~# /etc/init.d/wb-rules restart
Строка 611: Строка 712:
</pre>
</pre>


<!--T:117-->
В логе не должно быть сообщений об ошибке (выход через control-c)
В логе не должно быть сообщений об ошибке (выход через control-c)




<!--T:118-->
В веб-интерфейсе в разделе Devices должно появиться новое устройство "Send custom command to RS-485 port".
В веб-интерфейсе в разделе Devices должно появиться новое устройство "Send custom command to RS-485 port".




<!--T:119-->
6. Добавим функцию для конфигурирования порта.
6. Добавим функцию для конфигурирования порта.




<!--T:120-->
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">
function setup_port() {
function setup_port() {
Строка 625: Строка 730:
}
}


<!--T:121-->
</syntaxhighlight>
</syntaxhighlight>




<!--T:122-->
7. Опишем правила на включение и выключение переключателя
7. Опишем правила на включение и выключение переключателя


<!--T:123-->
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">
defineRule("_rs485_switch_on", {
defineRule("_rs485_switch_on", {
Строка 640: Строка 748:
});
});


<!--T:124-->
defineRule("_rs485_switch_off", {
defineRule("_rs485_switch_off", {
   asSoonAs: function () {
   asSoonAs: function () {
Строка 649: Строка 758:
});
});


<!--T:125-->
</syntaxhighlight>
</syntaxhighlight>




<!--T:126-->
Обратите внимание на двойное экранирование.
Обратите внимание на двойное экранирование.


Строка 657: Строка 768:




<!--T:127-->
7. Собираем всё вместе
7. Собираем всё вместе


<!--T:128-->
Полное содержимое файла с правилами:
Полное содержимое файла с правилами:


<!--T:129-->
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">
defineVirtualDevice("rs485_cmd", {
defineVirtualDevice("rs485_cmd", {
Строка 673: Строка 787:




<!--T:130-->
function setup_port() {
function setup_port() {
     runShellCommand("stty -F /dev/ttyNSC0 ospeed 9600 ispeed 9600 raw clocal -crtscts -parenb -echo cs8");
     runShellCommand("stty -F /dev/ttyNSC0 ospeed 9600 ispeed 9600 raw clocal -crtscts -parenb -echo cs8");
}
}


<!--T:131-->
defineRule("_rs485_switch_on", {
defineRule("_rs485_switch_on", {
   asSoonAs: function () {
   asSoonAs: function () {
Строка 686: Строка 802:
});
});


<!--T:132-->
defineRule("_rs485_switch_off", {
defineRule("_rs485_switch_off", {
   asSoonAs: function () {
   asSoonAs: function () {
Строка 695: Строка 812:
});
});


<!--T:133-->
setTimeout(setup_port, 1000); // запланировать выполнение setup_port() через 1 секунду после старта правил.
setTimeout(setup_port, 1000); // запланировать выполнение setup_port() через 1 секунду после старта правил.


<!--T:134-->
</syntaxhighlight>
</syntaxhighlight>






=== Пользовательские поля в интерфейсе ===
=== Пользовательские поля в интерфейсе === <!--T:135-->


<!--T:136-->
Чтобы дать пользователю возможность вводить точные значения параметров (уставки) из интерфейса, можно воспользоваться [https://wirenboard.com/wiki/index.php/%D0%A1%D0%BE%D0%B7%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5_%D1%80%D0%B5%D0%B4%D0%B0%D0%BA%D1%82%D0%B8%D1%80%D1%83%D0%B5%D0%BC%D1%8B%D1%85_%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D1%81%D0%BA%D0%B8%D1%85_%D0%BF%D0%BE%D0%BB%D0%B5%D0%B9_%D0%B2_%D0%B2%D0%B5%D0%B1-%D0%B8%D0%BD%D1%82%D0%B5%D1%80%D1%84%D0%B5%D0%B9%D1%81%D0%B5 инструкцией].
Чтобы дать пользователю возможность вводить точные значения параметров (уставки) из интерфейса, можно воспользоваться [https://wirenboard.com/wiki/index.php/%D0%A1%D0%BE%D0%B7%D0%B4%D0%B0%D0%BD%D0%B8%D0%B5_%D1%80%D0%B5%D0%B4%D0%B0%D0%BA%D1%82%D0%B8%D1%80%D1%83%D0%B5%D0%BC%D1%8B%D1%85_%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D1%81%D0%BA%D0%B8%D1%85_%D0%BF%D0%BE%D0%BB%D0%B5%D0%B9_%D0%B2_%D0%B2%D0%B5%D0%B1-%D0%B8%D0%BD%D1%82%D0%B5%D1%80%D1%84%D0%B5%D0%B9%D1%81%D0%B5 инструкцией].


<!--T:137-->
Более подробно и с примером - в [https://support.wirenboard.com/t/kak-na-wb5-wb6-sozdat-pole-dlya-vvoda-ustavok-i-peredat-znachenie-v-pravila/2180 теме на портале техподдержки].
Более подробно и с примером - в [https://support.wirenboard.com/t/kak-na-wb5-wb6-sozdat-pole-dlya-vvoda-ustavok-i-peredat-znachenie-v-pravila/2180 теме на портале техподдержки].






=== Сложные правила с расписаниями ===
=== Сложные правила с расписаниями === <!--T:138-->


<!--T:139-->
Объект - продуктовый магазин. Различные системы магазина управляются по обратной связи от датчиков температуры и с учётом расписания работы магазина.
Объект - продуктовый магазин. Различные системы магазина управляются по обратной связи от датчиков температуры и с учётом расписания работы магазина.


<!--T:140-->
Для расписаний используются не cron-правила, а обёртка над ними. Обёртка включает и выключает правила, которые, в отличие от cron-правил, выполняются постоянно, будучи включенными.
Для расписаний используются не cron-правила, а обёртка над ними. Обёртка включает и выключает правила, которые, в отличие от cron-правил, выполняются постоянно, будучи включенными.


<!--T:141-->
Например, мы хотим, чтобы освещение было включено с 10 до 17ч. Обёртка (libschedule) будет выполнять правило "включить освещение" раз в минуту с 10 утра до 17 вечера.
Например, мы хотим, чтобы освещение было включено с 10 до 17ч. Обёртка (libschedule) будет выполнять правило "включить освещение" раз в минуту с 10 утра до 17 вечера.


<!--T:142-->
Это значит, что даже если контроллер работает с перерывами и пропустил время перехода между расписаниями (10 утра), то контроллер всё равно включит освещение при первой возможности.
Это значит, что даже если контроллер работает с перерывами и пропустил время перехода между расписаниями (10 утра), то контроллер всё равно включит освещение при первой возможности.




<!--T:143-->
lib_schedules.js:
lib_schedules.js:


<!--T:144-->
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">
global.__proto__.Schedules = {};
global.__proto__.Schedules = {};


<!--T:145-->
(function(Schedules) { // замыкание
(function(Schedules) { // замыкание


   function todayAt(now, hours, minutes) {
   <!--T:146-->
function todayAt(now, hours, minutes) {
     var date = new Date(now);
     var date = new Date(now);
     // i.e. "today, at HH:MM". All dates are in UTC!
     // i.e. "today, at HH:MM". All dates are in UTC!
Строка 736: Строка 865:




   function checkScheduleInterval(now, start_time, end_time) {
   <!--T:147-->
function checkScheduleInterval(now, start_time, end_time) {
     var start_date = todayAt(now, start_time[0], start_time[1]);
     var start_date = todayAt(now, start_time[0], start_time[1]);
     var end_date = todayAt(now, end_time[0], end_time[1]);
     var end_date = todayAt(now, end_time[0], end_time[1]);
     log("checkScheduleInterval {} {} {}".format(now, start_date, end_date));
     log("checkScheduleInterval {} {} {}".format(now, start_date, end_date));


     if (end_date >= start_date) {
     <!--T:148-->
if (end_date >= start_date) {
       if ((now >= start_date) && (now < end_date)) {
       if ((now >= start_date) && (now < end_date)) {
         return true;
         return true;
Строка 749: Строка 880:
       // assuming they belong to a different days (e.g. today and tomorrow)
       // assuming they belong to a different days (e.g. today and tomorrow)


       // option 1: what if it's now the day of "end" date?
       <!--T:149-->
// option 1: what if it's now the day of "end" date?
       // in this case the following is enough:
       // in this case the following is enough:
       if (now < end_date) {
       if (now < end_date) {
Строка 755: Строка 887:
       }
       }


       // well, that seems not to be the case. ok,
       <!--T:150-->
// well, that seems not to be the case. ok,
       // option 2: it's the day of "start" date:
       // option 2: it's the day of "start" date:


       if (now >= start_date) {
       <!--T:151-->
if (now >= start_date) {
         return true;
         return true;
       }
       }
Строка 764: Строка 898:
     return false;
     return false;


   }
   <!--T:152-->
}


   function checkSchedule(schedule, now) {
   <!--T:153-->
function checkSchedule(schedule, now) {
     if (now == undefined) {
     if (now == undefined) {
       now = new Date();
       now = new Date();
     }
     }


     for (var i = 0; i < schedule.intervals.length; ++i) {
     <!--T:154-->
for (var i = 0; i < schedule.intervals.length; ++i) {
       var item = schedule.intervals[i];
       var item = schedule.intervals[i];
       if (checkScheduleInterval(now, item[0], item[1])) {
       if (checkScheduleInterval(now, item[0], item[1])) {
Строка 786: Строка 923:
   };
   };


   function addScheduleDevCronTasks(schedule) {
   <!--T:155-->
function addScheduleDevCronTasks(schedule) {
     for (var i = 0; i < schedule.intervals.length; ++i) {
     for (var i = 0; i < schedule.intervals.length; ++i) {
       var interval = schedule.intervals[i];
       var interval = schedule.intervals[i];
Строка 804: Строка 942:
   }
   }


   function addScheduleAutoUpdCronTask(schedule) {
   <!--T:156-->
function addScheduleAutoUpdCronTask(schedule) {
     defineRule("_schedule_auto_upd_{}".format(schedule.name), {
     defineRule("_schedule_auto_upd_{}".format(schedule.name), {
       when: cron("@every " + schedule.autoUpdate),
       when: cron("@every " + schedule.autoUpdate),
Строка 813: Строка 952:
   }
   }


   var _schedules = {};
   <!--T:157-->
var _schedules = {};


   Schedules.registerSchedule = function(schedule) {
   <!--T:158-->
Schedules.registerSchedule = function(schedule) {
     _schedules[schedule.name] = schedule;
     _schedules[schedule.name] = schedule;
   };
   };


   Schedules.initSchedules = function() {
   <!--T:159-->
Schedules.initSchedules = function() {
     var params = {
     var params = {
       title: "Schedule Status",  
       title: "Schedule Status",  
Строка 825: Строка 967:
     };
     };


     for (var schedule_name in _schedules) {
     <!--T:160-->
for (var schedule_name in _schedules) {
       if (_schedules.hasOwnProperty(schedule_name)) {
       if (_schedules.hasOwnProperty(schedule_name)) {
         var schedule = _schedules[schedule_name];
         var schedule = _schedules[schedule_name];
Строка 832: Строка 975:
     };
     };


     defineVirtualDevice("_schedules", params);
     <!--T:161-->
defineVirtualDevice("_schedules", params);






     for (var schedule_name in _schedules) {
     <!--T:162-->
for (var schedule_name in _schedules) {
       if (_schedules.hasOwnProperty(schedule_name)) {
       if (_schedules.hasOwnProperty(schedule_name)) {
         var schedule = _schedules[schedule_name];
         var schedule = _schedules[schedule_name];


         // setup cron tasks which updates the schedule dev status at schedule
         <!--T:163-->
// setup cron tasks which updates the schedule dev status at schedule
         //  interval beginings and ends
         //  interval beginings and ends
         addScheduleDevCronTasks(schedule);
         addScheduleDevCronTasks(schedule);


         // if needed, setup periodic task to trigger rules which use this schedule
         <!--T:164-->
// if needed, setup periodic task to trigger rules which use this schedule
         if (schedule.autoUpdate) {
         if (schedule.autoUpdate) {
           addScheduleAutoUpdCronTask(schedule);
           addScheduleAutoUpdCronTask(schedule);
         }
         }


         // set schedule dev status as soon as possible at startup
         <!--T:165-->
// set schedule dev status as soon as possible at startup
         (function(schedule) {
         (function(schedule) {
           setTimeout(function() {
           setTimeout(function() {
Строка 856: Строка 1004:
         })(schedule);
         })(schedule);


       };
       <!--T:166-->
};
     };
     };


      
      
   };
   <!--T:167-->
};


<!--T:168-->
})(Schedules);
})(Schedules);
</syntaxhighlight>
</syntaxhighlight>


<!--T:169-->
Пример правил, с использованием Schedules:
Пример правил, с использованием Schedules:
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">
(function() { // замыкание
(function() { // замыкание


   defineAlias("countersTemperature", "wb-msw2_30/Temperature");
   <!--T:170-->
defineAlias("countersTemperature", "wb-msw2_30/Temperature");
   defineAlias("vegetablesTemperature", "wb-msw2_31/Temperature");
   defineAlias("vegetablesTemperature", "wb-msw2_31/Temperature");


   defineAlias("heater1EnableInverted", "wb-mrm2-old_70/Relay 1");
   <!--T:171-->
defineAlias("heater1EnableInverted", "wb-mrm2-old_70/Relay 1");
   defineAlias("frontshopVentInverted", "wb-gpio/EXT1_R3A3");
   defineAlias("frontshopVentInverted", "wb-gpio/EXT1_R3A3");


   Schedules.registerSchedule({
   <!--T:172-->
Schedules.registerSchedule({
     "name" : "signboard", // вывеска
     "name" : "signboard", // вывеска
     "autoUpdate" : "1m",
     "autoUpdate" : "1m",
Строка 919: Строка 1074:
   Schedules.initSchedules();
   Schedules.initSchedules();


   // Вывеска и фасадное освещение
   <!--T:173-->
// Вывеска и фасадное освещение
   defineRule("signboardOnOff", {
   defineRule("signboardOnOff", {
     when: function() {
     when: function() {
Строка 935: Строка 1091:




   // Освещение торгового зала
   <!--T:174-->
// Освещение торгового зала
   defineRule("lightingFrontshopOnOff", {
   defineRule("lightingFrontshopOnOff", {
     when: function() {
     when: function() {
Строка 947: Строка 1104:




   // Вентиляция подсобного помещения
   <!--T:175-->
// Вентиляция подсобного помещения
   defineRule("ventBackstoreOnOff", {
   defineRule("ventBackstoreOnOff", {
     when: function() {
     when: function() {
Строка 960: Строка 1118:
   });
   });


   // Освещение холодильных горок
   <!--T:176-->
// Освещение холодильных горок
   defineRule("lightingCoolingshelfsOnOff", {
   defineRule("lightingCoolingshelfsOnOff", {
     when: function() {
     when: function() {
Строка 969: Строка 1128:
       var on = dev._schedules.working_hours_15m;
       var on = dev._schedules.working_hours_15m;


       // освещение в горках через нормально-закрытые реле (инвертировано)
       <!--T:177-->
// освещение в горках через нормально-закрытые реле (инвертировано)
       dev["wb-mrm2-old_60/Relay 1"] = !on;
       dev["wb-mrm2-old_60/Relay 1"] = !on;
       dev["wb-mrm2-old_61/Relay 1"] = !on;
       dev["wb-mrm2-old_61/Relay 1"] = !on;
Строка 982: Строка 1142:




   //Брендовые холодильники (пиво, лимонады)
   <!--T:178-->
//Брендовые холодильники (пиво, лимонады)
   defineRule("powerBrandFridgesOnOff", {
   defineRule("powerBrandFridgesOnOff", {
     when: function() {
     when: function() {
Строка 996: Строка 1157:




   // ========= Котлы и приточная вентиляция ТЗ  ===========
   <!--T:179-->
// ========= Котлы и приточная вентиляция ТЗ  ===========
   // обратная связь по температуре овощной зоны
   // обратная связь по температуре овощной зоны


   // днём работает позиционный регулятор
   <!--T:180-->
// днём работает позиционный регулятор
   defineRule("heatersDayOff", {
   defineRule("heatersDayOff", {
     when: function() {
     when: function() {
Строка 1010: Строка 1173:
   });
   });


   defineRule("heatersDayOn", {
   <!--T:181-->
defineRule("heatersDayOn", {
     when: function() {
     when: function() {
       return (dev._schedules.heaters_schedule) && (vegetablesTemperature < 16.7);
       return (dev._schedules.heaters_schedule) && (vegetablesTemperature < 16.7);
Строка 1020: Строка 1184:
   });
   });


   // ночью работает позиционный регулятор
   <!--T:182-->
// ночью работает позиционный регулятор
   defineRule("heatersNightOff", {
   defineRule("heatersNightOff", {
     when: function() {
     when: function() {
Строка 1031: Строка 1196:
   });
   });


   defineRule("heatersNightOn", {
   <!--T:183-->
defineRule("heatersNightOn", {
     when: function() {
     when: function() {
       return (!dev._schedules.heaters_schedule) && (vegetablesTemperature < 11.3);
       return (!dev._schedules.heaters_schedule) && (vegetablesTemperature < 11.3);
Строка 1042: Строка 1208:




   // приточная и вытяжная вентиляция принудительно выключены
   <!--T:184-->
// приточная и вытяжная вентиляция принудительно выключены
    
    
   defineRule("ventFrontshopAlwaysOff", {
   defineRule("ventFrontshopAlwaysOff", {
Строка 1055: Строка 1222:
   // ==================  Кассовая зона =================
   // ==================  Кассовая зона =================


   // в кассовой зоне в рабочее время температура поддерживается кондиционерами (позиционный регулятор)
   <!--T:185-->
// в кассовой зоне в рабочее время температура поддерживается кондиционерами (позиционный регулятор)


   defineRule("countersACOn", {
   <!--T:186-->
defineRule("countersACOn", {
     when: function() {
     when: function() {
       return (dev._schedules.working_hours_15m) && (countersTemperature < 17.7);
       return (dev._schedules.working_hours_15m) && (countersTemperature < 17.7);
Строка 1067: Строка 1236:
   });
   });


   // в нерабочее время кондиционер выключен
   <!--T:187-->
// в нерабочее время кондиционер выключен
   defineRule("countersACOff", {
   defineRule("countersACOff", {
     when: function() {
     when: function() {
Строка 1078: Строка 1248:
   });
   });


   // =============== Овощная зона ==============
   <!--T:188-->
// =============== Овощная зона ==============
   // Охлаждение овощей кондиционером только при температуре воздуха выше 18.5C   
   // Охлаждение овощей кондиционером только при температуре воздуха выше 18.5C   


   defineRule("acVegOn", {
   <!--T:189-->
defineRule("acVegOn", {
     when: function() {
     when: function() {
       return vegetablesTemperature >= 18.5
       return vegetablesTemperature >= 18.5
Строка 1091: Строка 1263:
   });
   });


   defineRule("acVegOff", {
   <!--T:190-->
defineRule("acVegOff", {
     when: function() {
     when: function() {
       return vegetablesTemperature < 17.8
       return vegetablesTemperature < 17.8
Строка 1105: Строка 1278:




== Полное описание возможностей движка правил ==
== Полное описание возможностей движка правил == <!--T:191-->


<!--T:192-->
Самое полное описание движка правил: https://github.com/contactless/wb-rules/blob/master/README.md
Самое полное описание движка правил: https://github.com/contactless/wb-rules/blob/master/README.md






== Новые возможности последних версий ==
== Новые возможности последних версий == <!--T:193-->


<!--T:194-->
* [[Special:MyLanguage/Движок_правил_wb-rules_1.7|Движок правил wb-rules 1.7]]
* [[Special:MyLanguage/Движок_правил_wb-rules_1.7|Движок правил wb-rules 1.7]]






== В разработке ==
== В разработке == <!--T:195-->


<!--T:196-->
Описание возможностей будущих версий движка правил можно прочесть здесь:
Описание возможностей будущих версий движка правил можно прочесть здесь:
* [[Special:MyLanguage/Движок_правил_wb-rules_2.0|Движок правил wb-rules 2.0]]
* [[Special:MyLanguage/Движок_правил_wb-rules_2.0|Движок правил wb-rules 2.0]]


</translate>
</translate>
12 063

правки