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

Материал из Wiren Board
(не показано 50 промежуточных версий 7 участников)
Строка 1: Строка 1:
{{DISPLAYTITLE: Примеры правил}}
<languages/>
<languages/>
<translate>
<translate>
<!--T:255-->
=== Слежение за контролом === <!--T:20-->
{{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-->
Строка 58: Строка 11:


<!--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-->
Строка 67: Строка 20:


<!--T:26-->
<!--T:26-->
defineRule({
defineRule("motion_detector", {
   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-->
Строка 99: Строка 52:
   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-->
Строка 107: Строка 60:
</syntaxhighlight>
</syntaxhighlight>


== Мастер-выключатель с восстановлением последнего состояния == <!--T:250-->
=== Детектор движения c таймаутом === <!--T:34-->
 
На вход контроллера подключен мастер-выключатель, который, при переключении, отключает все устройства, указанные в соответствующем правиле. При повторном нажатии на выключатель, устройствам возвращается первоначальное состояние.
 
Подключение осуществляется к контакту 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-->
Строка 160: Строка 70:
<!--T:36-->
<!--T:36-->
Правило работает так:
Правило работает так:
* когда движение появляется, свет включается. Если ранее был запущен тридцатисекундный таймер «на выключение», этот таймер отключается;
* когда движение появляется, свет включается. Если ранее был запущен тридцатисекундный таймер "на выключение", этот таймер отключается;
* когда движение пропадает, запускается тридцатисекундный таймер «на выключение». Если ему удаётся дойти до конца, свет выключается.
* когда движение пропадает, запускается тридцатисекундный таймер "на выключение". Если ему удаётся дойти до конца, свет выключается.


<!--T:38-->
<!--T:38-->
Строка 168: Строка 78:
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);
        }
  },
    },
});
});
</syntaxhighlight>
</syntaxhighlight>


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


<!--T:44-->
<!--T:44-->
Строка 199: Строка 107:
       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);
Строка 206: Строка 114:
               <!--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);
Строка 220: Строка 128:
</syntaxhighlight>
</syntaxhighlight>


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


<!--T:49-->
<!--T:49-->
Строка 251: Строка 159:
     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) {
Строка 258: Строка 166:
    
    
           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);               
Строка 267: Строка 175:
</syntaxhighlight>
</syntaxhighlight>


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


<!--T:54-->
<!--T:54-->
Строка 300: Строка 208:
   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 () {
Строка 309: Строка 217:
       <!--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);
     }
     }
Строка 317: Строка 225:
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 () {
Строка 325: Строка 233:
        
        
       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);
     }
     }
Строка 333: Строка 241:
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 () {
Строка 348: Строка 256:
       <!--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");
     }
     }
Строка 402: Строка 310:
})();
})();


<!--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 // Корректировочное значение счетчика в литрах
Строка 414: Строка 321:
var inpulseValue = 10 // Количество литров на один импульс
var inpulseValue = 10 // Количество литров на один импульс


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


<!--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; // Умножаем значение счетчика на количество литров/импульс и прибавляем корректировочное значение.
       }
       }
     }
     }
Строка 436: Строка 341:
</syntaxhighlight>
</syntaxhighlight>


