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

Материал из Wiren Board
Метки: правка с мобильного устройства правка из мобильной версии Расширенная мобильная правка
(Отметить эту версию для перевода)
Строка 78: Строка 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",
Строка 311: Строка 312:
})();
})();


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


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


<!--T:195-->
defineRule("water_meter_1", {
defineRule("water_meter_1", {
     whenChanged: "wb-mcm8_29/Input 1 counter",
     whenChanged: "wb-mcm8_29/Input 1 counter",
Строка 342: Строка 346:
</syntaxhighlight>
</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: Строка 368:
# Короткое, а затем длинное — уменьшает яркость.
# Короткое, а затем длинное — уменьшает яркость.


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


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


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




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




<!--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: Строка 414:
});
});


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




<!--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: Строка 446:
});
});


<!--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: Строка 453:
     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: Строка 463:
});
});


<!--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/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;">
Строка 487: Строка 510:
</syntaxhighlight>
</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">
Строка 511: Строка 535:
== Системные правила == <!--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>.  


Строка 675: Строка 700:
</syntaxhighlight>
</syntaxhighlight>


== Термостат ==
== Термостат == <!--T:223-->
Пример простого термостата из [https://support.wirenboard.com/t/novaya-versiya-dvizhka-pravil/4196/158 темы на портале поддержки].
Пример простого термостата из [https://support.wirenboard.com/t/novaya-versiya-dvizhka-pravil/4196/158 темы на портале поддержки].


<!--T:224-->
<syntaxhighlight lang="js">
<syntaxhighlight lang="js">
defineVirtualDevice("Termostat", {
defineVirtualDevice("Termostat", {
Строка 698: Строка 724:
    },.......
    },.......


<!--T:225-->
var hysteresis = 0.5;
var hysteresis = 0.5;
function Termostat(name, temp, setpoint, TS, TS_onoff) {
function Termostat(name, temp, setpoint, TS, TS_onoff) {
Строка 716: Строка 743:
}
}


<!--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"); // Прихожая теплый пол
Termostat("R01-TS16-1", "A60-M1W3/External Sensor 1", "Termostat/R01-TS16-1-setpoint", "wb-gpio/EXT4_R3A1", "Termostat/R01-TS16-1-onoff"); // Прихожая теплый пол


Строка 899: Строка 927:
</syntaxhighlight>
</syntaxhighlight>


== Отправка сообщения через Telegram-бота ==
== Отправка сообщения через Telegram-бота == <!--T:227-->
{{Anchor|telegram}}
{{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"; // напишите свой текст сообщения
Строка 909: Строка 938:
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", {
Строка 933: Строка 964:


   
   
<!--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))
Строка 946: Строка 978:




<!--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|Пример скрипта]]
Строка 954: Строка 987:
Но сложную настройку с меню и вариантами так не сделать.
Но сложную настройку с меню и вариантами так не сделать.


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


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


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


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


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


Строка 1444: Строка 1493:
   });
   });


   <!--T:190-->
   defineRule("acVegOff", {
defineRule("acVegOff", {
     when: function() {
     when: function() {
       return vegetablesTemperature < 17.8
       return vegetablesTemperature < 17.8
Строка 1456: Строка 1504:
})()
})()
</syntaxhighlight>
</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>

Версия 20:26, 21 сентября 2022

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

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

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

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

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

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

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

  }
});

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

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

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

  }
});

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

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

Освещение подключено через встроенное реле, соответствующий канал wb-gpio/Relay_1.

Правило работает так:

  • когда движение появляется, свет включается. Если ранее был запущен тридцатисекундный таймер "на выключение", этот таймер отключается;
  • когда движение пропадает, запускается тридцатисекундный таймер "на выключение". Если ему удаётся дойти до конца, свет выключается.
var motion_timer_1_timeout_ms = 30 * 1000;
var motion_timer_1_id = null;

