node-red-contrib-mytimeout
Version:
A timer that supports setting the run time by passing JSON to the input
541 lines (474 loc) • 21.5 kB
JavaScript
// Author: Neil Cherry <ncherry@linuxha.com>
// I'd be much better off with a state machine
// https://www.smashingmagazine.com/2018/01/rise-state-machines/
// the on/off trigger string below whould be user configurable
//
// Inputs:
// msg.payload = {
// "payload": "on",
// "timeout": 3600,
// "warning": 300
// }
//
// The above can be used to change the timeout and warning
//
// or
//
// {
// "payload": "on"
// }
//
// {
// "payload": <Don't care>
// }
//
// The above gets treated as an on condition
//
// {
// "payload": "off"
// }
//
// {
// "payload": "off"
// }
//
// {
// "payload": "stop"
// }
//
// {
// "payload": "cancel"
// }
//
// {
// "payload": "tick"
// }
// Initial is stop
// STOP => RUN => STOP (via timeout)
module.exports = function(RED) {
"use strict";
/*
node-input-name
node-input-outtopic
node-input-outsafe
node-input-outwarning
node-input-warning
node-input-timer
node-input-limit
node-input-repeat
node-input-again
node-input-atStart
*/
var countdownNode = function(n) {
// Local variables
var node = this;
var state = 'stop';
var wflag = false;
var ticks = -1; //
var lastPayload = Date.now();; //
var timeout = parseInt(n.timer||30); //
var warn = parseInt(n.warning||10); //
var ignoreCase = '';
var line = {};
var version = '3.2.2'; //
RED.nodes.createNode(this, n);
// GUI variables
node.timer = parseInt(n.timer||30); // Need to build checking into the html file
node.state = 'stop'; // For now it's a string, later an object?
/*
This is the Edit dialog for this node
node properties
Name Countdown timer
Output Topic topic <- if blank no output topic
Timer On payload on
Warning state payload Warning
Timer Off payload off
Countdown (secs) 30
Warning (secs) 5
Rate Limit (msg/secs) 30
[ ] Repeat message every second <- This doesn't seem like a good idea (???)
[ ] Auto-restart when timed out
[ ] Run at start
*/
node.name = n.name; // node-input-name - Name
node.warnT = parseInt(n.warning)||5;// node-input-warning - time in seconds (?)
node.topic = n.outtopic; // node-input-outtopic - Output topic
node.outsafe = n.outsafe; // node-input-outsafe - Timer on payload
node.outwarn = n.outwarning; // node-input-outwarning - Warning state payload
node.outunsafe = n.outunsafe || "off"; // node-input-outunsafe - Timer off payload
// // node-input-warning - warning seconds
// // node-input-timer - countdown seconds
// // node-input-repeat - Rate limit seconds
node.repeat = n.repeat; // node-input-repeat - Repeat message every second
node.again = n.again; // node-input-again - Auto restart when timed out
node.atStart = n.atStart; // node-input-atStart - Run at start
//
function ndebug(s){
// debug is either true or false
if(n.ndebug) { // only if true do we debug
node.log(s); // Can't use the obj this here, used obj node
}
}
if(n.ignoreCase) { // Only if true do we ignore case
ignoreCase = 'i';
}
// this.log("Debug = (" + n.ndebug + ")"); // Can't use the obj node here, use obj this
this.status({
fill : "red",
shape : "dot",
text : "Stopped: init"
});
// =====================================================================
// There can be:
// payload = "on" or 1 or "1" or anthing else except off/stop/cancel/debug
function on(msg) {
// My intention is to move all the calculations for
// these variables to here. At the moment they're all over
// the place and confusing
if(n.ndebug) {
ndebug(" msg: " + JSON.stringify(msg));
ndebug(" msg: " + typeof(msg));
ndebug(" node.timer: " + node.timer);
ndebug(" node.warnT: " + node.warnT);
try {
ndebug(" msg.timeout:" + msg.timeout);
ndebug(" msg.warning:" + msg.warning);
} catch(e) {
ndebug(" msg.timeout:undefined");
ndebug(" msg.warning:undefined");
}
ndebug(" timeout: " + timeout);
ndebug(" warn: " + warn);
ndebug(" ticks: " + ticks);
}
//
// There are 3 sets of variables
// default values (node.timer, node.warnT)
// passed values (timeout, warn - if any)
// running values (ticks)
//
timeout = msg.timeout||timeout||node.timer;
timeout = parseInt(timeout); // ncherry@linuxha.com parseInt()
if(msg.warning != 0) {
warn = msg.warning||warn||node.warnT;
} else {
warn = 0;
}
warn = parseInt(warn); // ncherry@linuxha.com parseInt()
ticks = timeout;
if(n.ndebug) {
ndebug("");
ndebug(" node.timer: " + node.timer);
ndebug(" node.warnT: " + node.warnT);
ndebug(" timeout: " + timeout);
ndebug(" warn: " + warn);
ndebug(" ticks: " + ticks);
ndebug("Count timer on");
}
node.status({
fill : "green",
shape : "dot",
text : "Running: " + ticks // provide a visual countdown
});
// @Feature: no on msg
if(msg.payload) {
msg.payload = node.outsafe;
}
msg.topic = node.topic;
lastPayload = msg.payload;
ndebug("Send Green: " + lastPayload || "''");
if(msg.payload) { // @FEATURE: no on msg
node.send([msg, null]);
}
state = 'run';
wflag = false; // rest the warning flag
} // on(msg)
function off() {
ndebug("off!");
ticks = -1;
stop('off');
} // off()
// I'm about to make this a bit complicated
// In: off out: off
// In: stop out: stop
// In: cancel out: (nothing)
function stop(s) {
// The states are only called from the node.on("input", ...)
// That's where I define the line obj after I massage the incoming
// message
var msg = line; // This is a biy of a risk, I'll leave it for now
// if the main input routine calls this function, it will pass an
// object (the input msg) which we don't care about really
if(typeof(s) !== "string") {
ndebug("Empty stop");
s = 'stop';
}
ndebug(s + "! A");
ticks = 0;
node.status({
fill : "red",
shape : "dot",
text : "Stopped: " + s // provide a visual countdown
});
// Stop or off can send, not cancel
switch(s) {
case 'stop':
msg.payload = "stop";
lastPayload = msg.payload;
ndebug("Send Red: " + lastPayload);
var tremain = {"payload": -1, "state": 0, "flag": "stop"};
node.send([msg, tremain]);
break;
case 'off':
msg.payload = node.outunsafe;
msg.topic = node.topic;
lastPayload = msg.payload;
ndebug("Send red: " + lastPayload);
var tremain = { "payload": 0, "state": 0, "flag": "off"};
node.send([msg, tremain]);
break;
case 'cancel':
ndebug("Send red: null");
var tremain = { "payload": -1, "state": 0, "flag": "cancel"};
lastPayload = Date.now();
node.send([null, tremain]);
break;
default:
ndebug("Send red: ???");
var tremain = { "payload": -1, "state": 0, "flag": "unknown"};
lastPayload = Date.now();
node.send([null, tremain]);
break;
}
state = 'stop';
ticks = -1;
timeout = parseInt(node.timer);
warn = parseInt(node.warnT);
ndebug('=[ fini ]=======================================================================');
}
function cancel() {
ndebug("cancel!");
stop('cancel');
ticks = -1;
}
function doNothing() {
ndebug("doNothing!");
state = 'stop';
ticks = -1;
}
// @TODO: This should return the original msg with as few changes as possible
function newMsg(msg) {
// Pretty much a deep clone but I don't think it
// will copy things like sockets
//nMsg.payload = JSON.parse(JSON.stringify(msg));
var nMsg = Object.assign(msg, {}); // create a copy perhaps use this: JSON.parse(JSON.stringify(msg));
// Okay now I need to figure out the format
// is it?
// A: { "payload": "on", "timeout": 30, ... }
// or
// B: { "payload": { "payload": "on", "timeout": 30, ... } }
// or
// C: { "payload": "{ \"payload\": \"on\", \"timeout\": 30, \"extra\":\"inside\" }", "other":"other", "extra": "outside" }
switch(typeof(msg.payload)) {
case "string":
ndebug("Str msg = " + JSON.stringify(msg));
if(/.*"payload".*/.test(msg.payload)) {
ndebug(" msg.payload = " + JSON.stringify(msg.payload));
// string contain payload, convery to JSON
var t = newMsg(JSON.parse(msg.payload));
nMsg = Object.assign(msg, t); // Perhaps I should use nMsg instead msg (and copy msg to nMsg above)?
}
//
try{
nMsg.payload = nMsg.payload.toLowerCase();
} catch(e) {
nMsg.payload = nMsg.payload.toString().toLowerCase();
}
break;
case "number":
ndebug("Num msg = " + JSON.stringify(msg));
nMsg.payload = msg.payload.toString();
break;
case "object":
var m = msg.payload;
ndebug("Obj msg = " + JSON.stringify(m));
t = JSON.parse(JSON.stringify(m));
ndebug("obj m = " + JSON.stringify(m));
nMsg = Object.assign(msg, t);
ndebug("obj t = " + JSON.stringify(t));
ndebug("obj nmsg = " + JSON.stringify(nMsg));
break;
default:
ndebug("??? msg = " + JSON.stringify(msg));
ndebug("??? msg = " + typeof(msg.payload));
nMsg.payload = "";
break;
}
ndebug("RTN msg = " + JSON.stringify(nMsg));
return nMsg;
}
// Leave this here, need the functions ref from above
var states = {
// Not sure if this is what I want in the long run but this is good for now
stop: { 0: off, on: on, off: off, stop: doNothing, cancel: doNothing },
run: { 0: off, on: on, off: off, stop: stop, cancel: cancel }
};
// -------------------------------------------------------------------------------
// Commands
// TIX
node.on("TIX", function(inMsg) {
lastPayload = Date.now();
var msg = {};
if(ticks > 0) {
// A blank outwarning means don't send
if((warn >= ticks) && (node.outwarn !== "")) {
// a warn of 0 seconds also means don't send
if (warn) {
// Timer at warning
node.status({
fill : "yellow",
shape : "dot",
text : "Warning: " + ticks // provide a visual countdown
});
if(!wflag) {
msg = Object.assign(msg, line); // Copy the original attributes into the new msg
msg.payload = node.outwarn;
msg.topic = node.topic;
lastPayload = msg.payload;
ndebug("Send Yellow: " + lastPayload);
node.send([msg, null]);
wflag = true;
}
} // warn if there's a warn message
var tremain = { "payload": ticks, "state": 2, "flag": "warn >= ticks"};
node.send([null, tremain]);
} else {
// HOW does this get sent out?
node.status({
fill : "green",
shape : "dot",
text : "Running: " + ticks // provide a visual countdown
});
var tremain = { "payload": ticks, "state": 1, "flag": "ticks > 0"};
node.send([null, tremain]);
}
ticks--;
} else if(ticks == 0) {
ndebug("ticks == 0");
stop("off");
//var tremain = { "payload": 0, "state": 0, "flag": "ticks == 0"}};
//node.send([null, tremain]);
ticks = -1;
} else {
// Do nothing
}
}); // node.on("TIX", function {});
// Stop (initial state at start up and when not running)
// On (timer reset to default value and running, on sent)
// Off (timer off, off sent, return to Stop)
// Cancel (timer off, nothing sent, return to Stop)
// Warning (timer still on, warning sent)
// Timeout (timer off, off sent, return to Stop
node.on( "input", function(inMsg) {
// inMsg = {"topic":"home/test/countdown-in-b","payload":"{ \"payload\":\"on\",\"timeout\":6,\"warning\":3}","qos":0,"retain":false,"_msgid":"10ea6e2f.68fb32"}
// inMsg = {"topic":"home/test/countdown-in-b","payload":"on","qos":0,"retain":false,"_msgid":"fd875a01.526a68"}
if(n.ndebug) {
ndebug('=[ input ]======================================================================');
ndebug('1 node.input("input");');
ndebug("1 inMsg = " + JSON.stringify(inMsg));
ndebug("1 State = " + state);
ndebug("1 timeout = " + node.timer + " - node.timer");
ndebug("1 timeout = " + timeout + " - timeout");
ndebug("1 warning = " + node.warnT);
}
// =================================================================
// First we need to drop any message than matches the last message
// sent. This will keep us from getting into an infinite loop.
// =================================================================
// From here to the try { } catch {} we should only need to get the
// inMsg's payload into line.payload (could be a string or object)
// This is taken from myTimeout.js (node-red-contrib-mytimeout)
// It's purposed is to stop from repeating the same message from
// being resent (an endless loop). For some reason this is on the
// isString() check ... hmmm
// =================================================================
// ================================================================================
// =========================================================
// Okay here's what I expect, user sends something (anything)
// node send node.outsafe,
// if node sees the exact string node.outstring just sent
// then drop it
// If the timer goes off then the lastPayload should be cleared
// We only send a simple message ('on', 'off', 'stop' or 'cancel')
var regex = new RegExp('^' + lastPayload + '$', ignoreCase);
//if(lastPayload === inMsg.payload) {
if(regex.test(inMsg.payload)) {
// So it's the same message as what was previously sent
ndebug("4 TO: In == Out match, skip (" + lastPayload + "/" + inMsg.payload + ")");
ndebug('=[ Skip ]=======================================================================');
lastPayload = Date.now();
return ; //
}
try {
line = newMsg(inMsg);
} catch (err) {
// Basically treat the unknown payload as an on (i.e. anything can
// tickle the timer as long as it's not off, 0, stop or cancel)
node.warn("L477: newMsg(inMsg): " + err + " <" + JSON.stringify(inMsg) + ">");
}
ndebug("line = " + JSON.stringify(line));
// ================================================================================
var s = "nada";
try {
states[state][line.payload](line);
} catch(err) {
// =============================================================
// Anything that is not an existing state/function is treated as
// an on request.
// =============================================================
ndebug("state = <" + line.payload + ">" + " (this is not an error - treated as 'on')");
ndebug("states catch: " + err + " (" + ticks + "/" + warn + " - this is not an error)");
// If it's not an existing state then treat it as an on
// that way anthing can be used as a kicker to keep the timer
// running
on(line);
}
/* This is handled in newMsg(msg) above
try {
if(typeof(line.payload) == 'string') {
s = ("1 states catch: line \"" + line.payload);
states[state][line.payload](line);
} else {
// Hopefully I've converted all the input to a string and
// to lower case.
s = ("2 states catch: line \"" + line.payload.payload);
states[state][line.payload.payload](line);
}
} catch(err) {
// =============================================================
// Anything that is not an existing state/function is treated as
// an on request.
// =============================================================
ndebug(s + "\" (this is not an error)");
ndebug("states catch: " + err + " (" + ticks + "/" + warn + " - this is not an error)");
// If it's not an existing state then treat it as an on
// that way anthing can be used as a kicker to keep the timer
// running
on(line);
} /* */
// ================================================================================
}); // node.on("input", ... )
// Once the node is instantiated this keeps running
// I'd like to change this to run when only needed
var tick = setInterval(function() {
var msg = { payload:'TIX', topic:""};
node.emit("TIX", msg);
}, 1000); // trigger every 1 sec
node.on("close", function() {
if (tick) {
clearInterval(tick);
}
});
} // function myTimeoutNode(n);
RED.nodes.registerType("mytimeout", countdownNode);
} // module.exports