== Инвертирование значения контрола ==
== Датчик MSW v.3 ==
[[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}}
=== Описание ===
Последние версии прошивок устройств Wiren Board могут распознавать типы нажатий подключённых к входам кнопок и транслировать их по [[Modbus]] на контроллер Wiren Board. О том, как устройство распознаёт типы нажатий, читайте в его документации.
<!--T:197-->
Для обработки нажатий нужно отслеживать на контроллере состояние счётчика нужного типа нажатия и, при его изменении, выполнять действие.
<!--T:198-->
Обработку счётчиков удобно делать на [[wb-rules]], но вы можете использовать любой инструмент для автоматизации, например, [[Node-RED]]. Чтобы ускорить опрос счетчиков, настройте [[RS-485:Configuration via Web Interface#poll-period |период опроса]].
=== Примеры === <!--T:199-->
{{YouTube
|link=https://youtu.be/C60KB7TCeKg
|text= Пример работы правила
}}
В примере мы используем модуль [[WB-MCM8 Modbus Count Inputs | WB-MCM8]] для управления первым каналом диммера [[WB-MDM3 230V Modbus Dimmer | WB-MDM3]]:
# Короткое нажатие включает канал.
# Двойное — выключает канал.
# Длинное — увеличивает яркость.
# Короткое, а затем длинное — уменьшает яркость.
<!--T:200-->
Так как изменение яркости требует растянутое во времени действие, то мы используем таймер. Также мы контролируем состояние входа с кнопкой и прекращаем действие при отпускании кнопки.
<!--T:201-->
<syntaxhighlight lang="js">
/* ---------------------------- */
/* 1. Single Press Counter: On action*/
/* ---------------------------- */
<!--T:202-->
defineRule({
  whenChanged: "wb-mcm8_20/Input 1 Single Press Counter",
  then: function (newValue, devName, cellName) {
    dev["wb-mdm3_58/K1"] = true;
  }
});
<!--T:203-->
/* ---------------------------- */
/* 2. Double Press Counter: Off action*/
/* ---------------------------- */
<!--T:204-->
defineRule({
  whenChanged: "wb-mcm8_20/Input 1 Double Press Counter",
  then: function (newValue, devName, cellName) {
    dev["wb-mdm3_58/K1"] = false;
  }
});
<!--T:205-->
/* --------------------------------------- */
/* 3. Long Press Counter: Increase brightness */
/* --------------------------------------- */
<!--T:206-->
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);
  }
});
<!--T:207-->
// 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"];
    <!--T:208-->
if (i < 100 && dev["wb-mcm8_20/Input 1"]) {
      i++
      dev["wb-mdm3_58/Channel 1"] = i
    } else {
      timers["input1_long_press"].stop();
    }
  }
});
<!--T:209-->
/* -------------------------------------------- */
/* 4. Shortlong Press Counter: Decrease brightness */
/* -------------------------------------------- */
<!--T:210-->
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);
  }
});
<!--T:211-->
// 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"];
    <!--T:212-->
if (i > 0 && dev["wb-mcm8_20/Input 1"]) {
      i--
      dev["wb-mdm3_58/Channel 1"] = i
    } else {
      timers["input1_shortlong_press"].stop();
    }
  }
});
<!--T:213-->
</syntaxhighlight>
=== Универсальный модуль для wb-rules === <!--T:214-->
Мы написали модуль для wb-rules [https://github.com/wirenboard/wb-community/tree/main/scripts/wb-press-actions wb-press-actions], который облегчает обработку нажатий в ваших скриптах.
== Датчик 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 === <!--T:217-->
=== CO2 ===
При концентрации 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;">
Строка 612: Строка 361:


         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">
Строка 680: Строка 389:
     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;
         }
         }
     }
     }
Строка 694: Строка 403:
== Системные правила == <!--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>.  


Строка 826: Строка 534:
     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"] ;
         }
         }
     }
     }
Строка 839: Строка 547:
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;
   }
   }
});
});
Строка 849: Строка 557:
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>


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


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


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


== Отправка сообщения через Telegram-бота == <!--T:227-->
== Пользовательские поля в интерфейсе == <!--T:135-->
{{Anchor|telegram}}
Сообщения отправляются с использованием [https://core.telegram.org/api#telegram-api Telegram API] через <code>curl</code>.
 
<!--T:228-->
<syntaxhighlight lang="ecmascript">
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);
 
<!--T:229-->
runShellCommand(command);
</syntaxhighlight>
 
== Обработка ошибок в работе с serial-устройствами == <!--T:230-->
Реализована через подписку на все топики '''meta/error'''.
 
<!--T:231-->
<syntaxhighlight lang="ecmascript">
defineVirtualDevice("meta_error_test", {
  title: "Metaerordisplay",
  cells: {
      topic: {
      type: "text",
      value: "",
      readonly: true
    },
    value: {
      type: "text",
      value: "",
      readonly: true
    },
  }
});
 
