UNPKG

node-red-contrib-vib-smart-scheduler

Version:
991 lines (823 loc) 44.7 kB
<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; ">&nbsp;').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;">&nbsp;&nbsp;&nbsp;</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>