Multiple Timer Subflow

This subflow/node facilitates the setting of multiple timers in an easy manner. For an example flow and some more infos check out this forum post.

Input Message to set timer

The subflow expects an input msg which contains the following to set a timer:

  • a msg.payload which will be send once the timer has finished
  • a msg.duration either in the format of a number in seconds or a string in the "hh:mm:ss" format
  • an optional msg.topic which will also be send on completion

Other Control Messages

There are also a number of other control messages which the subflow accept:

  • a msg.payload of reset will reset all running timers
  • a msg.payload of debug will send an array of all set timers to the secon debug output.
  • a msg with a duration in seconds as a number or a string in the hh:mm:ss format with msg.delete set to true will delete the first set timer with a matching duration

Configuration Settings

The subflow although offers several menu options which can be adjusted:

  • there is a setting which determines if a new timer should be appended to the list of existing timers and run in parallel or if every new timer should replace the previous
  • a setting for the output format of the duration and the time left to-go which affects both the status display and the debug messages
  • a setting to enable/disable the progress status being shown on the node status
  • a setting which when enabled sends a debug message every second to the second output

Debug Message Format

The node sends a debug message to its second output on several events or in addition every second if configured. Those events are:

  • When a new timer is set
  • When a timer completes
  • When a timer is deleted
  • When the timers are reset

The format of this message is an array which includes one object for every timer running that includes useful information about each timer in this format:

{
    "payload":"ON",
    "duration":"00:00:25",
    "due":1607330155654,
    "toGo":"00:00:25",
    "topic":"Test"
}

the toGo and duration properties will be in the format configured in the nodes settings.

