Rule Examples: различия между версиями

Материал из Wiren Board
 
(не показано 37 промежуточных версий 6 участников)
Строка 1: Строка 1:
{{DISPLAYTITLE: Примеры правил}}
<languages/>
<languages/>
<translate>
<translate>
=== Слежение за контролом === <!--T:20-->
<!--T:255-->
{{DISPLAYTITLE: Примеры правил}}
 
== Общая информация ==
Здесь вы найдёте учебные примеры скриптов, написанных для движка правил '''[[wb-rules| wb-rules]]'''.
 
Алгоритмы в примерах предельно просты и не учитывают многих факторов которые могут возникнуть в реальности. Поэтому используйте эту библиотеку только как учебный материал, а не источник готовых скриптов для реальных проектов.
 
== Виртуальное устройство ==
 
Виртуальное устройство можно использовать для объединения каналов, задания особой логики для устройства или просто так для красоты.
 
Пример ниже создаст виртуальное устройство с именем '''deviceName''' и двумя контролами '''value''' и '''state'''. А благодаря правилу с '''whenChanged''', значение контрола '''state''' будет менять в зависимости от значение контрола '''value'''.
 
<syntaxhighlight lang="ecmascript">
deviceName = 'my-virtual-device';
 
defineVirtualDevice(deviceName, {
    title: {'en': 'My Virtual Device', 'ru': 'Мое виртуальное устройство'} ,
    cells: {
      value: {
        title: {'en': 'Value', 'ru': 'Значение'},
        type: "range",
        value: 1,
        max: 3,
        min: 1
      },
      state: {
        title: {'en': 'State', 'ru': 'Состояние'},
        type: "value",
        value: 1,
        enum:{
          1: {'en': 'Normal', 'ru': 'В норме'},
          2: {'en': 'Warning', 'ru': 'Внимание'},
          3: {'en': 'Crash', 'ru': 'Авария'}} 
      },
    }
});
 
defineRule({
  whenChanged: deviceName+"/value",
  then: function (newValue, devName, cellName) {
dev[deviceName+"/state"] = newValue;
  }
});
 
</syntaxhighlight>
 
== Слежение за контролом == <!--T:20-->


<!--T:21-->
<!--T:21-->
Строка 11: Строка 58:


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


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


<!--T:25-->
<!--T:25-->
Строка 20: Строка 67:


<!--T:26-->
<!--T:26-->
defineRule("motion_detector", {
defineRule({
   whenChanged: "wb-gpio/D1_IN",
   whenChanged: "wb-gpio/D1_IN",
   then: function (newValue, devName, cellName) {
   then: function (newValue, devName, cellName) {
dev["wb-gpio"]["Relay_2"] = newValue;
dev["wb-gpio/Relay_2"] = newValue;
dev["wb-mrm2_6"]["Relay 1"] = newValue;
dev["wb-mrm2_6/Relay 1"] = newValue;


   <!--T:27-->
   <!--T:27-->
Строка 52: Строка 99:
   whenChanged: "simple_test/enabled",
   whenChanged: "simple_test/enabled",
   then: function (newValue, devName, cellName) {
   then: function (newValue, devName, cellName) {
dev["wb-gpio"]["Relay_2"] = newValue;
dev["wb-gpio/Relay_2"] = newValue;
dev["wb-mrm2_6"]["Relay 1"] = newValue;
dev["wb-mrm2_6/Relay 1"] = newValue;


   <!--T:32-->
   <!--T:32-->
Строка 60: Строка 107:
</syntaxhighlight>
</syntaxhighlight>


=== Детектор движения c таймаутом === <!--T:34-->
== Мастер-выключатель с восстановлением последнего состояния == <!--T:250-->
 
На вход контроллера подключен мастер-выключатель, который, при переключении, отключает все устройства, указанные в соответствующем правиле. При повторном нажатии на выключатель, устройствам возвращается первоначальное состояние.
 
Подключение осуществляется к контакту A1 и 5V на контроллере. При замыкании на соответствующем канале <code>wb-gpio/A1_IN</code>, состояние меняется, и срабатывает правило.
 
Для управления через веб-интерфейс создано виртуальное устройство, отображаемое на вкладке '''Устройства'''.
 
Первоначальные состояния устройств сохраняются в [https://github.com/wirenboard/wb-rules#%D0%BF%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%BD%D0%BE%D0%B5-%D1%85%D1%80%D0%B0%D0%BD%D0%B8%D0%BB%D0%B8%D1%89%D0%B5 постоянном хранилище]. Переменные в постоянном хранилище записываются на флеш-память, что обеспечивает доступ к ним после перезагрузки контроллера.
 
<syntaxhighlight lang="ecmascript">
defineVirtualDevice("power_off", {
    title: "Мастер-выключатель",
    cells: {
        power_off: {
            type: "pushbutton"
        },
    }
});
 
var ps = new PersistentStorage("power-storage", { global: true });
var lights = ["wb-mdm3_50/K1", "wb-mdm3_50/K2", "wb-mdm3_50/K3"];
 
var isPowerOff = true;
 
defineRule({
    whenChanged: ["wb-gpio/A1_IN", "power_off/power_off"],
    then: function (newValue, devName, cellName) {
        if (isPowerOff) {
            lights.forEach(function (light) {
                ps[light] = dev[light];
                dev[light] = false;
            });
        } else {
            lights.forEach(function (light) {
                dev[light] = ps[light];
            });
        }
        isPowerOff = !isPowerOff;
    }
});
</syntaxhighlight>
 
== Детектор движения c таймаутом == <!--T:34-->


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


<!--T:37-->
<!--T:37-->
Строка 70: Строка 160:
<!--T:36-->
<!--T:36-->
Правило работает так:
Правило работает так:
* когда движение появляется, свет включается. Если ранее был запущен тридцатисекундный таймер "на выключение", этот таймер отключается;
* когда движение появляется, свет включается. Если ранее был запущен тридцатисекундный таймер «на выключение», этот таймер отключается;
* когда движение пропадает, запускается тридцатисекундный таймер "на выключение". Если ему удаётся дойти до конца, свет выключается.
* когда движение пропадает, запускается тридцатисекундный таймер «на выключение». Если ему удаётся дойти до конца, свет выключается.


<!--T:38-->
<!--T:38-->
Строка 78: Строка 168:
var motion_timer_1_id = null;
var motion_timer_1_id = null;


<!--T:191-->
defineRule("motion_detector_1", {
defineRule("motion_detector_1", {
     whenChanged: "wb-gpio/D2_IN",
     whenChanged: "wb-gpio/D2_IN",
     then: function (newValue, devName, cellName) {
     then: function (newValue, devName, cellName) {
         if (newValue) {
         if (newValue) {
             dev["wb-gpio"]["Relay_1"] = true;
             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"]["Relay_1"] = false;
                 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);
Строка 95: Строка 186:
</syntaxhighlight>
</syntaxhighlight>


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


<!--T:44-->
<!--T:44-->
Строка 108: Строка 199:
       then: function(newValue, devName, cellName) {
       then: function(newValue, devName, cellName) {
           if (!newValue) {
           if (!newValue) {
               dev["wb-gpio"][relay_control] = true;
               dev["wb-gpio/relay_control"] = true;
               if (motion_timer_id) {
               if (motion_timer_id) {
                   clearTimeout(motion_timer_id);
                   clearTimeout(motion_timer_id);
Строка 115: Строка 206:
               <!--T:46-->
               <!--T:46-->
motion_timer_id = setTimeout(function() {
motion_timer_id = setTimeout(function() {
                   dev["wb-gpio"][relay_control] = false;
                   dev["wb-gpio/relay_control"] = false;
                   motion_timer_id = null;
                   motion_timer_id = null;
               }, timeout_ms);
               }, timeout_ms);
Строка 129: Строка 220:
</syntaxhighlight>
</syntaxhighlight>


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


<!--T:49-->
<!--T:49-->
Строка 160: Строка 251:
     if ((date > date_start) && (date < date_end)) {
     if ((date > date_start) && (date < date_end)) {
       if (newValue) {
       if (newValue) {
           dev["wb-gpio"]["EXT1_R3A1"] = 1;
           dev["wb-gpio/EXT1_R3A1"] = 1;
    
    
           if (motion_timer_1_id) {
           if (motion_timer_1_id) {
Строка 167: Строка 258:
    
    
           motion_timer_1_id = setTimeout(function () {
           motion_timer_1_id = setTimeout(function () {
             dev["wb-gpio"]["EXT1_R3A1"] = 0;             
             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);               
Строка 176: Строка 267:
</syntaxhighlight>
</syntaxhighlight>


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


<!--T:54-->
<!--T:54-->
Строка 209: Строка 300:
   defineRule( "roller_shutter_up_on" + suffix, {
   defineRule( "roller_shutter_up_on" + suffix, {
   asSoonAs: function() {
   asSoonAs: function() {
     return dev[relay_up_device][relay_up_control];
     return dev["relay_up_device/relay_up_control"];
   },
   },
     then: function () {
     then: function () {
Строка 218: Строка 309:
       <!--T:59-->
       <!--T:59-->
relay_up_timer_id = setTimeout(function() {
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);
     }
     }
Строка 226: Строка 317:
defineRule("roller_shutter_down_on" + suffix, {
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"];
     },
     },
     then: function () {
     then: function () {
Строка 234: Строка 325:
        
        
       relay_down_timer_id = setTimeout(function() {
       relay_down_timer_id = setTimeout(function() {
         dev[relay_down_device][relay_down_control] = 0;
         dev["relay_down_device/relay_down_control"] = 0;
       }, timeout_s * 1000);
       }, timeout_s * 1000);
     }
     }
Строка 242: Строка 333:
defineRule("roller_shutter_both_on" + suffix, {
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"];
     },
     },
     then: function () {
     then: function () {
Строка 257: Строка 348:
       <!--T:63-->
       <!--T:63-->
dev[relay_up_device][relay_up_control] = 0;
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");
     }
     }
Строка 311: Строка 402:
})();
})();


<!--T:69-->
</syntaxhighlight>
</syntaxhighlight>
== Импульсные счетчики ==
== Импульсные счетчики == <!--T:69-->


<!--T:192-->
Импульсный счетчик подключен к WB-MCM8. Выдает 1 импульс на 10 литров воды. При подключении на счетчике были показания 123.120 м³, что равно 123120 литрам воды. У WB-MCM8 при подключении было насчитано 7 импульсов.
Импульсный счетчик подключен к WB-MCM8. Выдает 1 импульс на 10 литров воды. При подключении на счетчике были показания 123.120 м³, что равно 123120 литрам воды. У WB-MCM8 при подключении было насчитано 7 импульсов.


<!--T:193-->
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">
var meterCorrection = 123120 // Корректировочное значение счетчика в литрах
var meterCorrection = 123120 // Корректировочное значение счетчика в литрах
Строка 322: Строка 414:
var inpulseValue = 10 // Количество литров на один импульс
var inpulseValue = 10 // Количество литров на один импульс


<!--T:194-->
defineVirtualDevice("water_meters", { // Создаем виртуальный девайс для отображения в веб интерфейсе.
defineVirtualDevice("water_meters", { // Создаем виртуальный девайс для отображения в веб интерфейсе.
     title: "Счетчики воды",
     title: "Счетчики воды",
Строка 332: Строка 425:
});
});


<!--T:195-->
defineRule("water_meter_1", {
defineRule("water_meter_1", {
     whenChanged: "wb-mcm8_29/Input 1 counter",
     whenChanged: "wb-mcm8_29/Input 1 counter",
     then: function(newValue, devName, cellName) {
     then: function(newValue, devName, cellName) {
       if(newValue){
       if(newValue){
       dev["water_meters"]["water_meter_1"] = ((parseInt(newValue) - counterCorrection) * inpulseValue) + meterCorrection; // Умножаем значение счетчика на количество литров/импульс и прибавляем корректировочное значение.
       dev["water_meters/water_meter_1"] = ((parseInt(newValue) - counterCorrection) * inpulseValue) + meterCorrection; // Умножаем значение счетчика на количество литров/импульс и прибавляем корректировочное значение.
       }
       }
     }
     }
Строка 342: Строка 436:
</syntaxhighlight>
</syntaxhighlight>


== Обработка счётчиков нажатий ==
== Инвертирование значения контрола ==
[[Image:wb-rules-ex-buzzer-invert.png|300px|thumb|right|Пример устройств с вкладки Устройства]]
Правило ниже создаёт виртуальное устройство ''my-invert-buzzer'', с контролом ''disabled'', который инвертирует состояние контрола ''enabled'' системной пищалки ''Buzzer''.
<syntaxhighlight lang="ecmascript">
defineVirtualDevice('my-invert-buzzer', {
    title: 'Buzzer Invert' ,
    cells: {
      Disabled: {
        title: "disabled",
    type: "switch",
    value: !dev["buzzer/enabled"]
    }
    }
})
 
defineRule({
    whenChanged: ["buzzer/enabled"],
    then: function(newValue, devName, cellName) {
      dev["my-invert-buzzer/Disabled"] = !newValue;
    }
});
 
defineRule({
    whenChanged: ["my-invert-buzzer/Disabled"],
    then: function(newValue, devName, cellName){
        dev["buzzer/enabled"] = !newValue;
    }
});
</syntaxhighlight>
 
== Обработка счётчиков нажатий == <!--T:196-->
{{Anchor|press-actions}}
{{Anchor|press-actions}}
=== Описание ===
=== Описание ===
Последние версии прошивок устройств Wiren Board могут распознавать типы нажатий подключённых к входам кнопок и транслировать их по [[Modbus]] на контроллер Wiren Board. О том, как устройство распознаёт типы нажатий, читайте в его документации.  
Последние версии прошивок устройств Wiren Board могут распознавать типы нажатий подключённых к входам кнопок и транслировать их по [[Modbus]] на контроллер Wiren Board. О том, как устройство распознаёт типы нажатий, читайте в его документации.  


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


<!--T:198-->
Обработку счётчиков удобно делать на [[wb-rules]], но вы можете использовать любой инструмент для автоматизации, например, [[Node-RED]]. Чтобы ускорить опрос счетчиков, настройте [[RS-485:Configuration via Web Interface#poll-period |период опроса]].
Обработку счётчиков удобно делать на [[wb-rules]], но вы можете использовать любой инструмент для автоматизации, например, [[Node-RED]]. Чтобы ускорить опрос счетчиков, настройте [[RS-485:Configuration via Web Interface#poll-period |период опроса]].


=== Примеры ===
=== Примеры === <!--T:199-->
{{YouTube
{{YouTube
|link=https://youtu.be/C60KB7TCeKg
|link=https://youtu.be/C60KB7TCeKg
Строка 362: Строка 488:
# Короткое, а затем длинное — уменьшает яркость.
# Короткое, а затем длинное — уменьшает яркость.


<!--T:200-->
Так как изменение яркости требует растянутое во времени действие, то мы используем таймер. Также мы контролируем состояние входа с кнопкой и прекращаем действие при отпускании кнопки.
Так как изменение яркости требует растянутое во времени действие, то мы используем таймер. Также мы контролируем состояние входа с кнопкой и прекращаем действие при отпускании кнопки.


<!--T:201-->
<syntaxhighlight lang="js">
<syntaxhighlight lang="js">
/* ---------------------------- */
/* ---------------------------- */
Строка 369: Строка 497:
/* ---------------------------- */
/* ---------------------------- */


<!--T:202-->
defineRule({
defineRule({
   whenChanged: "wb-mcm8_20/Input 1 Single Press Counter",
   whenChanged: "wb-mcm8_20/Input 1 Single Press Counter",
Строка 377: Строка 506:




<!--T:203-->
/* ---------------------------- */
/* ---------------------------- */
/* 2. Double Press Counter: Off action*/
/* 2. Double Press Counter: Off action*/
/* ---------------------------- */
/* ---------------------------- */


<!--T:204-->
defineRule({
defineRule({
   whenChanged: "wb-mcm8_20/Input 1 Double Press Counter",
   whenChanged: "wb-mcm8_20/Input 1 Double Press Counter",
Строка 389: Строка 520:




<!--T:205-->
/* --------------------------------------- */
/* --------------------------------------- */
/* 3. Long Press Counter: Increase brightness */
/* 3. Long Press Counter: Increase brightness */
/* --------------------------------------- */
/* --------------------------------------- */


<!--T:206-->
defineRule({
defineRule({
   whenChanged: "wb-mcm8_20/Input 1 Long Press Counter",
   whenChanged: "wb-mcm8_20/Input 1 Long Press Counter",
Строка 401: Строка 534:
});
});


<!--T:207-->
// A rule that will increase the brightness on a timer
// A rule that will increase the brightness on a timer
defineRule({
defineRule({
Строка 407: Строка 541:
     var i = dev["wb-mdm3_58/Channel 1"];
     var i = dev["wb-mdm3_58/Channel 1"];


     if (i < 100 && dev["wb-mcm8_20/Input 1"]) {
     <!--T:208-->
if (i < 100 && dev["wb-mcm8_20/Input 1"]) {
       i++
       i++
       dev["wb-mdm3_58/Channel 1"] = i
       dev["wb-mdm3_58/Channel 1"] = i
Строка 417: Строка 552:




<!--T:209-->
/* -------------------------------------------- */
/* -------------------------------------------- */
/* 4. Shortlong Press Counter: Decrease brightness */
/* 4. Shortlong Press Counter: Decrease brightness */
/* -------------------------------------------- */
/* -------------------------------------------- */


<!--T:210-->
defineRule({
defineRule({
   whenChanged: "wb-mcm8_20/Input 1 Shortlong Press Counter",
   whenChanged: "wb-mcm8_20/Input 1 Shortlong Press Counter",
Строка 429: Строка 566:
});
});


<!--T:211-->
// A rule that will decrease the brightness on a timer
// A rule that will decrease the brightness on a timer
defineRule({
defineRule({
Строка 435: Строка 573:
     var i = dev["wb-mdm3_58/Channel 1"];
     var i = dev["wb-mdm3_58/Channel 1"];


     if (i > 0 && dev["wb-mcm8_20/Input 1"]) {
     <!--T:212-->
if (i > 0 && dev["wb-mcm8_20/Input 1"]) {
       i--
       i--
       dev["wb-mdm3_58/Channel 1"] = i
       dev["wb-mdm3_58/Channel 1"] = i
Строка 444: Строка 583:
});
});


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


=== Универсальный модуль для wb-rules ===
=== Универсальный модуль для wb-rules === <!--T:214-->
Мы написали модуль для wb-rules [https://github.com/wirenboard/wb-community/tree/main/wb-press-actions wb-press-actions], который облегчает обработку нажатий в ваших скриптах.
Мы написали модуль для wb-rules [https://github.com/wirenboard/wb-community/tree/main/scripts/wb-press-actions wb-press-actions], который облегчает обработку нажатий в ваших скриптах.


== Датчик MSW v.3 ==
== Датчик MSW v.3 == <!--T:215-->


<!--T:216-->
При подключении датчика WB-MSW v.3 к контроллеру Wiren Board есть возможность создавать интересные сценарии, используя данные с датчика. На пример Включать свет по движению, сигнализировать светодиодами о превышении значения СО2 или VOC, Включать Кондиционер, если жарко или увлажнитель воздуха, если воздух слишком сухой. Правила создаются индивидуально под задачи. Здесь мы приведем несколько примеров для понимания принципа работы с датчиком. Больше примеров написания правил можно найти в документации '''[[Движок правил wb-rules]]'''.
При подключении датчика WB-MSW v.3 к контроллеру Wiren Board есть возможность создавать интересные сценарии, используя данные с датчика. На пример Включать свет по движению, сигнализировать светодиодами о превышении значения СО2 или VOC, Включать Кондиционер, если жарко или увлажнитель воздуха, если воздух слишком сухой. Правила создаются индивидуально под задачи. Здесь мы приведем несколько примеров для понимания принципа работы с датчиком. Больше примеров написания правил можно найти в документации '''[[Движок правил wb-rules]]'''.


=== CO2 ===
=== CO2 === <!--T:217-->
При концентрации CO2 меньше 650 - раз в 10 секунд мигаем зеленым.
При концентрации CO2 меньше 650 - раз в 10 секунд мигаем зеленым.


<!--T:218-->
При концентрации CO2 свыше 651, но меньше 1000 - раз в 5 секунд мигаем желтым.
При концентрации CO2 свыше 651, но меньше 1000 - раз в 5 секунд мигаем желтым.


<!--T:219-->
При концентрации CO2 свыше 1001 - раз в секунду мигаем красным.
При концентрации CO2 свыше 1001 - раз в секунду мигаем красным.
<div class="mw-collapsible mw-collapsed"; style="width:600px; overflow: hidden;">
<div class="mw-collapsible mw-collapsed"; style="width:600px; overflow: hidden;">
Строка 469: Строка 612:


         if (co2_good) {
         if (co2_good) {
             dev[devName]["Green LED"] = true;
             dev[devName+"/Green LED"] = true;
             dev[devName]["Red LED"] = false;
             dev[devName+"/Red LED"] = false;
             dev[devName]["LED Period (s)"] = 10;
             dev[devName+"/LED Period (s)"] = 10;
         }
         }
         if (co2_middle) {
         if (co2_middle) {
             dev[devName]["Green LED"] = true;
             dev[devName+"/Green LED"] = true;
             dev[devName]["Red LED"] = true;
             dev[devName+"/Red LED"] = true;
             dev[devName]["LED Period (s)"] = 5;
             dev[devName+"/LED Period (s)"] = 5;
         }
         }
         if (co2_bad) {
         if (co2_bad) {
             dev[devName]["Green LED"] = false;
             dev[devName+"/Green LED"] = false;
             dev[devName]["Red LED"] = true;
             dev[devName+"/Red LED"] = true;
             dev[devName]["LED Period (s)"] = 1;
             dev[devName+"/LED Period (s)"] = 1;
         }
         }
     }
     }
});
});
</syntaxhighlight>
</syntaxhighlight>
Но когда устройств/правил много их целесообразно создавать одной функцией, передавая в нее разные параметры:
<syntaxhighlight lang="ecmascript">
function ruleCO2 (devCO2, minCO2, maxCO2){
  log.debug("rule create", devCO2)
  defineRule ("ruleCO2"+devCO2, {
      whenChanged: devCO2+"/CO2",
      then: function(newValue, devName, cellName) {
        log.info("ruleCO2 " + devCO2 +" enter with", newValue)
        if (newValue < minCO2) {
          dev[devCO2+"/LED Glow Duration (ms)"] = 50;
          dev[devCO2+"/Green LED"] = true;
          dev[devCO2+"/Red LED"] = false;
          dev[devCO2+"/LED Period (s)"] = 3;
        }
        if ((newValue > minCO2) && (newValue < maxCO2)) {
          dev[devCO2+"/LED Glow Duration (ms)"] = 50;           
          dev[devCO2+"/Green LED"] = true;
          dev[devCO2+"/Red LED"] = true;
          dev[devCO2+"/LED Period (s)"] = 2;
        }
        if (newValue > maxCO2) {
          dev[devCO2+"/LED Glow Duration (ms)"] = 50;           
          dev[devCO2+"/Green LED"] = false;
          dev[devCO2+"/Red LED"] = true;
          dev[devCO2+"/LED Period (s)"] = 1;
        }
      }
  });
}
ruleCO2("wb-msw-v3_97", 650, 1000);
ruleCO2("wb-msw-v3_98", 650, 1000);
ruleCO2("wb-msw-v3_11", 500, 700);
</syntaxhighlight>
</div>
</div>
=== Max Motion ===
 
=== Max Motion === <!--T:220-->
"Max Motion" - максимальное значение датчика движения за N время. Время от 1 до 60 секунд можно выставить в 282 регистре. По умолчанию 10 секунд. При достижении Max Motion значения 50 проверяем достаточно ли освещена комната, если нет - включаем свет. Как только значение Max Motion упадет ниже 50 свет выключаем.
"Max Motion" - максимальное значение датчика движения за N время. Время от 1 до 60 секунд можно выставить в 282 регистре. По умолчанию 10 секунд. При достижении Max Motion значения 50 проверяем достаточно ли освещена комната, если нет - включаем свет. Как только значение Max Motion упадет ниже 50 свет выключаем.


<!--T:221-->
<div class="NavFrame">
<div class="NavFrame">
   <div class="NavContent">
   <div class="NavContent">
Строка 497: Строка 680:
     then: function(newValue, devName, cellName) {
     then: function(newValue, devName, cellName) {
         if (newValue > 50) {
         if (newValue > 50) {
             if (dev["wb-msw-v3_97"]["Illuminance"] < 50) {
             if (dev["wb-msw-v3_97/Illuminance"] < 50) {
                 dev["wb-mr3_11"]["K1"] = true;
                 dev["wb-mr3_11/K1"] = true;
             }
             }
         } else {
         } else {
             dev["wb-mr3_11"]["K1"] = false;
             dev["wb-mr3_11/K1"] = false;
         }
         }
     }
     }
Строка 511: Строка 694:
== Системные правила == <!--T:70-->
== Системные правила == <!--T:70-->


<!--T:222-->
Многие показания, которые видны в веб-интерфейсе контроллера из коробки, тоже создаются правилами на движке wb-rules. Их код находится здесь: [https://github.com/wirenboard/wb-rules-system https://github.com/wirenboard/wb-rules-system]. Системные правила собраны в пакет <code>wb-rules-system</code>, сами файлы скриптов на контроллере находятся в папке <code>/usr/share/wb-rules-system/</code>.  
Многие показания, которые видны в веб-интерфейсе контроллера из коробки, тоже создаются правилами на движке wb-rules. Их код находится здесь: [https://github.com/wirenboard/wb-rules-system https://github.com/wirenboard/wb-rules-system]. Системные правила собраны в пакет <code>wb-rules-system</code>, сами файлы скриптов на контроллере находятся в папке <code>/usr/share/wb-rules-system/</code>.  


Строка 642: Строка 826:
     whenChanged: "wb-adc/Vin",
     whenChanged: "wb-adc/Vin",
     then: function() {
     then: function() {
         if (dev["wb-adc"]["Vin"] < dev["wb-adc"]["BAT"] ) {
         if (dev["wb-adc"]["Vin"] < dev["wb-adc/BAT"] ) {
             dev["power_status"]["Vin"] = 0;
             dev["power_status/Vin"] = 0;
         } else {
         } else {
             dev["power_status"]["Vin"] = dev["wb-adc"]["Vin"] ;
             dev["power_status/Vin"] = dev["wb-adc/Vin"] ;
         }
         }
     }
     }
Строка 655: Строка 839:
defineRule("_system_dc_on", {
defineRule("_system_dc_on", {
   asSoonAs: function () {
   asSoonAs: function () {
     return  dev["wb-adc"]["Vin"] > dev["wb-adc"]["BAT"];
     return  dev["wb-adc/Vin"] > dev["wb-adc/BAT"];
   },
   },
   then: function () {
   then: function () {
     dev["power_status"]["working on battery"] = false;
     dev["power_status/working on battery"] = false;
   }
   }
});
});
Строка 665: Строка 849:
defineRule("_system_dc_off", {
defineRule("_system_dc_off", {
   asSoonAs: function () {
   asSoonAs: function () {
     return  dev["wb-adc"]["Vin"] <= dev["wb-adc"]["BAT"];
     return  dev["wb-adc/Vin"] <= dev["wb-adc/BAT"];
   },
   },
   then: function () {
   then: function () {
     dev["power_status"]["working on battery"] = true;
     dev["power_status/working on battery"] = true;
   }
   }
});
});


<!--T:98-->
<!--T:98-->
</syntaxhighlight>
== Термостат == <!--T:223-->
Пример простого термостата из [https://support.wirenboard.com/t/novaya-versiya-dvizhka-pravil/4196/158 темы на портале поддержки].
<!--T:224-->
<syntaxhighlight lang="js">
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,
    },
  }
})
<!--T:225-->
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;
  }
});
}
<!--T:226-->
Termostat("R01-TS16-1", "A60-M1W3/External Sensor 1", "Termostat/R01-TS16-1-setpoint", "wb-gpio/EXT4_R3A1", "Termostat/R01-TS16-1-onoff"); // Прихожая теплый пол
</syntaxhighlight>
</syntaxhighlight>


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


Строка 752: Строка 988:
<!--T:116-->
<!--T:116-->
<pre>
<pre>
root@wirenboard:~# /etc/init.d/wb-rules restart
systemctl restart wb-rules
root@wirenboard:~# tail -f /var/log/messages
</pre>
<pre>
journalctl -u wb-rules -f
</pre>
</pre>


Строка 853: Строка 1091:
</syntaxhighlight>
</syntaxhighlight>


== Отправка сообщения через Telegram-бота ==
== Отправка сообщения через Telegram-бота == <!--T:227-->
{{Anchor|telegram}}
Сообщения отправляются с использованием [https://core.telegram.org/api#telegram-api Telegram API] через <code>curl</code>.
Сообщения отправляются с использованием [https://core.telegram.org/api#telegram-api Telegram API] через <code>curl</code>.


<!--T:228-->
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">
var message = "Text"; // напишите свой текст сообщения
var message = "Text"; // напишите свой текст сообщения
Строка 862: Строка 1102:
var command = 'curl -s -X POST https://api.telegram.org/bot{}/sendMessage -d chat_id={} -d text="{}"'.format(token, chat_id, message);
var command = 'curl -s -X POST https://api.telegram.org/bot{}/sendMessage -d chat_id={} -d text="{}"'.format(token, chat_id, message);


<!--T:229-->
runShellCommand(command);
runShellCommand(command);
</syntaxhighlight>
</syntaxhighlight>


== Обработка ошибок в работе с serial-устройствами ==
== Обработка ошибок в работе с serial-устройствами == <!--T:230-->
Реализована через подписку на все топики '''meta/error'''.
Реализована через подписку на все топики '''meta/error'''.


<!--T:231-->
<syntaxhighlight lang="ecmascript">
<syntaxhighlight lang="ecmascript">
defineVirtualDevice("meta_error_test", {
defineVirtualDevice("meta_error_test", {
Строка 886: Строка 1128:


   
   
<!--T:232-->
trackMqtt("/devices/+/controls/+/meta/error", function(message){
trackMqtt("/devices/+/controls/+/meta/error", function(message){
   log.info("name: {}, value: {}".format(message.topic, message.value))
   log.info("name: {}, value: {}".format(message.topic, message.value))
Строка 899: Строка 1142:




<!--T:233-->
[[File:Sample-custom-config-1.png|300px|thumb|right|Пример конфигурации]]
[[File:Sample-custom-config-1.png|300px|thumb|right|Пример конфигурации]]
[[File:Sample-custom-config-2.png|300px|thumb|right|Пример скрипта]]
[[File:Sample-custom-config-2.png|300px|thumb|right|Пример скрипта]]
Строка 907: Строка 1151:
Но сложную настройку с меню и вариантами так не сделать.
Но сложную настройку с меню и вариантами так не сделать.


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


<!--T:235-->
Потребуются три файла:
Потребуются три файла:


<!--T:236-->
1. Схема вывода html странички в разделе Configs : /usr/share/wb-mqtt-confed/schemas/test-config.schema.json
1. Схема вывода html странички в разделе Configs : /usr/share/wb-mqtt-confed/schemas/test-config.schema.json
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
Строка 918: Строка 1165:
"description":"Long description configuration",
"description":"Long description configuration",


<!--T:237-->
"configFile": {
"configFile": {
"path":"/etc/test-config.conf",
"path":"/etc/test-config.conf",
Строка 923: Строка 1171:
},
},


<!--T:238-->
"properties": {
"properties": {
"temperature_setpoint": {
"temperature_setpoint": {
Строка 933: Строка 1182:
},
},


<!--T:239-->
"humidity_setpoint": {
"humidity_setpoint": {
"type":"number",
"type":"number",
Строка 946: Строка 1196:
</syntaxhighlight>
</syntaxhighlight>


<!--T:240-->
2. Описание конфигурации по умолчанию (при сохранении формы в веб интерфейсе, значения запишутся в этот файл) : /etc/test-config.conf
2. Описание конфигурации по умолчанию (при сохранении формы в веб интерфейсе, значения запишутся в этот файл) : /etc/test-config.conf
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
{
{
"temperature_setpoint": 60,
"temperature_setpoint": 25,
"humidity_setpoint": 14
"humidity_setpoint": 14
}
}
</syntaxhighlight>
</syntaxhighlight>


<!--T:241-->
3. Скрипт, обновляющий конфиг : /mnt/data/etc/wb-rules/test-config-script.js
3. Скрипт, обновляющий конфиг : /mnt/data/etc/wb-rules/test-config-script.js


<!--T:242-->
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
var config = readConfig("/etc/test-config.conf");
var config = readConfig("/etc/test-config.conf");


<!--T:243-->
log("temperature setpoint is: {}".format(config.temperature_setpoint));
log("temperature setpoint is: {}".format(config.temperature_setpoint));
</syntaxhighlight>
</syntaxhighlight>


<!--T:244-->
Последний файл можно в том числе редактировать из веб-интерфейса на вкладке Scripts.
Последний файл можно в том числе редактировать из веб-интерфейса на вкладке Scripts.


<!--T:245-->
В json файлах описаны схемы вывода html странички браузером, по общепринятому стандарту отображения. Описание ключей тут: json-schema.org.
В json файлах описаны схемы вывода html странички браузером, по общепринятому стандарту отображения. Описание ключей тут: json-schema.org.


<!--T:246-->
После создания файлов, нужно выполнить рестарт сервисов
После создания файлов, нужно выполнить рестарт сервисов


<!--T:247-->
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
service wb-mqtt-confed restart
service wb-mqtt-confed restart


<!--T:248-->
service wb-rules restart
service wb-rules restart
</syntaxhighlight>
</syntaxhighlight>


<!--T:249-->
При нажатии кнопки Save в веб-интерфейсе, будет перезапускаться сервис wb-rules, а значения установок - записываться в правила.
При нажатии кнопки Save в веб-интерфейсе, будет перезапускаться сервис wb-rules, а значения установок - записываться в правила.


Строка 985: Строка 1245:


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


<!--T:142-->
<!--T:142-->
Строка 1397: Строка 1657:
   });
   });


   <!--T:190-->
   defineRule("acVegOff", {
defineRule("acVegOff", {
     when: function() {
     when: function() {
       return vegetablesTemperature < 17.8
       return vegetablesTemperature < 17.8
Строка 1409: Строка 1668:
})()
})()
</syntaxhighlight>
</syntaxhighlight>
== Полезные ссылки ==
 
==Работа с JSON==
Движок wb-rules поддерживает стандартные функции языка JavaScript для работы с JSON:
 
*<code>JSON.stringify()</code> — преобразует объект в JSON-строку;
*<code>JSON.parse()</code> — преобразует JSON-строку в объект.
 
Более подробную информацию о функциях можно найти в учебнике [https://learn.javascript.ru/json JavaScript].
 
Эти функции требуются, когда вы получаете данные из другого сервиса в JSON-формате.
 
В приведенном примере создается виртуальное устройство с одной кнопкой и числовым параметром, который который хранится в виде JSON-строки. При нажатии на кнопку к значению параметра прибавляется 1.
<syntaxhighlight lang="bash">
defineVirtualDevice("JSON_test", {
    title: "JSON_device",
    cells: {
Button: {
    type: "pushbutton",
    value: false
},
    Json: {
        type : "text",
        value : JSON.stringify({param: 0}),
    },     
  }
});
 
defineRule("change_value", {
  whenChanged: "JSON_test/Button",
  then: function () {
    parameter = JSON.parse(dev["JSON_test/Json"]);
    parameter.param++;
    dev["JSON_test/Json"] = JSON.stringify(parameter)
    }
});
</syntaxhighlight>
 
 
==Работа с последовательным портом через RPC==
[[File:mqtt-rpc.png|300px|thumb|right|Работа с последовательным портом через RPC]]
Если устройство на шине работает по протоколу, который не поддерживается драйвером [[Wb-mqtt-serial_driver |wb-mqtt-serial]] можно формировать запросы вручную и отправлять их драйверу через [https://github.com/wirenboard/mqtt-rpc RPC-MQTT].
 
RPC-MQTT создает MQTT-топик для отправки запросов, и топик для чтения ответов от драйвера. Поэтому для его использования достаточно отправить запрос в нужный топик функцией <code>publish()</code> и прочитать ответ функцией <code>trackMqtt()</code>. Как узнать адреса топиков описано в [https://github.com/wirenboard/mqtt-rpc документации].
 
В примере написан скрипт на wb-rules для отправки Modbus-запроса устройству Wiren Board на шине RS-485.
 
Переменная <code>message</code> содержит Modbus-запрос, сформированный в соответствии со стандартом [[Modbus |Modbus RTU]].
 
Переменная <code>pathRPC</code> — это адрес MQTT-топика, в который отправляются запросы для драйвера wb-mqtt-serial. Для каждого сервиса используется свой топик, и узнать его адрес можно из документации на RPC-MQTT.
 
<syntaxhighlight lang="bash">
var pathRPC = "/rpc/v1/wb-mqtt-serial/port/Load/";  //Адрес топика в который отправляется запрос
var modbusPort = "/dev/ttyRS485-1";
var modbusSpeed = 9600;
var modbusParity = "N";
var modbusStopbit = 2;
var message = "E0300C8000644C9";
 
 
var clientID = "testRPC";
 
 
function requestRPC(modbusPort, modbusSpeed, modbusParity, modbusStopbit, clientID, requiestID, messageType, message, responseSize){
var strJson = JSON.stringify({params: {response_size: responseSize, format: messageType, path: modbusPort, baud_rate: modbusSpeed, parity: modbusParity, "data_bits" : 8, "stop_bits" : modbusStopbit, "msg": message}, "id" : requiestID});
  log.info("strJson =", strJson);
 
  publish(pathRPC+clientID, strJson, 2, false)
};
 
 
trackMqtt(pathRPC+clientID+"/reply", function(message){
  log.info("name: {}, value: {}".format(message.topic, message.value))
});
 
 
requestRPC(modbusPort, modbusSpeed, modbusParity, modbusStopbit, clientID, 1, "HEX", message, 8)
 
</syntaxhighlight>
 
Если запрос отправлен без ошибок, то в лог будет выведено сообщение вида:
<syntaxhighlight lang="bash">
name: /rpc/v1/wb-mqtt-serial/port/Load/testRPC/reply,
value: {"error":null,"id":1,"result":{"response":"0e030400002569df"}}
</syntaxhighlight>
 
== Получение SMS ==
В примере с периодом в 1 секунду выводится в лог вся информация о последнем сообщении. Полученные SMS будут в capturedOutput. Пример из [https://support.wirenboard.com/t/wb7-modem-rabota-s-sms-soobshheniyami/18159 темы на портале].
 
<syntaxhighlight lang="bash">
var period = 1000;
setInterval(function() {
    runShellCommand("mmcli --modem wbc --messaging-list-sms --output-keyvalue | grep length | cut -f2 -d':'", {
        captureOutput: true,
        exitCallback: function(exitCode, capturedOutput) {
            if (exitCode === 0) {
                runShellCommand("mmcli --modem wbc --sms " + (parseInt(capturedOutput) - 1).toString(), {
                    captureOutput: true,
                    exitCallback: function(exitCode, capturedOutput) {
                        if (exitCode === 0) {
                            log(capturedOutput);
                            return;
                        }
                    }
                });
                return;
            }
        }
    });
}, period);
</syntaxhighlight>
 
== Полезные ссылки == <!--T:190-->
* [[Wb-rules | Краткое описание wb-rules на wiki]]
* [[Wb-rules | Краткое описание wb-rules на wiki]]
* [https://github.com/wirenboard/wb-rules Полное описание wb-rules на Github]
* [https://github.com/wirenboard/wb-rules Полное описание wb-rules на Github]
</translate>
</translate>

Текущая версия на 13:51, 14 апреля 2024

Другие языки:


Общая информация

Здесь вы найдёте учебные примеры скриптов, написанных для движка правил wb-rules.

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

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

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

Пример ниже создаст виртуальное устройство с именем deviceName и двумя контролами value и state. А благодаря правилу с whenChanged, значение контрола state будет менять в зависимости от значение контрола value.

deviceName = 'my-virtual-device';

defineVirtualDevice(deviceName, {
    title: {'en': 'My Virtual Device', 'ru': 'Мое виртуальное устройство'} ,
    cells: {
      value: {
        title: {'en': 'Value', 'ru': 'Значение'},
        type: "range",
        value: 1,
        max: 3,
        min: 1
      },
      state: {
        title: {'en': 'State', 'ru': 'Состояние'},
        type: "value",
        value: 1,
        enum:{
          1: {'en': 'Normal', 'ru': 'В норме'},
          2: {'en': 'Warning', 'ru': 'Внимание'},
          3: {'en': 'Crash', 'ru': 'Авария'}}   
      },
    }
});

defineRule({
  whenChanged: deviceName+"/value",
  then: function (newValue, devName, cellName) {
	dev[deviceName+"/state"] = newValue;
  }
});

Слежение за контролом

Это простейшее правило следит за контролом и устанавливает другой контрол в такое же состояние.

Например правило может включать сирену и лампу, если датчик движения заметил движение.

В примере датчик движения подключен к входу «сухой контакт», контрол типа «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;

  }
});

Мастер-выключатель с восстановлением последнего состояния

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

Подключение осуществляется к контакту A1 и 5V на контроллере. При замыкании на соответствующем канале wb-gpio/A1_IN, состояние меняется, и срабатывает правило.

Для управления через веб-интерфейс создано виртуальное устройство, отображаемое на вкладке Устройства.

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

defineVirtualDevice("power_off", {
    title: "Мастер-выключатель",
    cells: {
        power_off: {
            type: "pushbutton"
        },
    }
});

var ps = new PersistentStorage("power-storage", { global: true });
var lights = ["wb-mdm3_50/K1", "wb-mdm3_50/K2", "wb-mdm3_50/K3"];

var isPowerOff = true;

defineRule({
    whenChanged: ["wb-gpio/A1_IN", "power_off/power_off"],
    then: function (newValue, devName, cellName) {
        if (isPowerOff) {
            lights.forEach(function (light) {
                ps[light] = dev[light];
                dev[light] = false;
            });
        } else {
            lights.forEach(function (light) {
                dev[light] = ps[light];
            });
        }
        isPowerOff = !isPowerOff;
    }
});

Детектор движения 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; // Умножаем значение счетчика на количество литров/импульс и прибавляем корректировочное значение.
      }
    }
});

Инвертирование значения контрола

Пример устройств с вкладки Устройства

Правило ниже создаёт виртуальное устройство my-invert-buzzer, с контролом disabled, который инвертирует состояние контрола enabled системной пищалки Buzzer.

defineVirtualDevice('my-invert-buzzer', {
    title: 'Buzzer Invert' ,
    cells: {
      Disabled: {
        title: "disabled",
	    type: "switch",
	    value: !dev["buzzer/enabled"]
	    }
    }
})

defineRule({
    whenChanged: ["buzzer/enabled"],
    then: function(newValue, devName, cellName) {
      dev["my-invert-buzzer/Disabled"] = !newValue;
    }
});

defineRule({
    whenChanged: ["my-invert-buzzer/Disabled"],
    then: function(newValue, devName, cellName){
        dev["buzzer/enabled"] = !newValue;
    }
});

Обработка счётчиков нажатий

Описание

Последние версии прошивок устройств Wiren Board могут распознавать типы нажатий подключённых к входам кнопок и транслировать их по Modbus на контроллер Wiren Board. О том, как устройство распознаёт типы нажатий, читайте в его документации.

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

Обработку счётчиков удобно делать на wb-rules, но вы можете использовать любой инструмент для автоматизации, например, Node-RED. Чтобы ускорить опрос счетчиков, настройте период опроса.

Примеры

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

В примере мы используем модуль WB-MCM8 для управления первым каналом диммера WB-MDM3:

  1. Короткое нажатие включает канал.
  2. Двойное — выключает канал.
  3. Длинное — увеличивает яркость.
  4. Короткое, а затем длинное — уменьшает яркость.

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

/* ---------------------------- */
/* 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 - раз в секунду мигаем красным.

defineRule("msw3_co2", {
    whenChanged: "wb-msw-v3_97/CO2",
    then: function(newValue, devName, cellName) {
        var co2_good = newValue < 650;
        var co2_middle = newValue < 1000 && newValue > 651;
        var co2_bad = newValue > 1001;

        if (co2_good) {
            dev[devName+"/Green LED"] = true;
            dev[devName+"/Red LED"] = false;
            dev[devName+"/LED Period (s)"] = 10;
        }
        if (co2_middle) {
            dev[devName+"/Green LED"] = true;
            dev[devName+"/Red LED"] = true;
            dev[devName+"/LED Period (s)"] = 5;
        }
        if (co2_bad) {
            dev[devName+"/Green LED"] = false;
            dev[devName+"/Red LED"] = true;
            dev[devName+"/LED Period (s)"] = 1;
        }
    }
});

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

function ruleCO2 (devCO2, minCO2, maxCO2){
  log.debug("rule create", devCO2)
  defineRule ("ruleCO2"+devCO2, {
      whenChanged: devCO2+"/CO2",
      then: function(newValue, devName, cellName) {
        log.info("ruleCO2 " + devCO2 +" enter with", newValue)
        if (newValue < minCO2) {
          dev[devCO2+"/LED Glow Duration (ms)"] = 50;
          dev[devCO2+"/Green LED"] = true;
          dev[devCO2+"/Red LED"] = false;
          dev[devCO2+"/LED Period (s)"] = 3;
        }
        if ((newValue > minCO2) && (newValue < maxCO2)) {
          dev[devCO2+"/LED Glow Duration (ms)"] = 50;            
          dev[devCO2+"/Green LED"] = true;
          dev[devCO2+"/Red LED"] = true;
          dev[devCO2+"/LED Period (s)"] = 2;
        }
        if (newValue > maxCO2) {
          dev[devCO2+"/LED Glow Duration (ms)"] = 50;            
          dev[devCO2+"/Green LED"] = false;
          dev[devCO2+"/Red LED"] = true;
          dev[devCO2+"/LED Period (s)"] = 1;
        }
      }
  });
}


ruleCO2("wb-msw-v3_97", 650, 1000);
ruleCO2("wb-msw-v3_98", 650, 1000);
ruleCO2("wb-msw-v3_11", 500, 700);

Max Motion

"Max Motion" - максимальное значение датчика движения за N время. Время от 1 до 60 секунд можно выставить в 282 регистре. По умолчанию 10 секунд. При достижении Max Motion значения 50 проверяем достаточно ли освещена комната, если нет - включаем свет. Как только значение Max Motion упадет ниже 50 свет выключаем.

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

Многие показания, которые видны в веб-интерфейсе контроллера из коробки, тоже создаются правилами на движке 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.

nano  /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 и проверяем работу

systemctl restart wb-rules
journalctl -u wb-rules -f

В логе не должно быть сообщений об ошибке (выход через 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": 25,
"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; // выключить
    }
  });
})()

Работа с JSON

Движок wb-rules поддерживает стандартные функции языка JavaScript для работы с JSON:

  • JSON.stringify() — преобразует объект в JSON-строку;
  • JSON.parse() — преобразует JSON-строку в объект.

Более подробную информацию о функциях можно найти в учебнике JavaScript.

Эти функции требуются, когда вы получаете данные из другого сервиса в JSON-формате.

В приведенном примере создается виртуальное устройство с одной кнопкой и числовым параметром, который который хранится в виде JSON-строки. При нажатии на кнопку к значению параметра прибавляется 1.

defineVirtualDevice("JSON_test", {
    title: "JSON_device",
    cells: {
	Button: {
	    type: "pushbutton",
	    value: false
	},
    Json: {
        type : "text",
        value : JSON.stringify({param: 0}),
    },       
  }
});

defineRule("change_value", {
  whenChanged: "JSON_test/Button",
  then: function () {
    parameter = JSON.parse(dev["JSON_test/Json"]);
    parameter.param++;
    dev["JSON_test/Json"] = JSON.stringify(parameter)
    }
});


Работа с последовательным портом через RPC

Работа с последовательным портом через RPC

Если устройство на шине работает по протоколу, который не поддерживается драйвером wb-mqtt-serial можно формировать запросы вручную и отправлять их драйверу через RPC-MQTT.

RPC-MQTT создает MQTT-топик для отправки запросов, и топик для чтения ответов от драйвера. Поэтому для его использования достаточно отправить запрос в нужный топик функцией publish() и прочитать ответ функцией trackMqtt(). Как узнать адреса топиков описано в документации.

В примере написан скрипт на wb-rules для отправки Modbus-запроса устройству Wiren Board на шине RS-485.

Переменная message содержит Modbus-запрос, сформированный в соответствии со стандартом Modbus RTU.

Переменная pathRPC — это адрес MQTT-топика, в который отправляются запросы для драйвера wb-mqtt-serial. Для каждого сервиса используется свой топик, и узнать его адрес можно из документации на RPC-MQTT.

var pathRPC = "/rpc/v1/wb-mqtt-serial/port/Load/";  //Адрес топика в который отправляется запрос
var modbusPort = "/dev/ttyRS485-1";
var modbusSpeed = 9600;
var modbusParity = "N";
var modbusStopbit = 2;
var message = "E0300C8000644C9";


var clientID = "testRPC";


function requestRPC(modbusPort, modbusSpeed, modbusParity, modbusStopbit, clientID, requiestID, messageType, message, responseSize){
 
var strJson = JSON.stringify({params: {response_size: responseSize, format: messageType, path: modbusPort, baud_rate: modbusSpeed, parity: modbusParity, "data_bits" : 8, "stop_bits" : modbusStopbit, "msg": message}, "id" : requiestID});
 
   log.info("strJson =", strJson);

   publish(pathRPC+clientID, strJson, 2, false)
};


trackMqtt(pathRPC+clientID+"/reply", function(message){
   log.info("name: {}, value: {}".format(message.topic, message.value))
 });


requestRPC(modbusPort, modbusSpeed, modbusParity, modbusStopbit, clientID, 1, "HEX", message, 8)

Если запрос отправлен без ошибок, то в лог будет выведено сообщение вида:

name: /rpc/v1/wb-mqtt-serial/port/Load/testRPC/reply, 
value: {"error":null,"id":1,"result":{"response":"0e030400002569df"}}

Получение SMS

В примере с периодом в 1 секунду выводится в лог вся информация о последнем сообщении. Полученные SMS будут в capturedOutput. Пример из темы на портале.

var period = 1000;
setInterval(function() {
    runShellCommand("mmcli --modem wbc --messaging-list-sms --output-keyvalue | grep length | cut -f2 -d':'", {
        captureOutput: true,
        exitCallback: function(exitCode, capturedOutput) {
            if (exitCode === 0) {
                runShellCommand("mmcli --modem wbc --sms " + (parseInt(capturedOutput) - 1).toString(), {
                    captureOutput: true,
                    exitCallback: function(exitCode, capturedOutput) {
                        if (exitCode === 0) {
                            log(capturedOutput);
                            return;
                        }
                    }
                });
                return;
            }
        }
    });
}, period);

Полезные ссылки