defineRule("motion_detector_1", {
    whenChanged: "wb-gpio/D2_IN",
    then: function (newValue, devName, cellName) {
        if (newValue) {
            dev["wb-gpio"]["Relay_1"] = true;
            if (motion_timer_1_id) {
                clearTimeout(motion_timer_1_id);
            }
            motion_timer_1_id = setTimeout(function () {
                dev["wb-gpio"]["Relay_1"] = false;
                motion_timer_1_id = null;
            }, motion_timer_1_timeout_ms);
        }
    },
});

Создание однотипных правил

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

 function makeMotionDetector(name, timeout_ms, detector_control, relay_control) {
  var motion_timer_id = null;
  defineRule(name, {
      whenChanged: "wb-gpio/" + detector_control,
      then: function(newValue, devName, cellName) {
          if (!newValue) {
              dev["wb-gpio"][relay_control] = true;
              if (motion_timer_id) {
                  clearTimeout(motion_timer_id);
              }

              motion_timer_id = setTimeout(function() {
                  dev["wb-gpio"][relay_control] = false;
                  motion_timer_id = null;
              }, timeout_ms);
          }
      }
  });
}

makeMotionDetector("motion_detector_1", 20000, "EXT1_DR1", "EXT2_R3A1");
makeMotionDetector("motion_detector_2", 10000, "EXT1_DR2", "EXT2_R3A2");
makeMotionDetector("motion_detector_3", 10000, "EXT1_DR3", "EXT2_R3A3");

Активация правила только в определённое время

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

var motion_timer_1_timeout_ms = 5 * 1000;
var motion_timer_1_id = null;
 
defineRule("motion_detector_1", {
  whenChanged: "wb-gpio/A1_IN",
  then: function (newValue, devName, cellName) {
    var date = new Date();

    // time point marking the beginning of the interval
    // i.e. "today, at HH:MM". All dates are in UTC!
    var date_start = new Date(date);
    date_start.setHours(9);
    date_start.setMinutes(30);

    // time point marking the end of the interval
    var date_end = new Date(date);
    date_end.setHours(17);
    date_end.setMinutes(10);
    
    // if time is between 09:30 and 17:10 UTC
    if ((date > date_start) && (date < date_end)) {
      if (newValue) {
          dev["wb-gpio"]["EXT1_R3A1"] = 1;
   
          if (motion_timer_1_id) {
            clearTimeout(motion_timer_1_id);
         }
   
          motion_timer_1_id = setTimeout(function () {
             dev["wb-gpio"]["EXT1_R3A1"] = 0;            
             motion_timer_1_id = null;
          }, motion_timer_1_timeout_ms);              
      }
     }
   }
});

Роллеты

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

Кроме этого, правило отключает двигатели спустя заданное время после включения.

(function() { //don't touch this line
  
  var suffix = "1"; // must be different in different JS files
 
  var relay_up_device = "lc103_4";
  var relay_up_control = "Relay 1";

  var relay_down_device = "lc103_4";
  var relay_down_control = "Relay 2";

  var timeout_s = 15;
  
  // End of settings
  
  
  var relay_up_timer_id = null;
  var relay_down_timer_id = null;
  
  defineRule( "roller_shutter_up_on" + suffix, {
   asSoonAs: function() {
     return dev[relay_up_device][relay_up_control];
   },
    then: function () {
      if (relay_up_timer_id) {
        relay_up_timer_id = clearTimeout(relay_up_timer_id); 
      };

      relay_up_timer_id = setTimeout(function() {
        return dev[relay_up_device][relay_up_control] = 0;
      }, timeout_s * 1000);
    }
  });

  defineRule("roller_shutter_down_on" + suffix, {
    asSoonAs: function() {
      return dev[relay_down_device][relay_down_control];
    },
    then: function () {
      if (relay_down_timer_id) {
        relay_down_timer_id = clearTimeout(relay_down_timer_id); 
      };
      
      relay_down_timer_id = setTimeout(function() {
        dev[relay_down_device][relay_down_control] = 0;
      }, timeout_s * 1000);
    }
  });

  defineRule("roller_shutter_both_on" + suffix, {
    asSoonAs: function() {
      return dev[relay_up_device][relay_up_control] && dev[relay_down_device][relay_down_control];
    },
    then: function () {
      if (relay_up_timer_id) {
        relay_up_timer_id = clearTimeout(relay_up_timer_id); 
      };

      if (relay_down_timer_id) {
        relay_down_timer_id = clearTimeout(relay_down_timer_id); 
      };

      
      dev[relay_up_device][relay_up_control] = 0;
      dev[relay_down_device][relay_down_control] = 0;
      log("Both roller shutter relays on, switching them off");
    }
  });
})();

