Примеры правил

Материал из Wiren Board
Версия от 16:30, 28 апреля 2020; Fizikdaos (обсуждение | вклад) (Новая страница: «{{DISPLAYTITLE: Примеры правил}} === Слежение за контролом === <!--T:20--> <!--T:21--> Это простейшее правило…»)

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

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

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

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


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

<!--T:26-->
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;

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

<!--T:28-->


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

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


<!--T:31-->
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;

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

<!--T:33-->


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

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

Правило включает свет при обнаружении движения и выключает свет, спустя 30 секунд после пропадания сигнала с датчика движения.

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

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

<!--T:40-->
defineRule("motion_detector_1", {
  whenChanged: "wb-gpio/D2_IN",
  then: function (newValue, devName, cellName) {
    if (newValue) {
        dev["wb-gpio"]["Relay_1"] = 1;

      	<!--T:41-->
if (motion_timer_1_id) {
          clearTimeout(motion_timer_1_id);
       }
      
        motion_timer_1_id = setTimeout(function () {
   		   dev["wb-gpio"]["Relay_1"] = 0;         
                   motion_timer_1_id = null;   
       	}, motion_timer_1_timeout_ms);           	
    }
   }
});

<!--T:42-->


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

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

 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] = 1;
              if (motion_timer_id) {
                  clearTimeout(motion_timer_id);
              }

              <!--T:46-->
motion_timer_id = setTimeout(function() {
                  dev["wb-gpio"][relay_control] = 0;
                  motion_timer_id = null;
              }, timeout_ms);
          }
      }
  });
}

<!--T:47-->
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();

    <!--T:51-->
// 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);

    <!--T:52-->
// 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";

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

  <!--T:58-->
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); 
      };

      <!--T:59-->
relay_up_timer_id = setTimeout(function() {
        return dev[relay_up_device][relay_up_control] = 0;
      }, timeout_s * 1000);
    }
  });

  <!--T:60-->
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);
    }
  });

  <!--T:61-->
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); 
      };

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

      
      <!--T:63-->
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-ов:

<!--T:65-->
(function() {
  defineAlias("relay_up_1", "lc103_4/Relay 1");
  defineAlias("relay_down_1", "lc103_4/Relay 2");
  var timeout_s = 15;

  <!--T:66-->
defineRule("roller_shutter_1_up_on", {
   asSoonAs: function() {
     return relay_up_1;
   },
    then: function () {
      setTimeout(function() {
        relay_up_1 = 0;
      }, timeout_s * 1000);
    }
  });

  <!--T:67-->
defineRule("roller_shutter_1_down_on", {
    asSoonAs: function() {
      return relay_down_1;
    },
    then: function () {
      setTimeout(function() {
        relay_down_1 = 0;
      }, timeout_s * 1000);
    }
  });

  <!--T:68-->
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");
    }
  });
})();

<!--T:69-->


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

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

Полный список правил в репозитории.

Некоторые примеры:


Правило для пищалки

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


defineVirtualDevice("buzzer", {
  title: "Buzzer", //

  <!--T:77-->
cells: {
    frequency : {
        type : "range",
        value : 3000,
        max : 7000,
    },
    volume : {
        type : "range",
        value : 10,
        max : 100,
    },
    enabled : {
        type : "switch",
        value : false,
    },
  }
});


<!--T:78-->
// setup pwm2
runShellCommand("echo 2 > /sys/class/pwm/pwmchip0/export");



<!--T:79-->
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);


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


<!--T:81-->
defineRule("_system_buzzer_params", {
  whenChanged: [
    "buzzer/frequency",
    "buzzer/volume",
    ],

  <!--T:82-->
then: function (newValue, devName, cellName) {
    if ( dev.buzzer.enabled) {
        _buzzer_set_params();
    }
  }
});


<!--T:83-->
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");
    }
   }
});



<!--T:84-->


Правило для статуса питания

Правило создаёт виртуальное устройство, которое сообщает текущий статус питания. В качестве входных данных используется два канала АЦП: измерение напряжения на аккумуляторе и измерение входного напряжения.

Реализована следующая логика:

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

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


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

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

  <!--T:93-->
cells: {
    'working on battery' : {
        type : "switch",
        value : false,
        readonly : true
    },
    'Vin' : {
        type : "voltage",
        value : 0
    }


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



<!--T:95-->
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"] ;
        }
    }
});



<!--T:96-->
defineRule("_system_dc_on", {
  asSoonAs: function () {
    return  dev["wb-adc"]["Vin"] > dev["wb-adc"]["BAT"];
  },
  then: function () {
    dev["power_status"]["working on battery"] = false;
  }
});

<!--T:97-->
defineRule("_system_dc_off", {
  asSoonAs: function () {
    return  dev["wb-adc"]["Vin"] <= dev["wb-adc"]["BAT"];
  },
  then: function () {
    dev["power_status"]["working on battery"] = true;
  }
});

<!--T:98-->


Отправка команд по 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");
}

<!--T:121-->


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

<!--T:124-->
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");
  }
});

<!--T:125-->


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



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

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

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


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

<!--T:131-->
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");
  }
});

<!--T:132-->
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");
  }
});

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

<!--T:134-->


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

Чтобы дать пользователю возможность вводить точные значения параметров (уставки) из интерфейса, можно воспользоваться инструкцией.

Более подробно и с примером - в теме на портале техподдержки.


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

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

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

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

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

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

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


lib_schedules.js:

global.__proto__.Schedules = {};

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

  <!--T:146-->
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;
  }


  <!--T:147-->
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));

    <!--T:148-->
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)

      <!--T:149-->
// 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;
      }

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

      <!--T:151-->
if (now >= start_date) {
        return true;
      }
    }
    return false;

  <!--T:152-->
}

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

    <!--T:154-->
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);
  };

  <!--T:155-->
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);
          }
        });
      }
    }    
  }

  <!--T:156-->
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];
      }
    });
  }

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

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

  <!--T:159-->
Schedules.initSchedules = function() {
    var params = {
      title: "Schedule Status", 
      cells: {}
    };

    <!--T:160-->
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};
      }
    };

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



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

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

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

        <!--T:165-->
// set schedule dev status as soon as possible at startup
        (function(schedule) {
          setTimeout(function() {
            updateSingleScheduleDevStatus(schedule);
          }, 1);
        })(schedule);

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

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

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

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

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

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

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

  <!--T:172-->
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();

  <!--T:173-->
// Вывеска и фасадное освещение
  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;
    }
  });


  <!--T:174-->
// Освещение торгового зала
  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; //инвертированный контактор
    }
  });


  <!--T:175-->
// Вентиляция подсобного помещения
  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; //инвертированный контактор
    }
  });

  <!--T:176-->
// Освещение холодильных горок
  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;

      <!--T:177-->
// освещение в горках через нормально-закрытые реле (инвертировано)
      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;
    }
  });


  <!--T:178-->
//Брендовые холодильники (пиво, лимонады)
  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; // инвертировано
    }
  });


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

  <!--T:180-->
// днём работает позиционный регулятор
  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; // инвертировано
    }
  });

  <!--T:181-->
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; // инвертировано
    }
  });

  <!--T:182-->
// ночью работает позиционный регулятор
  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; // инвертировано
    }
  });

  <!--T:183-->
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; // инвертировано
    }
  });


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

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

  <!--T:186-->
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; // кондиционер кассовой зоны на нагрев
    }
  });

  <!--T:187-->
// в нерабочее время кондиционер выключен
  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; // кондиционер кассовой зоны выключить
    }
  });

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

  <!--T:189-->
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
    }
  });

  <!--T:190-->
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; // выключить
    }
  });
})()