<!--T:232-->
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;
  }
 
});
</syntaxhighlight>
 
== Пользовательские поля в веб-интерфейсе == <!--T:135-->




<!--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|Пример скрипта]]
Задача - надо в веб-интерфейсе контроллера Wiren Board вводить уставки  температуры и влажности.  
Задача - надо в интерфейсе контроллера Wiren Board вводить уставки  температуры и влажности.  


<!--T:3-->
<!--T:3-->
Простой способ, это сделать в defineVirtualDevice() поле, ему сделать readonly: false. И оно появится в веб-интерфейсе в Devices как редактируемое, а значение будет сохраняться в движке правил.
Простой способ, это сделать в defineVirtualDevice() поле, ему сделать readonly: false. И оно появится в веб-интерфейсе в Devices как редактируемое, а значение будет сохраняться в движке правил.
Но сложную настройку с меню и вариантами так не сделать.
Но сложную настройку с менюшками и вариантами так не сделать.


<!--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">
Строка 1165: Строка 767:
"description":"Long description configuration",
"description":"Long description configuration",


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


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


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


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


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


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


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


   defineRule("acVegOff", {
   <!--T:190-->
defineRule("acVegOff", {
     when: function() {
     when: function() {
       return vegetablesTemperature < 17.8
       return vegetablesTemperature < 17.8
Строка 1668: Строка 1258:
})()
})()
</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]]
* [https://github.com/wirenboard/wb-rules Полное описание wb-rules на Github]
</translate>
</translate>

Версия 14:03, 17 марта 2021

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

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

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

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

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

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

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

  }
});

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

defineVirtualDevice("simple_test", {
    title: "Simple switch",
    cells: {
	enabled: {
	    type: "switch",
	    value: false
	},
    }
});

defineRule("simple_switch", {
  whenChanged: "simple_test/enabled",
  then: function (newValue, devName, cellName) {
	dev["wb-gpio"]["Relay_2"] = newValue;
	dev["wb-mrm2_6"]["Relay 1"] = newValue;

  }
});

Детектор движения c таймаутом

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

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

Датчик 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;
        }
    }
});

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;
  }
});

Отправка команд по RS-485

Для примера отправим команду устройству на порт /dev/ttyNSC0 (соответствует аппаратному порту RS-485-ISO на Wiren Board 4). Для этого будем использовать движок правил и возможность выполнения произвольных shell-команд. Подробнее см. в документации.

С помощью движка правил создадим виртуальное устройство с контролом типа switch (переключатель).

При включении переключателя будем отправлять команду (уст. Яркость кан. 00=0xff) для Uniel UCH-M141:

FF FF 0A 01 FF 00 00 0A

При выключении переключателя будем отправлять команду (уст. Яркость кан. 00=0x00) для Uniel UCH-M141:

FF FF 0A 01 00 00 00 0B

1. Настройка порта

Для настройки порта /dev/ttyNSC0 на скорость 9600 надо выполнить следующую команду

stty -F /dev/ttyNSC0 ospeed 9600 ispeed 9600 raw clocal -crtscts -parenb -echo cs8

2. Отправка команды

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

/usr/bin/printf '\xFF\xFF\x0A\x01\xD1\x06\x00\xE2' >/dev/ttyNSC0

где "\xFF\xFF\x0A\x01\xD1\x06\x00\xE2" - это запись команды "FF FF 0A 01 D1 06 00 E2".


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

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

root@wirenboard:~# mcedit  /etc/wb-rules/rs485_cmd.js

4. Описываем в файле виртуальный девайс

defineVirtualDevice("rs485_cmd", {
    title: "Send custom command to RS-485 port",
    cells: {
	enabled: {
	    type: "switch",
	    value: false
	},
    }
});

5. Перезапускаем wb-rules и проверяем работу

root@wirenboard:~# /etc/init.d/wb-rules restart
root@wirenboard:~# tail -f /var/log/messages

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

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

6. Добавим функцию для конфигурирования порта.

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

7. Опишем правила на включение и выключение переключателя

