UNPKG

node-red-contrib-vib-smart-scheduler

Version:
921 lines (728 loc) 38.5 kB
/* __ _____ ___ ___ Author: Vincent BESSON \ \ / /_ _| _ ) _ \ Release: 0.80 \ V / | || _ \ / Date: 20251201 \_/ |___|___/_|_\ Description: Nodered Heating Scheduler 2025 Licence: Creative Commons ______________________ */ /* TEST Env npm install ./modulename (SmartValve / SmartBoiler / SmartSchedule) node-red -v -D logging.console.level=trace // Web http://localhost:1880/ Deploy to npmjs npm publish */ /* TODO: ----- + Add local js lib for offline (calendat.js) */ var moment = require('moment'); // require const mqtt = require("mqtt"); const fs = require('fs'); const path = require('path'); const pjson = require('./package.json'); module.exports = function(RED) { 'use strict' const scheduler = require('./lib/scheduler.js') function SmartScheduler(n) { RED.nodes.createNode(this, n) this.name=n.name ? n.name : "smartscheduler"; this.triggerMode = n.triggerMode ? n.triggerMode : 'trigger.statechange.startup' try { this.schedules = n.schedules ? JSON.parse(n.schedules) : []; } catch (e) { this.schedules = []; node.error("Error parsing schedules JSON: " + e.message); } this.rules=n.rules; this.activScheduleId=n.activScheduleId; // ID of the active schedule this.defaultSp=n.defaultSp ? n.defaultSp : '5' // When no event, out put the default sp this.allowOverride=n.allowOverride ? n.allowOverride :false; this.executionMode = n.executionMode ? n.executionMode : 'auto' // Current execution mode this.overrideTs= n.overrideTs ? n.overrideTs : '0' // Timestamp of override mode start this.overrideDuration=n.overrideDuration ? n.overrideDuration :"120" // Duration of the override periode (set in setting) this.overrideSp=n.overrideSp ? n.overrideSp : "5" // Override set point by default this.cycleDuration=n.cycleDuration ?n.cycleDuration: 1 this.activeRuleIdx=0 // Active Rule ID for the current event this.prevRuleIdx=0 // Previous active Rule ID this.activeSp=0; // Active SP this.prevSp=0; // Previous SP this.firstEval = true // First iteration loop evaluation this.manualTrigger = false; // Manual trigger flag from the input this.endOfOverride=false; // Trigger after override period end this.debugInfo=n.debugInfo? n.debugInfo :"none"; // Flag to send message to the console this.noout=false; this.mqttclient=null; this.mqttstack=[]; const node = this; this.mqttSettings = RED.nodes.getNode(n.mqttSettings); if ( this.mqttSettings?.mqttHost) node.log(this.mqttSettings?.mqttHost); // START OF MQTT if (this.mqttPrefix && this.mqttSettings.mqttRootPath) this.mqttPrefix=this.mqttSettings.mqttRootPath else this.mqttPrefix="homeassistant"; this.uniqueId=n.uniqueId ? n.uniqueId : "SmartScheduler_1"; this.adv_mode_topic=this.mqttPrefix+"/select/"+node.uniqueId+"/mode/config"; this.state_mode_topic=this.mqttPrefix+"/"+node.uniqueId+"/mode/state"; this.set_mode_topic=this.mqttPrefix+"/"+node.uniqueId+"/mode/set"; this.adv_current_sp_topic=this.mqttPrefix+"/sensor/"+node.uniqueId+"/current_sp/config"; this.state_current_sp_topic=this.mqttPrefix+"/"+node.uniqueId+"/current_sp/state"; this.adv_override_duration_left_topic=this.mqttPrefix+"/sensor/"+node.uniqueId+"/duration_left/config"; this.state_override_duration_left_topic=this.mqttPrefix+"/"+node.uniqueId+"/duration_left/state"; this.adv_previous_sp_topic=this.mqttPrefix+"/sensor/"+node.uniqueId+"/previous_sp/config"; this.state_previous_sp_topic=this.mqttPrefix+"/"+node.uniqueId+"/previous_sp/state"; this.adv_current_event_name_topic=this.mqttPrefix+"/sensor/"+node.uniqueId+"/current_event_name/config"; this.state_current_event_name_topic=this.mqttPrefix+"/"+node.uniqueId+"/current_event_name/state"; this.adv_current_event_start_topic=this.mqttPrefix+"/sensor/"+node.uniqueId+"/current_event_start/config"; this.state_current_event_start_topic=this.mqttPrefix+"/"+node.uniqueId+"/current_event_start/state"; this.adv_current_event_end_topic=this.mqttPrefix+"/sensor/"+node.uniqueId+"/current_event_end/config"; this.state_current_event_end_topic=this.mqttPrefix+"/"+node.uniqueId+"/current_event_end/state"; this.adv_schedule_list_topic=this.mqttPrefix+"/select/"+node.uniqueId+"/schedule_list/config"; this.state_schedule_list_topic=this.mqttPrefix+"/"+node.uniqueId+"/schedule_list/state"; this.set_schedule_list_topic=this.mqttPrefix+"/"+node.uniqueId+"/schedule_list/set"; node.warn("Smart-Scheduler node started, name:"+node.name+", uniqueId:"+node.uniqueId); this.ev=function(){ node.manualTrigger=true; evaluate(); } this.dev={ ids:[node.uniqueId], name:node.name, mdl:"Smart-Scheduler", mf:"VIBR", sw:pjson.version, hw_version:"1.0" } function nlog(msg, level="debug"){ const levels = { "none": 0, "error": 1, "warn": 2, "info": 3, "debug": 4 }; const currentLevel = levels[node.debugInfo] || 0; const msgLevel = levels[level] || 4; if (msgLevel <= currentLevel){ if (level === "error") { node.error(msg); } else if (level === "warn") { node.warn(msg); } else { node.log(msg); // Also send to warn if it was previously doing so for debug visibility in sidebar // The original code did node.log AND node.warn for debugInfo=true if (level === "debug" || level === "info") { node.warn(msg); } } } } function saveState() { const state = { executionMode: node.executionMode, overrideTs: node.overrideTs, overrideSp: node.overrideSp, overrideDuration: node.overrideDuration, activScheduleId: node.activScheduleId, activeSp: node.activeSp, prevSp: node.prevSp, activeRuleIdx: node.activeRuleIdx, prevRuleIdx: node.prevRuleIdx, timestamp: Date.now() }; // Save to both context and file system node.context().set("schedulerState", state); // Save to file for persistence across deploys const stateDir = path.join(RED.settings.userDir, '.node-red-state'); const stateFile = path.join(stateDir, `scheduler-${node.id}.json`); try { if (!fs.existsSync(stateDir)) { fs.mkdirSync(stateDir, { recursive: true }); } fs.writeFileSync(stateFile, JSON.stringify(state, null, 2)); nlog("State saved to file: mode=" + node.executionMode + ", sp=" + node.activeSp, "debug"); } catch (err) { nlog("Error saving state to file: " + err.message, "error"); } } // Load saved state at startup let savedState = null; // Try to load from file first (persists across deploys) const stateDir = path.join(RED.settings.userDir, '.node-red-state'); const stateFile = path.join(stateDir, `scheduler-${node.id}.json`); try { if (fs.existsSync(stateFile)) { const fileData = fs.readFileSync(stateFile, 'utf8'); savedState = JSON.parse(fileData); nlog("Loaded state from file", "info"); } } catch (err) { nlog("Error loading state from file: " + err.message, "warn"); } // Fallback to context storage if file doesn't exist if (!savedState) { savedState = node.context().get("schedulerState"); if (savedState) { nlog("Loaded state from context", "info"); } } if (savedState) { node.executionMode = savedState.executionMode || node.executionMode; node.overrideTs = savedState.overrideTs || node.overrideTs; node.overrideSp = savedState.overrideSp || node.overrideSp; node.overrideDuration = savedState.overrideDuration || node.overrideDuration; node.activScheduleId = savedState.activScheduleId || node.activScheduleId; node.activeSp = savedState.activeSp || 0; node.prevSp = savedState.prevSp || 0; node.activeRuleIdx = savedState.activeRuleIdx || 0; node.prevRuleIdx = savedState.prevRuleIdx || 0; nlog("Loaded saved state: mode=" + node.executionMode + ", sp=" + node.activeSp, "info"); } function isEqual(a, b) { // simpler and more what we want compared to RED.utils.compareObjects() return JSON.stringify(a) === JSON.stringify(b) } function setState(matchingEvent=-1) { var msg = {}; let hasSpchanged=false; if (node.activScheduleId===undefined){ node.status({ fill: 'yellow', shape: 'dot', text:("Not ready, schedule not defined") }); return; } if (node.executionMode=="off"){ nlog("scheduler executionMode:off returning","debug"); node.status({ fill: 'black', shape: 'dot', text:("Off, no event") }); return; }else if (node.executionMode=="manual"){ // Manual Execution nlog("node.executionMode==manual","debug"); let ovrM=moment(node.overrideTs).add(node.overrideDuration,"m") let now = moment(); let diff=ovrM.diff(now,"m")+1; node.activeSp=node.overrideSp; if (parseFloat(node.activeSp)!=parseFloat(node.prevSp)) hasSpchanged=true; if (node.mqttclient!=null && node.mqttstack.length<100){ msg.payload={ command:"set", setpoint:parseFloat(parseFloat(node.overrideSp).toFixed(2)), previous_setpoint:parseFloat(parseFloat(node.prevSp).toFixed(2)), executionmode:node.executionMode, setname:"override", overrideduration:parseInt(node.overrideDuration), activeruleidx:-1, prevruleidx: parseInt(node.prevRuleIdx), triggermode:node.triggerMode, manualtrigger:node.manualTrigger, short_start:0, short_end:0, start:0, end:0, duration:diff, hasspchanged:hasSpchanged } let mqttmsg={topic:node.state_mode_topic,payload:{value:"manual"},qos:0,retain:false}; node.mqttstack.push(mqttmsg); mqttmsg={topic:node.state_current_sp_topic,payload:{value:parseFloat(node.overrideSp).toFixed(2)},qos:0,retain:false}; node.mqttstack.push(mqttmsg); mqttmsg={topic:node.state_previous_sp_topic,payload:{value:parseFloat(node.prevSp).toFixed(2)},qos:0,retain:false}; node.mqttstack.push(mqttmsg); mqttmsg={topic:node.state_current_event_name_topic,payload:{value:"None"},qos:0,retain:false}; node.mqttstack.push(mqttmsg); mqttmsg={topic:node.state_current_event_start_topic,payload:{value:"-"},qos:0,retain:false}; node.mqttstack.push(mqttmsg); mqttmsg={topic:node.state_current_event_end_topic,payload:{value:"-"},qos:0,retain:false}; node.mqttstack.push(mqttmsg); mqttmsg={topic:node.state_override_duration_left_topic,payload:{value:diff},qos:0,retain:false}; node.mqttstack.push(mqttmsg); sendMqtt(); } node.status({ fill: 'yellow', shape: 'dot', text:("Manual sp:"+node.overrideSp+" °C, "+diff+" min left") }); }else if (matchingEvent.ruleIdx==-1){ // No matching event found, output default sp nlog("matchingEvent.ruleIdx==-1 no event matched, output default sp","debug"); if (node.mqttclient!=null && node.mqttstack.length<100 ){ let mqttmsg={topic:node.state_current_sp_topic,payload:{value:parseFloat(node.defaultSp).toFixed(2)},qos:0,retain:false}; node.mqttstack.push(mqttmsg); mqttmsg={topic:node.state_previous_sp_topic,payload:{value:parseFloat(node.prevSp).toFixed(2)},qos:0,retain:false}; node.mqttstack.push(mqttmsg); mqttmsg={topic:node.state_current_event_name_topic,payload:{value:"None"},qos:0,retain:false}; node.mqttstack.push(mqttmsg); mqttmsg={topic:node.state_current_event_start_topic,payload:{value:"-"},qos:0,retain:false}; node.mqttstack.push(mqttmsg); mqttmsg={topic:node.state_current_event_end_topic,payload:{value:"-"},qos:0,retain:false}; node.mqttstack.push(mqttmsg); mqttmsg={topic:node.state_override_duration_left_topic,payload:{value:0},qos:0,retain:false}; node.mqttstack.push(mqttmsg); sendMqtt(); } node.activeSp=node.defaultSp; if (parseFloat(node.activeSp)!=parseFloat(node.prevSp)) hasSpchanged=true; msg.payload={ command:"set", setpoint:parseFloat(parseFloat(node.defaultSp).toFixed(2)), previous_setpoint:parseFloat(parseFloat(node.prevSp).toFixed(2)), setname:"default rule", short_start:0, short_end:0, start:0, end:0, manualtrigger:node.manualTrigger, triggermode:node.triggerMode, active_ruleidx:parseInt(matchingEvent.ruleIdx), previous_ruleidx: parseInt(node.prevRuleIdx), executionmode:node.executionMode, hasspchanged:hasSpchanged } node.status({ fill: 'gray', shape: 'ring', text:("Default sp: "+node.defaultSp+" °C") }); }else if (matchingEvent.ruleIdx>=0){ // Matching event found node.prevRuleIdx=node.activeRuleIdx; node.activeRuleIdx=parseInt(matchingEvent.ruleIdx); nlog("matchingEvent.ruleIdx:"+matchingEvent.ruleIdx + " eventId:"+matchingEvent.eventId,"debug"); var event=node.events.find((item) => parseInt(item.id)==parseInt(matchingEvent.eventId)); var m_s=moment(event.start); var m_e=moment(event.end); var d_s=m_s.format('HH:mm'); var d_e=m_e.format('HH:mm'); var dayStr=["","Mon","Tue","Wed","Thu","Fri","Sat","Sun"]; var period=dayStr[m_s.days()]+" "+d_s+" - "+dayStr[m_e.days()]+" "+d_e; if (node.rules===undefined){ nlog("error node.rules is undefined","error"); return; } let r=node.rules.find(({ruleIdx}) => parseInt(ruleIdx)==parseInt(matchingEvent.ruleIdx)); if (r===undefined){ nlog("rule should not be undefined","error"); return; } if (node.mqttclient!=null && node.mqttstack.length<100){ let mqttmsg={topic:node.state_mode_topic,payload:{value:"auto"},qos:0,retain:false}; node.mqttstack.push(mqttmsg); mqttmsg={topic:node.state_current_sp_topic,payload:{value:parseFloat(r.spTemp).toFixed(2)},qos:0,retain:false}; node.mqttstack.push(mqttmsg); mqttmsg={topic:node.state_previous_sp_topic,payload:{value:parseFloat(node.prevSp).toFixed(2)},qos:0,retain:false}; node.mqttstack.push(mqttmsg); mqttmsg={topic:node.state_current_event_name_topic,payload:{value:r.setName},qos:0,retain:false}; node.mqttstack.push(mqttmsg); mqttmsg={topic:node.state_current_event_start_topic,payload:{value:d_s},qos:0,retain:false}; node.mqttstack.push(mqttmsg); mqttmsg={topic:node.state_current_event_end_topic,payload:{value:d_e},qos:0,retain:false}; node.mqttstack.push(mqttmsg); mqttmsg={topic:node.state_override_duration_left_topic,payload:{value:0},qos:0,retain:false}; node.mqttstack.push(mqttmsg); sendMqtt(); } node.status({ fill: 'green', shape: 'dot', text:(r.setName+" "+r.spTemp+" °C "+period) }); node.activeSp=r.spTemp; if (parseFloat(node.activeSp)!=parseFloat(node.prevSp)) hasSpchanged=true; msg.payload={ command:"set", setpoint:parseFloat(parseFloat(r.spTemp).toFixed(2)), previous_setpoint:parseFloat(parseFloat(node.prevSp).toFixed(2)), setname:r.setName, short_start:d_s, short_end:d_e, start:event.start, end:event.end, manualtrigger:node.manualTrigger, triggermode:node.triggerMode, active_ruleIdx:parseInt(node.activeRuleIdx), previous_ruleIdx: parseInt(node.prevRuleIdx), executionmode:node.executionMode, hasspchanged:hasSpchanged } } // Only send anything if the state have changed, on trigger and when configured to output on a minutely basis. nlog(`-->setState(matchingEvent) output: activeRuleIdx:${node.activeRuleIdx}, prevRuleIdx:${node.prevRuleIdx}, activeSp:${node.activeSp}, prevSp:${node.prevSp}, noout:${node.noout}`, "debug"); if (node.manualTrigger || node.triggerMode == 'triggerMode.minutely' || node.activeRuleIdx !== node.prevRuleIdx || node.activeSp !== node.prevSp || node.firstEval==true) { if (/*!node.firstEval &&*/ !node.noout){ node.send(msg); node.noout=false; }else if (node.noout==true) node.noout=false; node.prevPayload = msg.payload node.prevSp= node.activeSp; node.prevRuleIdx=node.activeRuleIdx saveState(); // Save state after successful update } node.firstEval = false node.manualTrigger = false } function mqttAdvertise(){ let msg={}; msg.payload={ name:"Mode", uniq_id:node.uniqueId+"MODE", icon:"mdi:cog-clockwise", qos:0, retain:true, command_topic:node.set_mode_topic, options:["off","manual","auto"], state_topic:node.state_mode_topic, value_template:"{{value_json.value}}", dev:node.dev } let mqttmsg={topic:node.adv_mode_topic,payload:msg.payload,qos:msg.payload.qos,retain:msg.payload.retain}; node.mqttstack.push(mqttmsg); let arr=[]; if (node.schedules){ node.schedules.forEach(function(e){ arr.push(e.name); }); } msg.payload={ name:"Schedule", uniq_id:node.uniqueId+"SCHED", icon:"mdi:calendar-text-outline", qos:0, retain:true, state_topic:node.state_schedule_list_topic, command_topic:node.set_schedule_list_topic, options:arr, value_template:"{{value_json.value}}", dev:node.dev } mqttmsg={topic:node.adv_schedule_list_topic,payload:msg.payload,qos:0,retain:false}; node.mqttstack.push(mqttmsg); msg.payload={ name:"Current Setpoint", uniq_id:node.uniqueId+"SP", icon:"mdi:thermometer", qos:0, retain:true, unit_of_measurement:"°C", state_topic:node.state_current_sp_topic, value_template:"{{value_json.value}}", dev:node.dev } mqttmsg={topic:node.adv_current_sp_topic,payload:msg.payload,qos:msg.payload.qos,retain:msg.payload.retain}; node.mqttstack.push(mqttmsg); msg.payload={ name:"Previous Setpoint", uniq_id:node.uniqueId+"prevSP", icon:"mdi:thermometer", qos:0, retain:true, unit_of_measurement:"°C", state_topic:node.state_previous_sp_topic, value_template:"{{value_json.value}}", dev:node.dev } mqttmsg={topic:node.adv_previous_sp_topic,payload:msg.payload,qos:msg.payload.qos,retain:msg.payload.retain}; node.mqttstack.push(mqttmsg); msg.payload={ name:"Duration left", uniq_id:node.uniqueId+"MD", icon:"mdi:clock-time-eight-outline", qos:0, retain:true, state_topic:node.state_override_duration_left_topic, unit_of_measurement:"min", value_template:"{{value_json.value}}", dev:node.dev } mqttmsg={topic:node.adv_override_duration_left_topic,payload:msg.payload,qos:msg.payload.qos,retain:msg.payload.retain}; node.mqttstack.push(mqttmsg); msg.payload={ name:"Current event", uniq_id:node.uniqueId+"EVNAME", icon:"mdi:calendar-text-outline", qos:0, retain:true, state_topic:node.state_current_event_name_topic, value_template:"{{value_json.value}}", dev:node.dev } mqttmsg={topic:node.adv_current_event_name_topic,payload:msg.payload,qos:msg.payload.qos,retain:msg.payload.retain}; node.mqttstack.push(mqttmsg); msg.payload={ name:"Event start", uniq_id:node.uniqueId+"EVSTART", icon:"mdi:clock-time-eight-outline", qos:0, retain:true, state_topic:node.state_current_event_start_topic, value_template:"{{value_json.value}}", dev:node.dev } mqttmsg={topic:node.adv_current_event_start_topic,payload:msg.payload,qos:msg.payload.qos,retain:msg.payload.retain}; node.mqttstack.push(mqttmsg); msg.payload={ name:"Event end", uniq_id:node.uniqueId+"EVEND", icon:"mdi:clock-time-four-outline", qos:0, retain:true, state_topic:node.state_current_event_end_topic, value_template:"{{value_json.value}}", dev:node.dev } mqttmsg={topic:node.adv_current_event_end_topic,payload:msg.payload,qos:msg.payload.qos,retain:msg.payload.retain}; node.mqttstack.push(mqttmsg); sendMqtt(); } function evaluate() { if (node.executionMode == 'off') { //node.status({fill: 'gray', shape: 'dot', text: 'OFF'}) return setState(null); } if (node.executionMode == 'manual') { // First check if below threshold override duration let ovrM=moment(node.overrideTs).add(node.overrideDuration,"m") var now = moment(); if (now > ovrM){ node.executionMode='auto'; nlog(" exceed OverrideDuration, executionMode=manual->auto","debug"); node.noout=false; node.endOfOverride=true; } } let s=node.schedules.find(({idx}) => parseInt(idx)==parseInt(node.activScheduleId)); if (s===undefined){ node.activScheduleId=undefined; nlog("scheduler is undefined returning","error"); return setState(null); } var matchEvent = scheduler.matchSchedule(s) nlog("MatchEvent: ruleIdx:"+matchEvent.ruleIdx+", eventId:"+matchEvent.eventId); node.activeRuleIdx=parseInt(matchEvent.ruleIdx); return setState(matchEvent); } node.on('input', function(msg){ if (msg===undefined || msg.payload===undefined){ nlog("invalid msg in input","error"); } if (msg.payload.command!=undefined && msg.payload.command.match(/^(1|on|0|off|auto|override|trigger|schedule)$/i)) { let command=msg.payload.command; if (command == '1' || command == 'trigger' || command== 'on'){ node.manualTrigger = true; if (command == 'on' && node.executionMode=="off") node.executionMode="auto"; nlog("Command:trigger/on/1 manual trigger evaluation","info"); } else if (command=="auto"){ node.executionMode="auto"; let now = new Date(); node.overrideTs=now.toISOString(); nlog("Command:auto set executionMode to auto","info"); saveState(); // Save state when mode changes } else if (command=="schedule"){ if (msg.payload.name===undefined){ nlog('Command:schedule, missing input parameter:msg.name ','error'); return; } let s=node.schedules.find(({name}) => name==msg.payload.name); if (s===undefined){ nlog('Command:schedule, schedule name:'+msg.payload.name+' not found','error'); return; } node.events=s.events; // <------------------ TODO Change input by name and not by id node.activScheduleId=s.idx; nlog("Command:schedule change active schedule to name:"+s.name,"info"); msg.payload={value:node.schedules.find((sched)=>parseInt(sched.idx)==parseInt(node.activScheduleId)).name}; let mqttmsg={topic:node.state_schedule_list_topic,payload:msg.payload,qos:0,retain:false}; node.mqttstack.push(mqttmsg); sendMqtt(); saveState(); // Save state when schedule changes } else if (command=="off" || command == '0'){ nlog("Command:off set executionMode to off","info"); node.executionMode="off"; let now = new Date(); node.overrideTs=now.toISOString(); saveState(); // Save state when mode changes } else if (command=="override"){ if (msg.payload.setpoint=== undefined || isNaN(msg.payload.setpoint) || parseFloat(msg.payload.setpoint)<0 || parseFloat(msg.payload.setpoint)>40){ //<----------- Todo define Max & Min in config nlog('Command:override missing or invalid msg.setpoint number',"error"); return; } node.executionMode="manual"; let now = new Date(); node.overrideTs=now.toISOString(); node.overrideSp=msg.payload.setpoint; if (msg.payload.noout || msg.payload.noout==true){ node.noout=true; nlog("noout==true"); } nlog("Command:override set executionMode to manual, sp:"+node.overrideSp,"info"); saveState(); // Save state when entering override mode } // Evaluate is done is not returned before by one of the above conditions evaluate(); } else{ if (msg.payload.command!=undefined ) nlog("invalid command in input msg.payload.command:"+msg.payload.command,"error"); else nlog("invalid or missing command in input msg.payload","warn"); return; } }); if (node.activScheduleId){ let foundSchedule = node.schedules.find((sched)=>parseInt(sched.idx)==parseInt(node.activScheduleId)); if (foundSchedule) { node.events = foundSchedule.events; } else { nlog("Active schedule ID "+node.activScheduleId+" not found in schedules list", "warn"); node.activScheduleId = undefined; } } // re-evaluate every minute node.evalInterval = setInterval(evaluate, node.cycleDuration *60000) // Run initially directly after start / deploy. if (node.triggerMode != 'triggerMode.statechange') { setTimeout(evaluate, 1000) } if (node.mqttSettings && node.mqttSettings.mqttHost){ const protocol = 'mqtt' const host = node.mqttSettings.mqttHost const port = node.mqttSettings.mqttPort const clientId=`mqtt_${Math.random().toString(16).slice(3)}`; const connectUrl = `${protocol}://${host}:${port}` node.mqttclient = mqtt.connect(connectUrl, { clientId, clean: true, keepalive:60, connectTimeout: 4000, username: node.mqttSettings.mqttUser, password: node.mqttSettings.mqttPassword, reconnectPeriod: 1000, }); node.mqttclient.on('error', function (error) { nlog("MQTT error:"+error,"error"); }); node.mqttclient.on('connect', () => { mqttAdvertise(); // Initial value at startuo let mqttmsg={topic:node.state_mode_topic,payload:{value:node.executionMode},qos:0,retain:false}; node.mqttstack.push(mqttmsg); nlog("MQTT node.activScheduleId:"+node.activScheduleId,"debug") let s=node.schedules.find(({idx}) => parseInt(idx)==parseInt(node.activScheduleId)); if (s!==undefined){ mqttmsg={topic:node.state_schedule_list_topic,payload:{value:s.name},qos:0,retain:false}; node.log(JSON.stringify(mqttmsg)); node.mqttstack.push(mqttmsg); nlog("MQTT init schedule name:"+s.name,"debug"); }else{ nlog("MQTT init value can not find schedule","error"); } sendMqtt(); node.mqttclient.subscribe([node.set_mode_topic,node.set_schedule_list_topic], () => { nlog(`MQTT Subscribe to topic: ${node.set_mode_topic}, ${node.set_schedule_list_topic}`, "debug"); }) nlog('MQTT Connected start dequeuing',"debug"); let msg=node.mqttstack.shift(); while (msg!==undefined){ if (msg.topic===undefined || msg.payload===undefined) return; node.mqttclient.publish(msg.topic,JSON.stringify(msg.payload),{ qos: msg.qos, retain: msg.retain },(error) => { if (error) { node.error(error) } }); msg=node.mqttstack.shift(); } }); node.mqttclient.on('message', (topic, payload) => { node.log('MQTT Received topic:'+topic,"debug"); let p=payload.toString(); node.log('MQTT Received payload:'+p,"debug"); if (topic==node.set_mode_topic && (p=="auto" || p=="manual" || p=="off")){ nlog("MQTT change executionMode mode:"+p,"info"); var now = new Date(); node.overrideTs=now.toISOString(); node.executionMode=p; let mqttmsg={topic:node.state_mode_topic,payload:{value:node.executionMode},qos:0,retain:false}; node.mqttstack.push(mqttmsg); evaluate(); }else if(topic==node.set_schedule_list_topic){ let s=node.schedules.find(({name}) => name==p); if (s!==undefined){ nlog("MQTT change activeSchedule","info") node.activScheduleId=s.idx; node.events=s.events; let mqttmsg={topic:node.state_schedule_list_topic,payload:{value:p},qos:0,retain:false}; node.mqttstack.push(mqttmsg); sendMqtt(); evaluate(); }else{ nlog("MQTT received schedule name not found","error"); } } }) } function sendMqtt(){ if (node.mqttclient==null || node.mqttclient.connected!=true){ node.warn("MQTT not connected..."); return; } nlog('MQTT dequeueing',"debug"); let msg=node.mqttstack.shift(); while (msg!==undefined){ if (msg.topic===undefined || msg.payload===undefined) return; let msgstr=JSON.stringify(msg.payload); node.mqttclient.publish(msg.topic.toString(),msgstr,{ qos: msg.qos, retain: msg.retain },(error) => { if (error) { node.error(error) } }); msg=node.mqttstack.shift(); } }; node.on('close', function(done) { nlog("closing mqtt connexion","debug"); node.log('MQTT disconnecting',"debug"); clearInterval(node.evalInterval); if (node.mqttclient) { node.mqttclient.end(); } done(); }) } RED.nodes.registerType('smart-scheduler', SmartScheduler) RED.httpAdmin.post("/smartsched/:id", RED.auth.needsPermission("inject.write"), function(req,res) { const node = RED.nodes.getNode(req.params.id); if (node != null) { try { node.ev(); res.sendStatus(200); } catch(err) { res.sendStatus(500); node.error(RED._("inject.failed",{error:err.toString()})); } } else { res.sendStatus(404); } }); }