[{"id":"d3399d9e.0ff088","type":"subflow","name":"timers","info":"This subflow/node facilitates the setting\nof multiple timers in an easy manner.\n\n# Input Message to set timer\n\nThe subflow expects an input `msg` which\ncontains the following to set a timer:\n\n+ a `msg.payload` which will be send once the timer has finished\n+ a `msg.duration` either in the format of a number in seconds or a string in the \"hh:mm:ss\" format\n+ an optional `msg.topic` which will also be send on completion\n\n# Other Control Messages\n\nThere are also a number of other control\nmessages which the subflow accept:\n\n+ a `msg.payload` of `reset` will reset all running timers\n+ a `msg.payload` of `debug` will send an array of all set timers to the secon debug output.\n+ a `msg` with a duration in seconds as a number or a string in the hh:mm:ss format with `msg.delete` set to true will delete the first set timer with a matching duration\n\n# Configuration Settings\n\nThe subflow although offers several menu options which can be adjusted:\n\n+ there is a setting which determines if a new timer should be appended to the list of existing timers and run in parallel or if every new timer should replace the previous\n+ a setting for the output format of the duration and the time left to-go which affects both the status display and the debug messages\n+ a setting to enable/disable the progress status being shown on the node status\n+ a setting which when enabled sends a debug message every second to the second output\n\n# Debug Message Format\nThe node sends a debug message to its second output on several events or in addition every second if configured. Those events are:\n\n+ When a new timer is set\n+ When a timer completes\n+ When a timer is deleted\n+ When the timers are reset\n\nThe format of this message is an array which\nincludes one object for every timer running\nthat includes useful information about each\ntimer in this format:\n```\n{\n    \"payload\":\"ON\",\n    \"duration\":\"00:00:25\",\n    \"due\":1607330155654,\n    \"toGo\":\"00:00:25\",\n    \"topic\":\"Test\"\n}\n```\nthe `toGo` and `duration` properties will be in the format configured in the nodes settings.","category":"","in":[{"x":100,"y":140,"wires":[{"id":"fe62abda.f8f7a"}]}],"out":[{"x":840,"y":140,"wires":[{"id":"c50c6881.b2fdf","port":0}]},{"x":840,"y":200,"wires":[{"id":"12c839f6.01eed6","port":0}]}],"env":[{"name":"on_input","type":"str","value":"append","ui":{"label":{"en-US":"On new input"},"type":"select","opts":{"opts":[{"l":{"en-US":"append to list of current timers"},"v":"append"},{"l":{"en-US":"replace current timer"},"v":"replace"}]}}},{"name":"time_format","type":"str","value":"hhmmss","ui":{"label":{"en-US":"Time format of output/status"},"type":"select","opts":{"opts":[{"l":{"en-US":"hh:mm:ss"},"v":"hhmmss"},{"l":{"en-US":"seconds"},"v":"seconds"}]}}},{"name":"show_progress","type":"bool","value":"true","ui":{"label":{"en-US":"show progress in status"},"type":"checkbox"}},{"name":"send_progress","type":"bool","value":"false","ui":{"label":{"en-US":"send progress to second output"},"type":"checkbox"}}],"color":"#FDF0C2","icon":"node-red/timer.svg","status":{"x":840,"y":80,"wires":[{"id":"415b9d3f.6e5754","port":0}]}},{"id":"fe326ef6.0184b","type":"function","z":"d3399d9e.0ff088","name":"tick","func":"const timers = flow.get(\"timers\") || [];\nif (msg.delete === true || timers.length > 0) {\n    return msg;\n}\nconst start = Date.now();\nconst startCorrected = (Math.ceil(start / 1000)) * 1000;\nfunction initial() {\n    initialTimeout = startCorrected - start;\n    setTimeout(()=>{\n        tick(1000);\n        node.send({payload:\"tick\"});\n    },initialTimeout);\n}\nfunction tick(timeout) {\n    setTimeout(()=>{\n        if (flow.get(\"timers\").length > 0) {\n            const newStart = Date.now();\n            const offset = (newStart-startCorrected) % 10;\n            const newTimeout = (offset > 0) ? 1000-(offset) : 1000;\n            tick(newTimeout);\n            node.send({payload:\"tick\"});\n        }\n    },timeout)\n}\ninitial();\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":550,"y":140,"wires":[["c50c6881.b2fdf","415b9d3f.6e5754"]]},{"id":"c50c6881.b2fdf","type":"function","z":"d3399d9e.0ff088","name":"check timers","func":"const now = Date.now();\nlet timers = flow.get(\"timers\") || [];\nif (msg.payload === \"tick\") {\n    if (timers.length === 0) { return null; } \n    let done = [];\n    timers.forEach(timer => {\n        if (timer.due < now) {\n            let outputMsg = {};\n            outputMsg.payload = timer.payload;\n            if (timer.hasOwnProperty(\"topic\")) { outputMsg.topic = timer.topic }\n            node.send([outputMsg, null]);\n            done.push(timer);\n        }\n    });\n    if (done.length !== 0) {\n        done.forEach(item => {\n            let toDelete = timers.indexOf(item);\n            timers.splice(toDelete, 1);\n        });\n        node.send([null,{payload:timers}]);\n    }\n    timers.forEach((timer, index) => {\n        timers[index].toGo = Math.round((timer.due - now) / 1000);\n    });\n    if (env.get(\"send_progress\") === true && done.length === 0 ) {\n        node.send([null,{payload:timers}]);\n    }\n} else if (msg.delete) {\n    for(i=0;i<timers.length;i++) {\n        if (timers[i].duration === msg.payload) {\n            timers.splice(i, 1);\n            node.send([null,{payload:timers}]);\n            break;\n        }\n    }\n} else {\n    let newTimer = {\n        payload: msg.payload,\n        duration: msg.duration,\n        due: now + (msg.duration * 1000),\n        toGo: msg.duration\n    };\n    if (msg.hasOwnProperty(\"topic\")) { newTimer.topic = msg.topic }\n    if (env.get(\"on_input\") === \"append\") {\n        timers.push(newTimer);\n    } else {\n        timers = [newTimer];\n    }\n    node.send([null,{payload:timers}]);\n}\nflow.set(\"timers\",timers);\nreturn null;","outputs":2,"noerr":0,"initialize":"","finalize":"","x":710,"y":140,"wires":[[],["12c839f6.01eed6"]]},{"id":"415b9d3f.6e5754","type":"function","z":"d3399d9e.0ff088","name":"set status","func":"function convertFormat(input){\n    let hoursRaw = input / 3600;\n    hoursRaw = hoursRaw.toString().split(\".\");\n    let hours = hoursRaw[0];\n    let minutesRaw = (input / 60) - (Number(hours) * 60);\n    minutesRaw = minutesRaw.toString().split(\".\");\n    let minutes = minutesRaw[0];\n    let seconds = (input - (Number(hours) * 3600) - (Number(minutes) * 60)).toString();\n    if (hours.length === 1) { hours = \"0\" + hours; }\n    if (minutes.length === 1) { minutes = \"0\" + minutes; }\n    if (seconds.length === 1) { seconds = \"0\" + seconds; }\n    const output = `${hours}:${minutes}:${seconds}`;\n    return output;\n}\nconst timers = flow.get(\"timers\") || [];\nif (env.get(\"show_progress\") === true) {\n    if (timers.length !== 0) {\n        const now = Date.now();\n        let newPayload = \"\";\n        timers.forEach(timer => {\n            let toGo = Math.round((timer.due - now) / 1000);\n            let addPayload = \"\";\n            if (env.get(\"time_format\") === \"seconds\") {\n                addPayload = `${toGo}s of ${timer.duration}s to go`;\n            } else {\n                let newDuration = convertFormat(timer.duration);\n                let newToGo = convertFormat(toGo);\n                addPayload = `${newToGo} of ${newDuration} to go`;\n            }\n            if (newPayload.length !== 0) { newPayload += \", \"; }\n            newPayload += addPayload;\n        });\n        node.send({payload:newPayload});\n    } else {\n        node.send({payload:\"no timer\"});\n    }\n}\nreturn null;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":700,"y":80,"wires":[[]]},{"id":"fe62abda.f8f7a","type":"switch","z":"d3399d9e.0ff088","name":"","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"reset","vt":"str"},{"t":"eq","v":"debug","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":3,"x":210,"y":140,"wires":[["1e5213c7.d9c46c"],["1461b44c.50eae4"],["595b735c.08ff54"]]},{"id":"1e5213c7.d9c46c","type":"change","z":"d3399d9e.0ff088","name":"reset","rules":[{"t":"set","p":"timers","pt":"flow","to":"[]","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":350,"y":80,"wires":[["415b9d3f.6e5754","1461b44c.50eae4"]]},{"id":"595b735c.08ff54","type":"function","z":"d3399d9e.0ff088","name":"error checking","func":"if (typeof msg.duration === \"string\" && msg.duration.match(/[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}/g)) {\n    msg.duration = msg.duration.split(\":\");\n    msg.duration = (Number(msg.duration[0]) * 3600) + (Number(msg.duration[1]) * 60) + Number(msg.duration[2]);\n}\nif (msg.delete === true && typeof msg.payload === \"string\" && msg.payload.match(/[0-9]{2}\\:[0-9]{2}\\:[0-9]{2}/g)) {\n    msg.payload = msg.payload.split(\":\");\n    msg.payload = (Number(msg.payload[0]) * 3600) + (Number(msg.payload[1]) * 60) + Number(msg.payload[2]);\n}\nif (msg.delete !== true) {\n    if (msg.duration === undefined || typeof msg.duration !== \"number\" || msg.duration <= 0) {\n        node.warn(\"input message needs a msg.duration property which is either of type number or a string in the hh:mm:ss second format\");\n        return null;\n    } else if (msg.payload === undefined) {\n        node.warn(\"msg.payload needs to be defined\");\n        return null;\n    }\n} else if (typeof msg.payload !== \"number\" || msg.payload <= 0) {\n    node.warn(\"if msg.delete is true msg.payload needs to be of type number and coresponding to the duration of the timer to be deleted\");\n    return null;\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":380,"y":140,"wires":[["fe326ef6.0184b"]]},{"id":"12c839f6.01eed6","type":"function","z":"d3399d9e.0ff088","name":"format output","func":"function convertFormat(input){\n    let hoursRaw = input / 3600;\n    hoursRaw = hoursRaw.toString().split(\".\");\n    let hours = hoursRaw[0];\n    let minutesRaw = (input / 60) - (Number(hours) * 60);\n    minutesRaw = minutesRaw.toString().split(\".\");\n    let minutes = minutesRaw[0];\n    let seconds = (input - (Number(hours) * 3600) - (Number(minutes) * 60)).toString();\n    if (hours.length === 1) { hours = \"0\" + hours; }\n    if (minutes.length === 1) { minutes = \"0\" + minutes; }\n    if (seconds.length === 1) { seconds = \"0\" + seconds; }\n    const output = `${hours}:${minutes}:${seconds}`;\n    return output;\n}\nif(env.get(\"time_format\") === \"hhmmss\") {\n    let newMsg = RED.util.cloneMessage(msg);\n    newMsg.payload.forEach(timer => {\n        timer.duration = convertFormat(timer.duration);\n        timer.toGo = convertFormat(timer.toGo);\n    });\n    return newMsg;\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":700,"y":200,"wires":[[]]},{"id":"8370617b.6272a","type":"inject","z":"d3399d9e.0ff088","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":170,"y":80,"wires":[["1e5213c7.d9c46c"]]},{"id":"1461b44c.50eae4","type":"change","z":"d3399d9e.0ff088","name":"timers","rules":[{"t":"set","p":"payload","pt":"msg","to":"timers","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":350,"y":200,"wires":[["12c839f6.01eed6"]]}]

Flow Info

Created 3 years, 7 months ago
Rating: not yet rated

Actions

Rate:

Node Types

Core
  • change (x2)
  • function (x5)
  • inject (x1)
  • switch (x1)
Other
  • subflow (x1)

Tags

  • timer
  • timers
  • timeout
  • time
  • delay
  • multiple
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option