Движок правил wb-rules: различия между версиями
Метка: visualeditor |
|||
Строка 659: | Строка 659: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
=== Сложные правила с расписаниями === | |||
Обьект - продуктовый магазин. Различные системы магазина управляются по обратной связи от датчиков температуры и с учётом расписания работы магазина. | |||
Для расписаний использются не cron-правила, а обёртка над ними. Обёртка включает и выключает правила, которые, в отличие от cron-правил, выполняются постоянно, будучи включенными. | |||
Например мы хотим, чтобы освещение было включено с 10 утра до 17 вечера. Обёртка (libschedule) будет выполнять правило "включить освещением" раз в минуту с 10 утра до 17 вечера. | |||
Это значит, что даже если контроллер работает с перерывами и пропустил время перехода между расписаниями (10 утра), то контроллер всё равно включит отвещение при первой возможности. | |||
lib_schedules.js: | |||
<syntaxhighlight lang="ecmascript"> | |||
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); | |||
</syntaxhighlight> | |||
Пример правил, с использованием Schedules: | |||
<syntaxhighlight lang="ecmascript"> | |||
(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; // выключить | |||
} | |||
}); | |||
})() | |||
</syntaxhighlight> | |||