defineRule("_rs485_switch_on", {
  asSoonAs: function () {
    return dev.rs485_cmd.enabled;
  },
  then: function() {
    runShellCommand("/usr/bin/printf '\\xff\\xff\\x0a\\x01\\xff\\x00\\x00\\x0a' > /dev/ttyNSC0");
  }
});

defineRule("_rs485_switch_off", {
  asSoonAs: function () {
    return !dev.rs485_cmd.enabled;
  },
  then: function() {
    runShellCommand("/usr/bin/printf '\\xff\\xff\\x0a\\x01\\x00\\x00\\x00\\x0b' >/dev/ttyNSC0");
  }
});

Обратите внимание на двойное экранирование.

7. Собираем всё вместе

Полное содержимое файла с правилами:

defineVirtualDevice("rs485_cmd", {
    title: "Send custom command to RS-485 port",
    cells: {
	enabled: {
	    type: "switch",
	    value: false
	},
    }
});

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

defineRule("_rs485_switch_on", {
  asSoonAs: function () {
    return dev.rs485_cmd.enabled;
  },
  then: function() {
    runShellCommand("/usr/bin/printf '\\xff\\xff\\x0a\\x01\\xff\\x00\\x00\\x0a' > /dev/ttyNSC0");
  }
});

defineRule("_rs485_switch_off", {
  asSoonAs: function () {
    return !dev.rs485_cmd.enabled;
  },
  then: function() {
    runShellCommand("/usr/bin/printf '\\xff\\xff\\x0a\\x01\\x00\\x00\\x00\\x0b' >/dev/ttyNSC0");
  }
});

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

Пользовательские поля в интерфейсе

Пример конфигурации
Пример скрипта

Задача - надо в интерфейсе контроллера Wiren Board вводить уставки температуры и влажности.

Простой способ, это сделать в defineVirtualDevice() поле, ему сделать readonly: false. И оно появится в веб-интерфейсе в Devices как редактируемое, а значение будет сохраняться в движке правил. Но сложную настройку с менюшками и вариантами так не сделать.

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

Потребуются три файла:

1. Схема вывода html странички в разделе Configs : /usr/share/wb-mqtt-confed/schemas/test-config.schema.json

{
"type":"object",
"title":"Test configuration",
"description":"Long description configuration",

"configFile": {
"path":"/etc/test-config.conf",
"service":"wb-rules"
},

"properties": {
"temperature_setpoint": {
"type":"number",
"title":"Temperature Setpoint (Degrees C)",
"default": 25,
"propertyOrder": 1,
"minimum": 5,
"maximum": 40
},

"humidity_setpoint": {
"type":"number",
"title":"Humidity Setpoint (RH, %)",
"default": 60,
"propertyOrder": 2,
"minimum": 10,
"maximum": 95
}
},
"required": ["temperature_setpoint", "humidity_setpoint"]
}

2. Описание конфигурации по умолчанию (при сохранении формы в веб интерфейсе, значения запишутся в этот файл) : /etc/test-config.conf

{
"temperature_setpoint": 60,
"humidity_setpoint": 14
}

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

var config = readConfig("/etc/test-config.conf");

log("temperature setpoint is: {}".format(config.temperature_setpoint));

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

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

После создания файлов, нужно выполнить рестарт сервисов

service wb-mqtt-confed restart

service wb-rules restart

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

Сложные правила с расписаниями

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

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

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

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

lib_schedules.js:

global.__proto__.Schedules = {};