Более старая версия того же сценария демонстрирует использование alias-ов:

(function() {
  defineAlias("relay_up_1", "lc103_4/Relay 1");
  defineAlias("relay_down_1", "lc103_4/Relay 2");
  var timeout_s = 15;

  defineRule("roller_shutter_1_up_on", {
   asSoonAs: function() {
     return relay_up_1;
   },
    then: function () {
      setTimeout(function() {
        relay_up_1 = 0;
      }, timeout_s * 1000);
    }
  });

  defineRule("roller_shutter_1_down_on", {
    asSoonAs: function() {
      return relay_down_1;
    },
    then: function () {
      setTimeout(function() {
        relay_down_1 = 0;
      }, timeout_s * 1000);
    }
  });

  defineRule("roller_shutter_1_both_on", {
    asSoonAs: function() {
      return relay_up_1 && relay_down_1;
    },
    then: function () {
      relay_up_1 = 0;
      relay_down_1 = 0;
      log("Both roller shutter relays on, switching them off");
    }
  });
})();

Импульсные счетчики

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

var meterCorrection = 123120 // Корректировочное значение счетчика в литрах
var counterCorrection = 7 // Корректировочное значение WB-MCM8 в импульсах
var inpulseValue = 10 // Количество литров на один импульс

defineVirtualDevice("water_meters", { // Создаем виртуальный девайс для отображения в веб интерфейсе.
    title: "Счетчики воды",
    cells: {
        water_meter_1: {
            type: "value",
            value: 0
        },
    }
});

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

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

Описание

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

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

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

Примеры

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

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

  1. Короткое нажатие включает канал.
  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;
        }
    }
});

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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Отправка сообщения через Telegram-бота

Сообщения отправляются с использованием Telegram API через curl.

var message = "Text"; // напишите свой текст сообщения
var token = "TOKEN"; // замените на токен бота
var chat_id = CHATID; // замените на свой chat_id
var command = 'curl -s -X POST https://api.telegram.org/bot{}/sendMessage -d chat_id={} -d text="{}"'.format(token, chat_id, message);

runShellCommand(command);

Обработка ошибок в работе с serial-устройствами

Реализована через подписку на все топики meta/error.

defineVirtualDevice("meta_error_test", {
  title: "Metaerordisplay",
  cells: {
      topic: {
      type: "text",
      value: "",
      readonly: true
    },
    value: {
      type: "text",
      value: "",
      readonly: true
    },
  }
});

 
trackMqtt("/devices/+/controls/+/meta/error", function(message){
  log.info("name: {}, value: {}".format(message.topic, message.value))
  if (message.value=="r"){
  	dev["meta_error_test/topic"] = message.topic;
    dev["meta_error_test/value"] = message.value;
  }
  
});

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

service wb-mqtt-confed restart

service wb-rules restart

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

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

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

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

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

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

lib_schedules.js:

global.__proto__.Schedules = {};

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

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

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

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

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

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

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

  }

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

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

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

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

  var _schedules = {};

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

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

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

    defineVirtualDevice("_schedules", params);


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

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

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

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

      };
    };
};

})(Schedules);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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