node-red-contrib-vib-smart-scheduler
Version:
Node Red Smart Scheduler MQTT Home Assistant
991 lines (823 loc) • 44.7 kB
HTML
<script type="text/javascript">
// Terminal command node-red -v -D logging.console.level=trace
var eColor="#558006";
var ctrl_i=0;
RED.nodes.registerType('smart-scheduler',{
category: 'vib-node',
color: '#bdeeff',
defaults: {
mqttSettings: {value: "", type: "smart-scheduler-settings"},
name: {value:"smart-scheduler"},
rules:{value:[{setName:"Confort",setColor:"#CCCCCC",spTemp:"19"}],validate: function(rules, opt) {
return true;
}},
schedules:{value:"[]"},
activScheduleId:{value:""},
triggerMode:{value:"triggerMode.statechange.startup",required: true},
topic:{value:""},
defaultSp:{value:"5",required: true,validate:function(v) {
if (isNaN(v)){
return false;
}
if (v<1 || v> 35){
//this.warn("defaultSp need to be >0 et <= 35")
return false;
}
return true;
} },
allowOverride:{value:false},
executionMode:{value:"auto"},
overrideDuration:{value:"120",validate:RED.validators.number() },
overrideTs:{value:"5"},
overrideSp:{value:"0",required: true, validate:function(v) {
if (isNaN(v)){
return false;
}
if (v<0 || v> 35){
//this.warn("overrideSp need to be >0 et <= 35")
return false;
}
return true;
}},
settingChanged:{value:"0",validate:function(v) {
return true;
}},
uniqueId:{value:"SmartScheduler_1",required: true,validate:function(v) {
return true;
}},
cycleDuration:{value:1,required:true,validate:function(v){
if (isNaN(v)){
return false;
}
if (v<1 || v> 60){
//this.warn("overrideSp need to be >1 et <= 60")
return false;
}
return true
}},
debugInfo:{value:"none"}
},
inputs:1,
outputs:1,
icon: "font-awesome/fa-calendar",
label: function() {
this.nlog=function(message){
// if (this.debugInfo){
console.log("Smart-Scheduler["+this.name+"]:"+message);
// }
}
return this.name || "smart-scheduler";
},
button: {
onclick: function() {
let node=this;
$.ajax({
url: "smartsched/" + node.id,
type: "POST",
data: JSON.stringify({payload:{command:"trigger"}}),
contentType: "application/json; charset=utf-8",
success: function (resp) {
RED.notify(node._("smart-scheduler trigger success", { label: "ss" }), { type: "success", id: "inject", timeout: 2000 });
},
error: function (jqXHR, textStatus, errorThrown) {
if (jqXHR.status == 404) {
RED.notify(node._("common.notification.error", { message: node._("common.notification.errors.not-deployed") }), "error");
} else if (jqXHR.status == 500) {
RED.notify(node._("common.notification.error", { message: node._("inject.errors.failed") }), "error");
} else if (jqXHR.status == 0) {
RED.notify(node._("common.notification.error", { message: node._("common.notification.errors.no-response") }), "error");
} else {
RED.notify(node._("common.notification.error", { message: node._("common.notification.errors.unexpected", { status: jqXHR.status, message: textStatus }) }), "error");
}
}
});
}
},
oneditprepare: function() {
var node = this;
node.fromMoment = function(m) {
m = moment(m);
return {
dow: m.day(),
mod: m.hours() * 60 + m.minutes(),
};
};
node.toMoment = function(o) {
var day = o.dow == 0 ? 7 : o.dow;
var m = moment('2018-01-0' + day + ' 00:00:00');
m.hours(Math.floor(o.mod / 60));
m.minutes(o.mod % 60);
return m;
};
node.maxRuleId=0;
node.isMaxRuleId=function(id){
if (id>node.maxRuleId){
node.maxRuleId=id;
return true
}
return false;
}
node.activeSched={};
node.maxEventId=0;
node.isMaxEventId=function(id){
if (id>node.maxEventId){
node.maxEventId=id;
return true
}
return false;
}
node.maxSchedId=0;
node.isMaxSchedId=function(id){
if (id>node.maxSchedId){
node.maxSchedId=id;
return true
}
return false;
}
node.doInject=function(){
$.ajax({
url: "inject/" + node.id,
type: "POST",
data: JSON.stringify({}),
contentType: "application/json; charset=utf-8",
success: function (resp) {
RED.notify(node._("inject.success", { label: "ss" }), { type: "success", id: "inject", timeout: 2000 });
},
error: function (jqXHR, textStatus, errorThrown) {
if (jqXHR.status == 404) {
RED.notify(node._("common.notification.error", { message: node._("common.notification.errors.not-deployed") }), "error");
} else if (jqXHR.status == 500) {
RED.notify(node._("common.notification.error", { message: node._("inject.errors.failed") }), "error");
} else if (jqXHR.status == 0) {
RED.notify(node._("common.notification.error", { message: node._("common.notification.errors.no-response") }), "error");
} else {
RED.notify(node._("common.notification.error", { message: node._("common.notification.errors.unexpected", { status: jqXHR.status, message: textStatus }) }), "error");
}
}
});
}
var configEventArray=function (ar){
// 2 - We remove all events
node.maxEventId=0;
if (node.calendar){
var listEvent = node.calendar.getEvents();
listEvent.forEach(event => {
event.remove()
});
}
if (ar===undefined || ar==null)
return;
ar.forEach(function(e) {
let r=node.rules.find(({ruleIdx}) => ruleIdx==e.ruleIdx)
if (r===undefined)
return;
node.isMaxEventId(e.id);
var eventData = {
id: e.id,
title: r.setName,
start: e.start,
end: e.end,
stick: true,
backgroundColor: r.setColor,
ruleIdx:e.ruleIdx
};
node.calendar.addEvent(eventData);
});
node.calendar.render();
}
function EmptySchedule(){
if (node.calendar){
var listEvent = node.calendar.getEvents();
listEvent.forEach(event => {
event.remove()
});
node.activeSched.events=[];
}
}
function cloneDailySchedule(src,dest){
// Iterate to delete any existing event on the dest day
if (node.calendar){
var listEvent = node.calendar.getEvents();
listEvent.forEach(event => {
let m_e = moment(event.endStr);
let s_dow=m_e.day();
if (s_dow==dest){
node.activeSched.events = node.activeSched.events.filter(function(e) {
return event.id != e.id;
});
event.remove();
}
});
listEvent.forEach(event => {
let m_s = moment(event.startStr);
let m_e = moment(event.endStr);
let s_dow=m_s.day();
if (s_dow==src){
let newStart="2018-01-0"+dest+" "+m_s.format('HH:mm:ss');
let newEnd="2018-01-0"+dest+" "+m_e.format('HH:mm:ss');
let ev=node.activeSched.events.filter(function(e) {
return event.id == e.id;
});
let r=node.rules.find(({ruleIdx}) => ruleIdx==ev[0].ruleIdx)
node.maxEventId++;
var eventData = {
id: node.maxEventId,
title: r.setName,
start: newStart,
end: newEnd,
s_dow:dest,
s_mod:ev[0].s_mod,
e_dow:dest,
e_mod:ev[0].e_mod,
stick: true,
backgroundColor: r.setColor,
ruleIdx:ev[0].ruleIdx
};
node.calendar.addEvent(eventData);
node.activeSched.events.push(eventData);
}
});
}
}
var setup = function(node) {
node.nlog("start setup");
//$("#node-input-triggerMode").val(node.triggerMode ? node.triggerMode : 'triggerMode.statechange.startup');
// $("#node-input-executionMode").val(node.override ? node.override : 'auto');
node.configuredSchedules=[];
if (node.schedules===undefined){
node.schedules="[]";
}
var colorPicker = new iro.ColorPicker("#colorPicker", {
width: 250,
color: "#f00"
});
function colorChangeCallback(color) {
console.log("ctrl colorChangeCallback:"+ctrl_i);
$("#node-input-temp-color_"+ctrl_i).val(color.hexString);
$("#cc_btn_"+ctrl_i).css('background-color',color.hexString);
node.rules.find(({ruleIdx}) => ruleIdx==ctrl_i).setColor=color.hexString;
}
colorPicker.on("color:change", colorChangeCallback);
let mm=moment();
let dt='2018-01-0'+mm.day()+" "+mm.format('HH:mm:ss');
var calendarEl = document.querySelector('#calendar');
node.calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'timeGridWeek',
initialDate:'2018-01-01 00:00:00',
headerToolbar:false,
dayHeaders:true,
allDaySlot:false,
slotEventOverlap:false,
firstDay: 1,
nowIndicator:true,
now:dt,
dayHeaderFormat:{ weekday: 'short' },
duration: '00:05:00',
snapDuration: '00:05:00',
slotMinTime:'00:00:00',
slotDuration:'00:30:00',
scrollTime: '06:00:00',
slotLabelInterval: '01:00:00',
selectable: true,
editable: true,
eventOverlap:false,
eventTimeFormat:{
hour: 'numeric',
minute: '2-digit',
meridiem: false,
meridiem: false,
hour12: false
},
slotLabelFormat:{
hour: 'numeric',
minute: '2-digit',
omitZeroMinute: false,
meridiem: false,
hour12: false
},
eventClick: function(info) {
if (node.activeSched===undefined || ctrl_i!=-1)
return;
node.activeSched.events = node.activeSched.events.filter(function(e) {
return info.event.id != e.id;
});
info.event.remove();
},
select: function(info) {
//node.nlog("select start:"+info.startStr+", end:"+info.endStr+ ", ctrl_i:"+ctrl_i);
//console.log("select info:"+JSON.stringify(info));
if (node.activeSched===undefined || ctrl_i==-1){
node.nlog("select ignored, no active schedule or no rule selected");
return;
}
end=info.endStr;
start=info.startStr;
m_s = moment(start);
m_e = moment(end);
s_dow=m_s.day();
s_mod=m_s.hours()*60+m_s.minutes();
e_dow=m_e.day();
e_mod=m_e.hours()*60+m_e.minutes();
let r=node.rules.find(({ruleIdx}) => ruleIdx==ctrl_i)
if (r===undefined){
alert("Please select a set point before adding schedule events");
console.log("err while finding rule for ruleIdx:"+ctrl_i);
node.rules.forEach( rule => {
node.nlog(" Available ruleIdx:"+rule.ruleIdx);
if (parseInt(rule.ruleIdx)===parseInt(ctrl_i)){
node.nlog(" Found matching rule:"+JSON.stringify(rule));
}
});
node.nlog("r is undefined returning");
return;
}
node.maxEventId++;
var eventData = {
id: node.maxEventId,
title: r.setName,
start: start,
end: end,
s_dow:s_dow,
s_mod:s_mod,
e_dow:e_dow,
e_mod:e_mod,
stick: true,
backgroundColor: r.setColor,
ruleIdx:ctrl_i
};
node.calendar.addEvent(eventData);
node.activeSched.events.push(eventData);
},
eventResize: function(info) {
let found=0;
if (node.activeSched===undefined)
return;
node.activeSched.events.forEach(function(e) {
if(info.event.id == e.id) {
console.log("info.event.id:"+info.event.id+", e.id:"+e.id);
found++;
m_s = moment(info.event.startStr);
m_e = moment(info.event.endStr);
e.s_dow=m_s.day();
e.s_mod=m_s.hours()*60+m_s.minutes();
e.e_dow=m_e.day();
e.e_mod=m_e.hours()*60+m_e.minutes();
e.end=info.event.endStr;
e.start=info.event.startStr;
}
console.log('found:'+found);
});
},
eventDrop: function(info) {
if (node.activeSched===undefined)
return;
node.activeSched.events.forEach(function(e) {
if(info.event.id == e.id) {
m_s = moment(info.event.startStr);
m_e = moment(info.event.endStr);
e.s_dow=m_s.day();
e.s_mod=m_s.hours()*60+m_s.minutes();
e.e_dow=m_e.day();
e.e_mod=m_e.hours()*60+m_e.minutes();
e.end=info.event.endStr;
e.start=info.event.startStr;
}
});
}
});
node.configuredSchedules=JSON.parse(node.schedules);
node.configuredSchedules.forEach(function(e){
node.isMaxSchedId(e.idx);
$('#node-input-activeSchedule').append($('<option>', {value:e.idx, text:e.name}));
if (e.idx==node.activScheduleId){
$("#node-input-activeSchedule").val(node.activScheduleId).change();
node.activeSched=e;
$("#node-input-scheduleName").val(node.activeSched.name);
configEventArray(e.events);
}
})
$("#node-input-addSchedule").on("click", function(e) { // Add a new schedule to the list
node.maxSchedId++;
$('#node-input-activeSchedule').append($('<option>', {value:node.maxSchedId, text:'New schedule '+node.maxSchedId}));
node.configuredSchedules.push({name:'New schedule '+node.maxSchedId,idx:node.maxSchedId,events:[]})
$("#node-input-activeSchedule").val(node.maxSchedId).change();
node.activeSched = node.configuredSchedules[node.configuredSchedules.length - 1];
$("#node-input-scheduleName").prop("disabled", false).val(node.activeSched.name);
requestAnimationFrame(function () {
if (node.calendar) {
node.calendar.updateSize();
node.calendar.render();
}
});
});
$("#node-input-deleteSchedule").on("click", function(event) { // Delete current schedule
let id=$("#node-input-activeSchedule").find(":selected").val();
$("#node-input-activeSchedule").find('[value="'+id+'"]').remove();
node.configuredSchedules = node.configuredSchedules.filter(function(e) {
return id != e.idx;
});
id=$("#node-input-activeSchedule").find(":selected").val();
node.activeSched=node.configuredSchedules.find((sched)=>parseInt(sched.idx)==parseInt(id));
if (node.activeSched===undefined){
$("#node-input-scheduleName").val("");
$("#node-input-scheduleName").prop('disabled', true);
configEventArray(null);
return;
}
configEventArray(node.activeSched.events);
$("#node-input-scheduleName").val(node.activeSched.name);
});
$('#node-input-activeSchedule').on('change', function() {
node.activScheduleId=this.value;
node.activeSched=node.configuredSchedules.find((sched)=>parseInt(sched.idx)==parseInt(node.activScheduleId));
$("#node-input-scheduleName").prop('disabled', false);
configEventArray(node.activeSched.events);
$("#node-input-scheduleName").val(node.activeSched.name);
});
$("#node-input-scheduleName").keyup(function (event) {
node.activeSched.name=$('#node-input-scheduleName').val();
console.log(node.configuredSchedules);
$('#node-input-activeSchedule').find(":selected").text(node.activeSched.name);
});
}
$('#node-input-rule-container').css('min-height','150px').css('min-width','400px').editableList({
removable: true,
sortable: true,
addItem: function(container,i,opt) {
let rule = opt;
console.log("rule:"+JSON.stringify(rule));
console.log("object.keys(opt).length:"+Object.keys(opt).length);
let maxId=0;
node.rules.forEach(function(r){
if (parseInt(r.ruleIdx)>maxId)
maxId=parseInt(r.ruleIdx);
});
node.maxRuleId=maxId+1;
console.log("node.maxRuleId after calc:"+node.maxRuleId);
if ( typeof opt == 'undefined' || Object.keys(opt).length==0){
rule.ruleIdx=node.maxId;
rule = {setName:"Heating "+node.maxRuleId,setColor:"#CCCCCC",spTemp:"19",ruleIdx:node.maxRuleId};
node.rules.push(rule);
}else{
if (rule.ruleIdx===undefined){
rule.ruleIdx=node.maxRuleId;
console.log("ruleIdx undefined, set to maxRuleId:"+node.maxRuleId);
}
}
$(container).data('data',i);
let fragment = document.createDocumentFragment();
var row1 = $('<div/>',{style:"display:flex; align-items: baseline"}).appendTo(fragment);
$('<label style="width:20px !important;display: inline-block; text-align: right;" ><span><i class="fa fa-sign-out"></i></span></label>').appendTo(row1);
$('<label style="width:20px !important;" ><span><b>'+(i+1)+'</b></span></label>').appendTo(row1);
$('<input type="text" id="node-input-set-name_'+rule.ruleIdx+'" idx="'+rule.ruleIdx+'" class="node-input-set-name" placeholder="Set name" style="width:140px" >').appendTo(row1);
$('<label style="width:20px !important; display: inline-block; text-align: right; margin-right:5px;" ><span><i class="fa fa-thermometer-empty"></i></span></label>').appendTo(row1);
$('<input type="text" id="node-input-temp-sp_'+rule.ruleIdx+'" class="node-input-sp" placeholder="SetPoint" style="width:60px !important; text-align: right;">').appendTo(row1);
$('<label style="width:20px !important;display: inline-block; text-align: right; margin-right:15px;" >°C</label>').appendTo(row1);
$('<input type="text" id="node-input-temp-color_'+rule.ruleIdx+'" class="node-input-temp-color" size="7" placeholder="color" style="width:70px !important; "> ').appendTo(row1);
$('<label style="width:20px !important; display: inline-block; text-align: right; margin-right:5px;margin-left:5px;"><span><i class="fa fa-paint-brush"></i></span></label>').appendTo(row1);
$('<button type="button" id="cc_btn_'+rule.ruleIdx+'" idx="'+rule.ruleIdx+'" class="cc_btn red-ui-button red-ui-button" style="margin-left: 5px !important; height: 32px !important;"> </button>').appendTo(row1);
//$('<button type="button" id="sched_btn_'+rule.ruleIdx+'" idx="'+rule.ruleIdx+'" class="red-ui-button toggle my-button-group"></button>').appendTo(sched_btn_grp);
$('<button type="button" id="sched_btn_'+rule.ruleIdx+'" idx="'+rule.ruleIdx+'" class="red-ui-button toggle my-button-group"></button>').insertBefore($('#sched_btn_del'));
container[0].appendChild(fragment);
$('#node-input-set-name_'+rule.ruleIdx).val(rule.setName);
$('#node-input-temp-color_'+rule.ruleIdx).val(rule.setColor);
$("#cc_btn_"+rule.ruleIdx).css('background-color',rule.setColor);
$('#node-input-temp-sp_'+rule.ruleIdx).val(rule.spTemp);
$("#sched_btn_"+rule.ruleIdx).html(rule.setName);
const favDialog = document.getElementById("ColorPickerDialog");
$("#cc_btn_"+rule.ruleIdx).on('click',function(event) {
favDialog.showModal();
ctrl_i=$(event.target).attr('idx')
console.log("cc_btn_ click ctrl_i:"+ctrl_i);
});
$('#node-input-set-name_'+rule.ruleIdx).keyup(function (event) {
ctrl_i=$(event.target).attr('idx');
console.log("set-name_ keyup ctrl_i:"+ctrl_i);
$("#sched_btn_"+ctrl_i).html($('#node-input-set-name_'+ctrl_i).val());
let r=node.rules.find(({ruleIdx}) => ruleIdx==ctrl_i);
r.setName=$('#node-input-set-name_'+ctrl_i).val();
});
$('#node-input-temp-sp_'+rule.ruleIdx).keyup(function (event) {
ctrl_i=$(event.target).attr('idx');
node.rules.find(({ruleIdx}) => ruleIdx==ctrl_i).spTemp=$('#node-input-temp-sp_'+ctrl_i).val();
});
$(".my-button-group").off("click");
$(".my-button-group").on("click", function(event) {
$(".my-button-group").removeClass("selected");
$(this).addClass("selected");
console.log("here1");
ctrl_i=$(event.target).attr('idx');
});
$(".my-button-group").removeClass("selected");
$("#sched_btn_"+rule.ruleIdx).addClass("selected");
ctrl_i=rule.ruleIdx;
},
removeItem:function(data) {
// data is the index of the item to remove in the list not the ruleIdx
// When removing we need to remove all events linked to this ruleIdx
// for event in the calendar but also for all undispalyed schedules
idx=node.rules[data].ruleIdx;
node.rules.splice(data,1);
$("#sched_btn_"+idx).remove();
// Remove matching event from calendar
node.calendar.getEvents().forEach(function(event) {
if (event.extendedProps.ruleIdx==idx){
event.remove();
node.activeSched.events = node.activeSched.events.filter(function(e) {
return event.id != e.id;
});
}
});
// Remove matching event from other schedules
node.configuredSchedules.forEach(function(sched){
sched.events = sched.events.filter(function(e) {
return idx != e.ruleIdx;
});
});
},
resizeItem: function(row,index) {
var originalData = $(row).data('data');
console.log("Resize the row for item:", originalData)
},
resize: function() {
console.log("the size has changed");
}
});
if (!node.rules) {
console.log("No rules defined, add a default one");
var rule = {
setName:"Confort",
setColor:"#CCCCCC",
spTemp:"19",
ruleIdx:0
};
node.rules = [rule];
//node.maxRuleId=0;
}
$('<button type="button" id="sched_btn_del" idx="-1" class="red-ui-button toggle my-button-group"><i idx="-1" class="fa fa-trash-o fa-2"></i></button>').appendTo(sched_btn_grp);
this.rules.forEach((rule,index,ar) => {
$("#node-input-rule-container").editableList('addItem',rule);
});
$(".my-button-group").on("click", function(event) {
$(".my-button-group").removeClass("selected");
$(this).addClass("selected");
console.log("here1");
ctrl_i=$(event.target).attr('idx');
//console.log("my-button-group ctrl_i:"+ctrl_i);
});
$("#node-input-CloneDay").on("click", function(event) {
let src=parseInt($("#node-input-cc-src").find(":selected").val());
let dest=parseInt($("#node-input-cc-dest").find(":selected").val());
if (src==dest)
return;
cloneDailySchedule(src,dest);
});
$("#node-input-EmptySched").on("click", function(event) {
EmptySchedule();
});
$.getScript('resources/node-red-contrib-vib-smart-scheduler/moment.min.js')
.done(function(data, textStatus, jqxhr) {
// $.getScript('resources/node-red-contrib-vib-smart-scheduler/fullcalendar.min.js')
$.getScript('https://cdn.jsdelivr.net/npm/@jaames/iro@5')
.done(function(data, textStatus, jqxhr) {
$.getScript('https://cdn.jsdelivr.net/npm/fullcalendar@6.1.9/index.global.min.js')
.done(function(data, textStatus, jqxhr){
setup(node);
})
})
.fail(function(jqxhr, settings, exception ){
console.log("failed to load fullcalendar.min.js");
console.log(exception);
console.log(exception.stack);
});
/*End Editprepare */
});
},
oneditsave: function() {
var rules = $("#node-input-rule-container").editableList('items');
var node = this;
node.rules= [];
rules.each(function(i){
var rule = $(this);
var type = rule.find(".node-input-rule-type").val();
var r = {
setName:rule.find(".node-input-set-name").val(),
setColor:rule.find(".node-input-temp-color").val(),
spTemp:rule.find(".node-input-sp").val(),
ruleIdx:rule.find(".node-input-set-name").attr("idx")
}
node.rules.push(r);
});
node.settingChanged="1"; // Refresh runtime value;
node.activScheduleId=$("#node-input-activeSchedule").find(":selected").val();
//node.triggerMode=$("#node-input-triggerMode").find(":selected").val();
//node.override=$("#node-input-override").find(":selected").val();
// Transform and store data
//node.configuredEvents=[];
// clean up the table
node.configuredSchedules.forEach(function(e) {
let ev=e.events.map(function(ee) {
return {
id:ee.id,
start: ee.start,
end: ee.end,
ruleIdx: parseInt(ee.ruleIdx),
s_dow:ee.s_dow,
s_mod:ee.s_mod,
e_dow:ee.e_dow,
e_mod:ee.e_mod
};
});
e.events=ev;
});
node.schedules= JSON.stringify(node.configuredSchedules);
delete window.calendar;
},
oneditresize: function() {
}
});
</script>
<script type="text/html" data-template-name="smart-scheduler">
<style>
ol#node-input-rule-container .red-ui-typedInput-container {flex:1;}
</style>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" title="node & home asssitant device name" id="node-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-input-uniqueId"><i class="fa fa-tasks"></i> UniqueId</label>
<input type="text" title="Unique id used for home assistant, has to be unique" id="node-input-uniqueId" placeholder="uniqueId"></input>
</div>
<div class="form-row">
<label for="node-input-defaultSp"><i class="fa fa-tasks"></i> Set point</label>
<input type="text" title="set point temperature when no schedule event found" id="node-input-defaultSp" style="width:60px !important; text-align: right;" placeholder="temp"></input>
<label for="node-input-defaultSp"> °C (default)</label>
</div>
<div class="form-row">
<label for="node-input-mqttSettings"><i class="fa fa-wrench"></i> MQTT</label>
<input type="text" id="node-input-mqttSettings"></input>
</div>
<div class="form-row" style="margin-bottom:0;">
<label><i class="fa fa-list"></i> <span data-i18n="change.label.rules"><b>Set points</b></span></label>
<dialog id="ColorPickerDialog" style="width: 300px; height:320px" closed>
<div id="colorPicker"></div>
<form method="dialog">
<div align="center" style="margin-top:5px;"><button>OK</button></div>
</form>
</dialog>
</div>
<div class="form-row node-input-rule-container-row">
<ol id="node-input-rule-container"></ol>
</div>
<div class="form-row">
<label for="node-input-activeSchedule"><i class="fa fa-calendar"></i><b>Schedules</b></label>
</div>
<div class="form-row">
<label for="node-input-activeSchedule">Active</label>
<select id="node-input-activeSchedule"></select>
<a id="node-input-addSchedule" title="Add new schedule" class="red-ui-button"><i class="fa fa-calendar-plus-o fa-2"></i></a>
</div>
<div class="form-row">
<label><span data-i18n=""> Name</span></label>
<input type="text" title="Active schedule name" style="width:250px;" id="node-input-scheduleName" placeholder="name"></input>
<a id="node-input-deleteSchedule" title="Delete current schedule" class="red-ui-button"><i class="fa fa-trash-o fa-2"></i></a>
</div>
<div class="form-row">
<label><span data-i18n=""> Copy</span></label>
<select id="node-input-cc-src" style="width:75px;">
<option value="1">Mon</option>
<option value="2">Tue</option>
<option value="3">Wed</option>
<option value="4">Thu</option>
<option value="5">Fri</option>
<option value="6">Sat</option>
<option value="7">Sun</option>
</select>
<i class="fa fa-arrow-circle-right"></i><span data-i18n=""></span>
<select id="node-input-cc-dest" style="width:75px;">
<option value="1">Mon</option>
<option value="2">Tue</option>
<option value="3">Wed</option>
<option value="4">Thu</option>
<option value="5">Fri</option>
<option value="6">Sat</option>
<option value="7">Sun</option>
</select>
<a id="node-input-CloneDay" title="Clone day" class="red-ui-button"><i class="fa fa-clone fa-2"></i></a>
</div>
<div class="form-row">
<label><span data-i18n=""> Clean</span></label>
<a id="node-input-EmptySched" title="Clone day" class="red-ui-button"><i class="fa fa-eraser fa-2"></i></a>
</div>
<div class="form-row">
<span id="sched_btn_grp" class="button-group"></span>
<link rel='stylesheet' href='resources/node-red-contrib-vib-smart-scheduler/fullcalendar.min.css' />
<style type="text/css">
.wc-business-hours {
font-size: 1.0em;
}
</style>
<div id="calendar" style="width: 550px; border: 1px solid grey"></div>
<div class="form-row">
<p style="padding-top: 10px"></p>
</div>
</div>
<div class="form-row">
<label for="node-input-triggerMode"><i class="fa fa-hand-o-right"></i> <b> Trigger</b></label>
</div>
<div class="form-row">
<label for="node-input-triggerMode"><i class="fa fa-hand-o-right"></i> Mode</label>
<select id="node-input-triggerMode">
<option value="triggerMode.statechange">when state changes</option>
<option value="triggerMode.statechange.startup">when state changes + startup</option>
<option value="triggerMode.minutely">every minute</option>
</select>
</div>
<div class="form-row">
<label style="width:120px !important;" for="node-input-cycleDuration">Cycle</label>
<input type="text" title="override duration period" id="node-input-cycleDuration" style="width:60px !important; text-align: right;" placeholder="min"></input>
<label for="node-input-overrideDuration-2"> min</label>
</div>
<div class="form-row">
<p style="padding-top: 10px"><i class="fa fa-chain-broken"></i><b> Execution</b></p>
</div>
<div class="form-row">
<label style="width:120px !important;" for="node-input-allowOverride">Override</label>
<input type="checkbox" title="allow override mode" id="node-input-allowOverride" ></input>
</div>
<div class="form-row">
<label for="node-input-executionMode"> Mode </label>
<select id="node-input-executionMode">
<option value="auto">auto</option>
<option value="manual">manual</option>
<option value="off">off</option>
</select>
</div>
<div class="form-row">
<label for="node-input-overrideDuration"> Duration</label>
<input type="text" title="override duration period" id="node-input-overrideDuration" style="width:60px !important; text-align: right;" placeholder="min"></input>
<label for="node-input-overrideDuration-2"> min</label>
</div>
<div class="form-row">
<label for="node-input-overrideSp"> Set point</label>
<input type="text" title="override temperature set point" id="node-input-overrideSp" style="width:60px !important; text-align: right;" placeholder="temperature"></input>
<label for="node-input-overrideDuration-2"> °C</label>
</div>
<div class="form-row">
<label for="node-input-debugInfo"><i class="fa fa-bug"></i> Debug Level</label>
<select id="node-input-debugInfo">
<option value="none">None</option>
<option value="error">Error</option>
<option value="warn">Warning</option>
<option value="info">Info</option>
<option value="debug">Debug</option>
</select>
</div>
</script>
<script type="text/html" data-help-name="smart-scheduler">
<h3>SMART-SCHEDULER NODE</h3>
<p>This node manages heating schedules with visual weekly calendar, multiple schedules, and Home Assistant integration via MQTT.</p>
<h3>INPUT MESSAGE FORMAT:</h3>
<pre><code>msg.payload = {
command: [string], // Required: "trigger", "set", "on", "off", "override"
setpoint: [number], // Target temperature (°C) - required for "set" and "override"
groupId: [number] // Group identifier - required for all commands
}
Examples:
// Trigger schedule evaluation
msg.payload = { command: "trigger", groupId: 1 }
// Set new setpoint
msg.payload = { command: "set", setpoint: 22, groupId: 1 }
// Manual override with setpoint
msg.payload = { command: "override", setpoint: 24, groupId: 1 }</code></pre>
<h3>BUSINESS RULES:</h3>
<h4>Schedule Activation Condition:</h4>
<p>A schedule is <strong>ACTIVE</strong> when current time falls within a defined heating block in the weekly calendar.</p>
<h4>Target Conditions on Input:</h4>
<ul>
<li><strong>Command "trigger":</strong> Evaluates current schedule, sends setpoint if <code>executionMode != "off"</code></li>
<li><strong>Command "set":</strong> Updates active schedule setpoint, triggers evaluation</li>
<li><strong>Command "on":</strong> Sets <code>executionMode = "auto"</code>, enables schedule-based control</li>
<li><strong>Command "off":</strong> Sets <code>executionMode = "off"</code>, disables all output</li>
<li><strong>Command "override":</strong>
<ul>
<li>Sets <code>executionMode = "override"</code></li>
<li>Stores override setpoint and duration</li>
<li>Bypasses schedule until override expires or cancelled</li>
<li>Override timestamp saved for duration tracking</li>
</ul>
</li>
<li><strong>GroupId matching:</strong> Input messages only processed if <code>msg.payload.groupId == node.groupId</code></li>
</ul>
<h4>KEY FEATURES:</h4>
<ul>
<li>Visual weekly calendar with color-coded heating zones</li>
<li>Multiple schedule management</li>
<li>Manual override mode with configurable duration</li>
<li>On/Off/Auto/Override execution modes</li>
<li>Home Assistant integration via MQTT</li>
<li><strong>State Persistence:</strong> Automatically saves execution mode, active schedule, override settings, and setpoints to <code>~/.node-red/.node-red-state/scheduler-{node-id}.json</code>. State is restored on Node-RED restarts and flow redeployments, preventing heating schedule disruptions.</li>
</ul>
<p>For detailed documentation, please refer to the README.md file.</p>
</script>