(function(Schedules) { // замыкание

  function todayAt(now, hours, minutes) {
    var date = new Date(now);
    // i.e. "today, at HH:MM". All dates are in UTC!
    date.setHours(hours);
    date.setMinutes(minutes);
    return date;
  }

  function checkScheduleInterval(now, start_time, end_time) {
    var start_date = todayAt(now, start_time[0], start_time[1]);
    var end_date = todayAt(now, end_time[0], end_time[1]);
    log("checkScheduleInterval {} {} {}".format(now, start_date, end_date));

    if (end_date >= start_date) {
      if ((now >= start_date) && (now < end_date)) {
        return true;
      }
    } else {
      // end date is less than start date, 
      // assuming they belong to a different days (e.g. today and tomorrow)

      // option 1: what if it's now the day of "end" date?
      // in this case the following is enough:
      if (now < end_date) {
        return true;
      }

      // well, that seems not to be the case. ok,
      // option 2: it's the day of "start" date:

      if (now >= start_date) {
        return true;
      }
    }
    return false;

  }

  function checkSchedule(schedule, now) {
    if (now == undefined) {
      now = new Date();
    }

    for (var i = 0; i < schedule.intervals.length; ++i) {
      var item = schedule.intervals[i];
      if (checkScheduleInterval(now, item[0], item[1])) {
        log("found matching schedule interval at {}".format(item));
        return true;
      }
    }
    return false;
  }
  
  function updateSingleScheduleDevStatus(schedule) {
    log("updateSingleScheduleDevStatus {}".format(schedule.name));
    dev["_schedules"][schedule.name] = checkSchedule(schedule);
  };

  function addScheduleDevCronTasks(schedule) {
    for (var i = 0; i < schedule.intervals.length; ++i) {
      var interval = schedule.intervals[i];
      for (var j = 0; j < 2; ++j) { // either start or end of the interval
        var hours = interval[j][0]; 
        var minutes = interval[j][1];
        log("cron at " + "0 " + minutes + " " + hours + " * * *");
        defineRule("_schedule_dev_{}_{}_{}".format(schedule.name, i, j), {
          when: cron("0 " + minutes + " " + hours + " * * *"),
          then: function () {
            log("_schedule_dev_ {}_{}_{}".format(schedule.name, i, j));
            updateSingleScheduleDevStatus(schedule);
          }
        });
      }
    }    
  }

  function addScheduleAutoUpdCronTask(schedule) {
    defineRule("_schedule_auto_upd_{}".format(schedule.name), {
      when: cron("@every " + schedule.autoUpdate),
      then: function() {
        dev._schedules[schedule.name] = dev._schedules[schedule.name];
      }
    });
  }

  var _schedules = {};

  Schedules.registerSchedule = function(schedule) {
    _schedules[schedule.name] = schedule;
  };

  Schedules.initSchedules = function() {
    var params = {
      title: "Schedule Status", 
      cells: {}
    };

    for (var schedule_name in _schedules) {
      if (_schedules.hasOwnProperty(schedule_name)) {
        var schedule = _schedules[schedule_name];
        params.cells[schedule_name] = {type: "switch", value: false, readonly: true};
      }
    };

    defineVirtualDevice("_schedules", params);


    for (var schedule_name in _schedules) {
      if (_schedules.hasOwnProperty(schedule_name)) {
        var schedule = _schedules[schedule_name];

        // setup cron tasks which updates the schedule dev status at schedule
        //   interval beginings and ends
        addScheduleDevCronTasks(schedule);

        // if needed, setup periodic task to trigger rules which use this schedule
        if (schedule.autoUpdate) {
          addScheduleAutoUpdCronTask(schedule);
        }

        // set schedule dev status as soon as possible at startup
        (function(schedule) {
          setTimeout(function() {
            updateSingleScheduleDevStatus(schedule);
          }, 1);
        })(schedule);

      };
    };
};

})(Schedules);

Пример правил, с использованием Schedules:

