UNPKG

node-red-contrib-automation-controller

Version:
826 lines (713 loc) 37.6 kB
module.exports = function(RED) { "use strict"; var vm = require("vm"); function getTime(v, t) { if (t === "ms") return v; else if (t === "m") return v * (60 * 1000); else if (t === "h") return v * (60 * 60 * 1000); else if (t === "d") return v * (24 * 60 * 60 * 1000); else return v * 1000; } function getRule(node, rule) { if (Number.isInteger(rule)) return node.rules[rule]; return node.rules.find(r=>rule==r.r.name); } // Creates a new script object function createScript(node, type, code) { var opt = { filename: 'Function automationController.'+type+':'+node.id+(node.name?' ['+node.name+']':''), // filename for stack traces displayErrors: true }; var sc = "(function(msg) {"+code +"})(msg)"; return vm.createScript(sc, opt); } // Initiates the script object. function runScript(node, ri, s, type, fName, msg, inp) { // Setup the script object functionality for the node. if (!node.script) { node.script = { sandbox: { console:console, Buffer:Buffer, Date: Date, __node__: { id: node.id, name: node.name }, context: { set: function() { node.context().set.apply(node,arguments); }, get: function() { return node.context().get.apply(node,arguments); }, keys: function() { return node.context().keys.apply(node,arguments); }, get global() { return node.context().global; }, get flow() { return node.context().flow; } }, flow: { set: function() { node.context().flow.set.apply(node,arguments); }, get: function() { return node.context().flow.get.apply(node,arguments); }, keys: function() { return node.context().flow.keys.apply(node,arguments); } }, global: { set: function() { node.context().global.set.apply(node,arguments); }, get: function() { return node.context().global.get.apply(node,arguments); }, keys: function() { return node.context().global.keys.apply(node,arguments); } }, env: { get: function(envVar) { var flow = node._flow; return flow.getSetting(envVar); } }, rules: { index: function(rule) { var r = getRule(node, rule); return !!r?r.i:"Invalid rule: " + rule; }, name: function(rule) { var r = getRule(node, rule); return !!r?r.r.name:"Invalid rule: " + rule; }, value: function(rule, nv) { var r = getRule(node, rule); if (!r) return "Invalid rule"; if (nv!==undefined) r.v.c = nv; return r.vLast; }, isActive: function(rule) { var r = getRule(node, rule); return !!r?r.a:"Invalid rule: " + rule; }, length: function() { return node.rules.length; } } } }; } if (!ri.script) { ri.script = { ctx: vm.createContext(node.script.sandbox), s:{}, run: function(name, msg, inp) { this.ctx.msg = msg; this.ctx.lastValue = node.lastRule.vLast; this.ctx.lastRuleValue = ri.vLast; this.ctx.lastRuleName = node.lastRule.r.name; if (inp !== undefined) { if (name!='outputJS') this.ctx.input = inp; else this.ctx.output = inp; } return this.s[name].runInContext(this.ctx); } }; } if (!ri.script.s[fName]) { ri.script.s[fName] = createScript(node, type, s[fName]); } try { return ri.script.run(fName, msg, inp); } catch (e) { console.log("Error when executing script: " + fName, e); } } function AutomationControllerNode(config) { RED.nodes.createNode(this,config); var node = this; var T_INACTIVE = 0; var T_ACTIVE = 1; var T_REPEAT = 2; var T_TIMEOUT = 3; var so = config.seperated; // Seperated outputs var lm; // Latest message var act = []; // Active nodes var lazy = false; // Lazy saving var sav = []; // Active savings var r = []; // Rules var ri; // Rule item var ru; // Rule config this.lastRule = undefined; function flagArr(ar,r,a) { if (a) { if (ar.indexOf(r)==-1) ar.push(r); return true; } else { for (var i=ar.length-1;i>=0;i--) { if (ar[i]==r) { ar.splice(i,1); return true; } } } return false; } function flagActive(r) { return flagArr(act, r, r.a); } function flagSave(r,s) { return flagArr(sav, r, s); } function checkDone() { if (act.length==0 && sav.length==0) { node.done(); } } function chkNum(v, id, err, r) { if (!Number.isInteger(v)) { node.error(RED._("error."+id, {error:err + r.name})); return false; } return true; } function updateMsg(rule, msg, c) { if (rule.ist) { switch (rule.r.repMsgType) { case 'pay': if (rule.a && c) rule.msg = msg; break; case 'payl': rule.msg = lm; break; case 'payr': rule.msg = msg; break; } } else { // If event, then set message rule.msg = msg; } return rule.msg; } function updateStatus(r,custom) { node.status({fill:r.a?'green':'red',shape:"dot",text:r.r.name + ': ' + RED._("status."+custom)}); } // Rule, ScriptStorage, Type, ValueType, Value, JSFieldName, message, isInt function evalCmd(r, s, type, vt, v, js, msg, fInt, inp) { switch (vt) { case 'js': r = runScript(node, r, s, type, js, msg, inp); break; default: r = RED.util.evaluateNodeProperty(v, vt, node, msg); break; } if (fInt===true) r = Number.parseInt(r, 10); return r; } // Input, Rule, ScriptStorage, Type, ValueType, Value, JSFieldName, message, isInt function matchCmd(inp, r, s, type, vt, v, js, msg, fInt) { v = evalCmd(r,s,type,vt,v,js,msg,fInt,inp); // If javascript, then accept true/false if (vt=='js' && (v==true || v==false)) return v; return inp==v; } // Setup rules for (var i=0; i<config.rules.length; i++) { ru = config.rules[i]; ri = { i:i, // Index r:ru, // Rule isState:ru.matchMode=='state', // isState a:false, // Active ma:ru.triggerActive, // Multiple activations msg:undefined, // Usage message rt:getTime(ru.rep, ru.repType), // Repeat hr:undefined, // Repeat handler to:getTime(ru.to, ru.toType), // Timeout co:getTime(ru.cool, ru.coolType), // Cooldown rs:getTime(ru.resEvent, ru.resEventType), // reset event ht:undefined, // Timeout handler vLast:undefined, t:function(inp,msg,send,res) { // Test var v,t; var act = matchCmd(inp, this.r, this.r, "active", this.r.activeType, this.r.active, "activeJS", msg, false); if (act) { updateMsg(this, msg, false); } // If not active or multi activations if (act && (!this.a || this.ma)) { return this.e(msg, T_ACTIVE, send, res); } if (this.a && this.isState) { if (matchCmd(inp, this.r, this.r, "inactive", this.r.inactiveType, this.r.inactive, "inactiveJS", msg, false)) { return this.e(msg, T_INACTIVE, send, res); } } }, e:function(msg,event,send,res) { // Execute var state = (event==T_ACTIVE || event==T_REPEAT); // New state // If activating/active, then if (state) { // Check behavior switch (config.behavior) { case 'sng': // Accept single // If has active and not only this, then abort if (act.length > 0 && (act.length!=1 || act[0]!=this)) { return; } break; case 'mul': // Accept multiple break; case 'can': // Cancel others // Loop though active and cancel all if (state) act.forEach(r=>{ if (r!=this) r.c(send); }); break; } } var c = this.a != state; // Changed this.a = state; // Set state // If inactive and not changed, then abort if (!this.a && !c) { return; } // Update message msg = updateMsg(this, msg, c); // If state if (this.isState) { // If state changed, then if (c) { // If active, then add timers if (this.a) { this.hr = setInterval(()=>{ if (this.a) this.e(this.msg, T_REPEAT, send); }, this.rt); this.ht = setTimeout(()=>{ if (this.a) this.e(this.msg, T_TIMEOUT, send); }, this.to); } else { clearInterval(this.hr); clearTimeout(this.ht); this.ht=this.hr=undefined; } } else { // If not changed but active, then restart timers if (event==T_ACTIVE && this.ma) { clearInterval(this.hr); clearTimeout(this.ht); this.hr = setInterval(()=>{ if (this.a) this.e(this.msg, T_REPEAT, send); }, this.rt); this.ht = setTimeout(()=>{ if (this.a) this.e(this.msg, T_TIMEOUT, send); }, this.to); } } // Else if event } else { if (this.a) { // If already active, then abort if (!c) { return; } clearInterval(this.hr); this.hr = undefined; // If has time out, then if (this.co > 0) { this.ht = setTimeout(()=>{ if (this.a) { this.e(this.msg, T_TIMEOUT, send); } }, this.co); } // If has reset event, then if (this.rs > 0) { this.hr = setTimeout(()=>{ this.v.c = undefined; updateStatus(this, "reset"); }, this.rs); } } } flagActive(this); var v; if (this.a) { var rv = event==T_ACTIVE && this.isState && this.r.resetInitial; if (!rv && msg!==undefined && msg.state=='reset') rv = true; // Check if is custom engine value var al = !rv; if (msg!==undefined && !isNaN(Number.parseInt(msg.engineValue))) { this.v.c = Number.parseInt(msg.engineValue); al = false; } v = this.v.e(rv, al); this.vLast = v; node.lastRule = this; delete msg.state; delete msg.engineValue; } else if (this.isState) { switch (this.r.onInactiveType) { case 'nul': v = undefined; break; default: v = evalCmd(this.r, this.r, "onInactive", this.r.onInactiveType, this.r.onInactive, "onInactiveJS", this.msg, false); this.vLast = v; node.lastRule = this; } } if (!this.isState && this.a && this.co == 0) { this.a = false; this.msg = undefined; } if (msg !== undefined && v !== undefined) { var m; switch (this.r.outputType) { case 'msg': m = Object.assign({},msg); m[this.r.output] = v; break; case 'flow': case 'global': m = msg; var ctx = RED.util.parseContextStore(this.r.output); // Use lacy flag to check context callback directly returned. lazy=true; node.context()[this.r.outputType].set(ctx.key, v, ctx.store, (err,cur)=>{ lazy = false; var fnd = flagSave(this, false); if (err) node.error(undefined,null); // Only done if removed. if (fnd) checkDone(); }); // If context not yet stored, then add to lazy saving. if (lazy) flagSave(this, true); lazy=false; break; case 'js': m = Object.assign({},msg); runScript(node, this.r, this.r, "output", "outputJS", m, v); break; } if (so) { // If no result object, then create one and send, otherwise add to it if (res==undefined) { res = []; res[this.i] = m; send(res); } else { res[this.i] = m; } } else { send(m); } } // Show status icon. updateStatus(this, this.a?"active":"inactive"); // If is a time out/cooldown, then check if all done if (event==T_TIMEOUT) checkDone(); }, c:function(send) { // Cancel // If not active, then skip cancel if (!this.a) return; // Execute timeout this.e(this.msg, T_TIMEOUT, send); clearTimeout(this.ht); }, init:function() { var rule = this; switch (this.r.mode) { case 'single': this.v = { r:rule, // Rule e:function(rv) { return evalCmd(this.r, this.r.r, "value", this.r.r.sValueType, this.r.r.sValue, "sValueJS", this.r.msg, false); } }; break; case 'iterate': this.v = { r:rule, // Rule i:function() { // Initial value return evalCmd(this.r, this.r.r, "init", this.r.r.iInitType, this.r.r.iInit, "iInitJS", this.r.msg, true); }, c:undefined, // Current mi:undefined, // Min ma:undefined, // Max s: undefined, // Steps e:function(rv,al,neg) { // If no current or to reset value, then var r = this.c==undefined || rv; // If to reset, then if (r) { this.c = this.i(); // Validate number if (!chkNum(this.c, "invalidInit", "Invalid iterate value for rule ", this.r)) return; // If no reset, then iterate to next value } else { this.mi = Number.parseInt(RED.util.evaluateNodeProperty(this.r.r.iMin, this.r.r.iMinType, node, this.r.msg), 10); this.ma = Number.parseInt(RED.util.evaluateNodeProperty(this.r.r.iMax, this.r.r.iMaxType, node, this.r.msg), 10); this.s = Number.parseInt(RED.util.evaluateNodeProperty(this.r.r.iSteps, this.r.r.iStepsType, node, this.r.msg), 10); // Validate number if (!chkNum(this.s, "stepVal", "Invalid step value for rule ", this.r)) return; // Validate number if (!chkNum(this.mi, "minVal", "Invalid min value for rule ", this.r)) return; // Validate number if (!chkNum(this.ma, "maxVal", "Invalid max value for rule ", this.r)) return; var p; if (al) { p=this.c; if (neg!==true) this.c+=this.s; else this.c-=this.s; } // -1 < min, 1 > max var ch=this.c<this.mi?-1:this.c>this.ma?1:0; if (ch!=0) { // If move to edge, then if (this.r.r.iEdge) { if (ch==1 && p < this.ma) { this.c = this.ma; } else if (ch==-1 && p > this.mi) { this.c = this.mi; } else { // If to cycle the value, then if (this.r.r.iCycle) { this.c = ch==1?this.mi:this.ma; // If is max then, min otherwise max } else { // Otherwise, complete return undefined; } } } else { // If to cycle the value, then if (this.r.r.iCycle) { var ml = 10; // Test times var size = this.ma-this.mi; if (ch==1) { while (this.c>this.ma && ml>0) { this.c-= size; ml--; } } else { while (this.c<this.mi && ml>0) { this.c+= size; ml--; } } } else { // Otherwise, complete //this.c=this.ma; return undefined; } } } } return this.c; } }; break; case 'bounce': this.v = { r:rule, // Rule i:function() { // Initial value return evalCmd(this.r, this.r.r, "init", this.r.r.bInitType, this.r.r.bInit, "bInitJS", this.r.msg, true); }, c:undefined, // Current p:rule.r.bIPos, // Positive mode mi:undefined, // Min ma:undefined, // Max s: undefined, // Steps e:function(rv,al,neg) { var po = neg!==true; // If no current or to reset value, then var r = this.c==undefined || rv; // If to reset, then if (r) { this.c = this.i(); this.p = this.r.r.bIPos; // Validate number if (!chkNum(this.c, "invalidInit", "Invalid iterate value for rule ", this.r)) return; // If no reset, then iterate to next value } else { this.mi = Number.parseInt(RED.util.evaluateNodeProperty(this.r.r.bMin, this.r.r.bMinType, node, this.r.msg), 10); this.ma = Number.parseInt(RED.util.evaluateNodeProperty(this.r.r.bMax, this.r.r.bMaxType, node, this.r.msg), 10); if (this.p==po) { this.s = Number.parseInt(RED.util.evaluateNodeProperty(this.r.r.bUp, this.r.r.bUpType, node, this.r.msg), 10); } else { this.s = Number.parseInt(RED.util.evaluateNodeProperty(this.r.r.bDown, this.r.r.bDownType, node, this.r.msg), 10); } // Validate number if (!chkNum(this.s, "stepVal", "Invalid step value for rule ", this.r)) return; // Validate number if (!chkNum(this.mi, "minVal", "Invalid min value for rule ", this.r)) return; // Validate number if (!chkNum(this.ma, "maxVal", "Invalid max value for rule ", this.r)) return; // If positive, then convert value to negative after validation. if (this.p!=po && this.s > 0) this.s=-this.s; if (al) { this.c+=this.s; } if (this.c<this.mi || this.c>this.ma) { this.c-=this.s; this.p = !this.p; // If move to edge, then if (this.r.r.bEdge) { if (this.p!=po && this.c < this.ma) { this.c = this.ma; } else if (this.p==po && this.c > this.mi) { this.c = this.mi; } else { return this.e(false, true, neg); } } else { return this.e(false, true, neg); } } } return this.c; } }; break; case 'fixed': this.v = { r:rule, // Rule i:function() { // Initial value return evalCmd(this.r, this.r.r, "init", this.r.r.fInitType, this.r.r.fInit, "fInitJS", this.r.msg, true); }, c:undefined, // Current value v:rule.r.fValues, // values array e:function(rv, al,neg) { // Reset value, // If no current or to reset value, then var r = this.c==undefined || rv; if (r) { this.c = this.i(); } // Validate number if (!Number.isInteger(this.c)) { node.error(RED._("error.invalidIdx", {error:'Invalid fixed index for rule ' + this.r.name})); return; } // If no reset, then iterate to next value if (!r && al) { if (neg!==true) { if (this.c<this.v.length) this.c++; } else { if (this.c>=0) this.c--; } } // Validate value if (this.c < 0) { // If not negative, then just set to zero if (neg!==true) { this.c = 0; } else { if (this.r.r.fCycle) { this.c = this.v.length - 1; } else { return undefined; } } } if (this.c>=this.v.length) { // If to cycle, then if (this.r.r.fCycle) { this.c = 0; } else { // Otherwise complete return undefined; } } var v = this.v[this.c]; return evalCmd(v, v, "value", v.t, v.v, "js", this.r.msg, false); } }; break; case 'linked': this.v = { r:rule, // Rule l:r.find(ri=>ri.r.id==rule.r.lLink), // Linked rule e:function(rv,al) { var r = this.l.v.e(rv,al,this.r.r.lNeg); if (this.r.r.lUpRule) { this.l.vLast = r; } return r; } }; break; } } }; // If JSON repeat message, then static parse if (ri.r.repMsgType == 'json') { ri.msg = JSON.parse(ri.r.repMsg); } r.push(ri); if (!this.lastRule) this.lastRule = ri; } // Initiate all rules. for (var i=0; i<r.length; i++) { r[i].init(); } this.rules = r; this.on("input", function(msg, send, done) { node.done = done; lm = msg; var inp = RED.util.evaluateNodeProperty(config.inputValue, config.inputType, node, msg); if (so) { // Seperate output var res = []; r.forEach(e=> e.t(inp,msg,send,res) ); // Check if has any values to send. if (res.findIndex(re=>re!=undefined) != -1) send(res); } else { r.forEach(e=> e.t(inp,msg,send,res) ); } checkDone(); }); this.on("close", function() { r.forEach(e=> { if (e.hr!=undefined) clearInterval(e.hr); if (e.ht!=undefined) clearTimeout(e.ht); }); }); } RED.nodes.registerType("automation controller",AutomationControllerNode); }