(function() { // замыкание

  defineAlias("countersTemperature", "wb-msw2_30/Temperature");
  defineAlias("vegetablesTemperature", "wb-msw2_31/Temperature");

  defineAlias("heater1EnableInverted", "wb-mrm2-old_70/Relay 1");
  defineAlias("frontshopVentInverted", "wb-gpio/EXT1_R3A3");

  Schedules.registerSchedule({
    "name" : "signboard", // вывеска
    "autoUpdate" : "1m",
    "intervals" : [
      [ [12, 30], [20, 30] ],  // в UTC, 15:30 - 23:30 MSK
      [ [3, 30], [5, 20] ],  // в UTC, 6:30 - 8:20 MSK
    ]
  });
  Schedules.registerSchedule({
    "name" : "ext_working_hours_15m",
    "autoUpdate" : "1m",
    "intervals" : [
      [ [4, 45], [20, 15] ],  // всё ещё UTC, 7:45 - 23:15 MSK
    ]
  });
  Schedules.registerSchedule({
    "name" : "working_hours",
    "autoUpdate" : "1m",
    "intervals" : [
      [ [5, 0], [19, 0] ],  // всё ещё UTC, 8:00 - 22:00 MSK
    ]
  });
  Schedules.registerSchedule({
    "name" : "working_hours_15m",
    "autoUpdate" : "1m",
    "intervals" : [
      [ [4, 45], [19, 15] ],  // всё ещё UTC, 7:45 - 22:15 MSK
    ]
  });
  Schedules.registerSchedule({
    "name" : "frontshop_lighting",
    "autoUpdate" : "1m",
    "intervals" : [
      [ [4, 20], [20, 45] ],  // всё ещё UTC, 7:20 -23:45 MSK
    ]
  });
  Schedules.registerSchedule({
    "name" : "heaters_schedule",
    "intervals" : [
       [ [4, 0], [17, 0] ],  // всё ещё UTC, 07:00 - 20:00 MSK дневной режим
    ]
  });
  Schedules.initSchedules();

  // Вывеска и фасадное освещение
  defineRule("signboardOnOff", {
    when: function() {
      return dev._schedules.signboard || true;
    },
    then: function (newValue, devName, cellName) {
      log("signboardOnOff  newValue={}, devName={}, cellName={}", newValue, devName, cellName);
      var on = dev._schedules.signboard; //
      
      dev["wb-mr6c_80/K2"] = !on;
      dev["wb-mr6c_80/K1"] = !on;
      dev["wb-mr6c_80/K3"] = !on;
    }
  });

  // Освещение торгового зала
  defineRule("lightingFrontshopOnOff", {
    when: function() {
      return dev._schedules.frontshop_lighting || true;
    },
    then: function (newValue, devName, cellName) {
      log("lightingFrontshopOnOff  newValue={}, devName={}, cellName={}", newValue, devName, cellName);
      dev["wb-gpio/EXT1_R3A1"] = ! dev._schedules.frontshop_lighting; //инвертированный контактор
    }
  });

  // Вентиляция подсобного помещения
  defineRule("ventBackstoreOnOff", {
    when: function() {
      return dev._schedules.ext_working_hours_15m || true;
    },
    then: function (newValue, devName, cellName) {
      log("ventBackstoreOnOff  newValue={}, devName={}, cellName={}", newValue, devName, cellName);
      var on = dev._schedules.ext_working_hours_15m;
      dev["wb-mr6c_81/K1"] = ! on; //инвертированный контактор
      dev["wb-mr6c_81/K5"] = ! on; //инвертированный контактор
    }
  });

  // Освещение холодильных горок
  defineRule("lightingCoolingshelfsOnOff", {
    when: function() {
      return dev._schedules.frontshop_lighting || true;
    },
    then: function (newValue, devName, cellName) {
      log("lightingCoolingshelfsOnOff  newValue={}, devName={}, cellName={}", newValue, devName, cellName);
      var on = dev._schedules.working_hours_15m;

      // освещение в горках через нормально-закрытые реле (инвертировано)
      dev["wb-mrm2-old_60/Relay 1"] = !on;
      dev["wb-mrm2-old_61/Relay 1"] = !on;
      dev["wb-mrm2-old_62/Relay 1"] = !on;
      dev["wb-mrm2-old_63/Relay 1"] = !on;
      dev["wb-mrm2-old_64/Relay 1"] = !on;
      dev["wb-mrm2-old_65/Relay 1"] = !on;
      dev["wb-mrm2-old_66/Relay 1"] = !on;
      dev["wb-mrm2-old_67/Relay 1"] = !on;
    }
  });

  //Брендовые холодильники (пиво, лимонады)
  defineRule("powerBrandFridgesOnOff", {
    when: function() {
      return dev._schedules.working_hours || true;
    },
    then: function (newValue, devName, cellName) {
      log("powerBrandFridgesOnOff  newValue={}, devName={}, cellName={}", newValue, devName, cellName);
      var on = dev._schedules.working_hours;
      
      dev["wb-gpio/EXT1_R3A5"] = !on; // инвертировано
    }
  });

  // ========= Котлы и приточная вентиляция ТЗ  ===========
  // обратная связь по температуре овощной зоны

  // днём работает позиционный регулятор
  defineRule("heatersDayOff", {
    when: function() {
      return (dev._schedules.heaters_schedule) && (vegetablesTemperature > 17.0);
    },
    then: function (newValue, devName, cellName) {
      log("heatersDayOff  newValue={}, devName={}, cellName={}", newValue, devName, cellName);
      heater1EnableInverted = !false; // инвертировано
    }
  });

  defineRule("heatersDayOn", {
    when: function() {
      return (dev._schedules.heaters_schedule) && (vegetablesTemperature < 16.7);
    },
    then: function (newValue, devName, cellName) {
      log("heatersDayOn  newValue={}, devName={}, cellName={}", newValue, devName, cellName);
      heater1EnableInverted = !true; // инвертировано
    }
  });

  // ночью работает позиционный регулятор
  defineRule("heatersNightOff", {
    when: function() {
      return (!dev._schedules.heaters_schedule) && (vegetablesTemperature > 11.6);
    },
    then: function (newValue, devName, cellName) {
      log("heatersNightOff  newValue={}, devName={}, cellName={}", newValue, devName, cellName);
      heater1EnableInverted = !false; // инвертировано
    }
  });

  defineRule("heatersNightOn", {
    when: function() {
      return (!dev._schedules.heaters_schedule) && (vegetablesTemperature < 11.3);
    },
    then: function (newValue, devName, cellName) {
      log("heatersNightOn  newValue={}, devName={}, cellName={}", newValue, devName, cellName);
      heater1EnableInverted = !true; // инвертировано
    }
  });

  // приточная и вытяжная вентиляция принудительно выключены
  
  defineRule("ventFrontshopAlwaysOff", {
    when: cron("@every 1m"),
    then: function() {
  		dev["wb-gpio/EXT1_R3A3"] = !false;
  		dev["wb-gpio/EXT1_R3A4"] = !false;
    }
  });
  
  // ==================  Кассовая зона =================

  // в кассовой зоне в рабочее время температура поддерживается кондиционерами (позиционный регулятор)

  defineRule("countersACOn", {
    when: function() {
      return (dev._schedules.working_hours_15m) && (countersTemperature < 17.7);
    },
    then: function (newValue, devName, cellName) {
      log("countersACOn  newValue={}, devName={}, cellName={}", newValue, devName, cellName);
      dev["wb-mir_75/Play from ROM7"] = true; // кондиционер кассовой зоны на нагрев
    }
  });

  // в нерабочее время кондиционер выключен
  defineRule("countersACOff", {
    when: function() {
      return (!dev._schedules.working_hours_15m) || (countersTemperature > 18.0);
    },
    then: function (newValue, devName, cellName) {
      log("countersACOff  newValue={}, devName={}, cellName={}", newValue, devName, cellName);
      dev["wb-mir_75/Play from ROM2"] = true; // кондиционер кассовой зоны выключить
    }
  });

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

  defineRule("acVegOn", {
    when: function() {
      return vegetablesTemperature >= 18.5
    },
    then: function (newValue, devName, cellName) {
      log("acVegOn  newValue={}, devName={}, cellName={}", newValue, devName, cellName);
      dev["wb-mir_76/Play from ROM3"] = true; // Охлаждение +18
    }
  });

  defineRule("acVegOff", {
    when: function() {
      return vegetablesTemperature < 17.8
    },
    then: function (newValue, devName, cellName) {
      log("acVegOff  newValue={}, devName={}, cellName={}", newValue, devName, cellName);
      dev["wb-mir_76/Play from ROM2"] = true; // выключить
    }
  });
})()