Skip to content

Instantly share code, notes, and snippets.

@johanneskropf
Last active August 15, 2022 12:09
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save johanneskropf/f6ddf18d9c0b36a5f0131189af22f928 to your computer and use it in GitHub Desktop.
Save johanneskropf/f6ddf18d9c0b36a5f0131189af22f928 to your computer and use it in GitHub Desktop.
Event Scheduler

About

A scheduler that repeatedly executes schedules put in as an array of objects. Examples and a Dashboard Ui Scheduler Example Flow can be found here in this thread:

https://discourse.nodered.org/t/announce-scheduler-subflow-and-dashboard-ui-scheduler-updated/36399?u=jgkk

Usage

The msg.payload input format to set a schedule is:

[
  {
    "item": "test",
    "command": "ON",
    "time": "08:30"
  },
  {
    "item": "test",
    "command": "OFF",
    "time": "19:45"
  }
]

Each schedule object can also contain an optional days property:

  {
    "item": "test",
    "command": "ON",
    "days": [1,2,6,7],
    "time": "08:30"
  }

the days property has the format of an array. Each day that the schedule object should be executed on has to be in the array. The week starts with monday(1) and ends with sunday(7). So the example above would only be executed on monday, tuesday, saturday and sunday. You can mix objects with and without a days property in one schedule array.

Sending an empty array as a msg.payload to the subflow will reset it. The second output is a debug. Sending a msg.payload string of "debug" will send an object of the schedule and the corresponding timers that are scheduled to this output. At schedule time a msg object will be send from the first output. The command will be passed as the msg.payload, the item in msg.item and the original schedule object in msg.original. The subflows status will show the next item to be executed once a schedule is set. Commands can be of type string, of type number or of type boolean.

[{"id":"dc275690.07356","type":"subflow","name":"scheduler","info":"A scheduler that repeatedly executes\nschedules put in as an array of objects\nas a ```msg.payload``` in the format of:\n```\n[\n {\n \"item\": \"test\",\n \"command\": \"ON\",\n \"time\": \"08:30\"\n },\n {\n \"item\": \"test\",\n \"command\": \"OFF\",\n \"time\": \"19:45\"\n }\n]\n```\nEach schedule object can also contain an optional ```days``` property:\n```\n {\n \"item\": \"test\",\n \"command\": \"ON\",\n \"days\": [1,2,6,7],\n \"time\": \"08:30\"\n }\n```\nthe ```days property``` has the format of\nan array. Each day that the schedule object \nshould be executed on has to be in the array.\nThe week starts with monday(1) and ends with\nsunday(7). So the example above would only be\nexecuted on monday, tuesday, saturday and\nsunday.\nYou can mix objects with and without a\ndays property in one schedule array.\n\nSending an **empty array** as a\n```msg.payload``` to the subflow will\nreset it.\nThe second output is a debug.\nSending a ```msg.payload``` string of **\"debug\"**\nwill send an object of the schedule and the\ncorresponding timers that are scheduled\nto this output.\nAt schedule time a ```msg``` object will be send\nfrom the first output. The command will be\npassed as the ```msg.payload```, the item in\n```msg.item``` and the original schedule object\nin ```msg.original```.\nThe subflows status will show the next item to \nbe executed once a schedule is set.","category":"","in":[{"x":80,"y":180,"wires":[{"id":"be194cd9.78b218"}]}],"out":[{"x":1720,"y":180,"wires":[{"id":"8eb795c0.24899","port":0}]},{"x":560,"y":120,"wires":[{"id":"6680ab33.f83b5c","port":0}]}],"env":[],"color":"#FFAAAA","inputLabels":["schedule input"],"outputLabels":["command output",""],"icon":"node-red/timer.svg","status":{"x":1320,"y":360,"wires":[{"id":"a0ba0058.4cf528","port":0},{"id":"3bf2be21.9401aa","port":1}]}},{"id":"f476595.aa436a8","type":"function","z":"dc275690.07356","name":"schedule function","func":"const schedule = msg.payload;\nif(typeof msg.payload === \"undefined\") return null;\nlet scheduled = context.scheduled || [];\nlet todelete = [];\nscheduled.forEach((item,index) => {\n if(!schedule.some(element => JSON.stringify(element) == JSON.stringify(item.schedule))){\n clearTimeout(item.timer);\n todelete.push(index);\n }\n})\nscheduled = scheduled.filter((item,index) => !todelete.includes(index));\ncontext.scheduled = scheduled;\nschedule.forEach(element => {\n const execute = element;\n const time = new Date();\n const timestamp = time.getTime();\n const hour = time.getHours();\n const minute = time.getMinutes();\n const year = time.getFullYear();\n const month = time.getMonth();\n const day = time.getDate();\n const inputS = execute.time.split(\":\");\n const hourS = parseInt(inputS[0]);\n const minuteS = parseInt(inputS[1]);\n if(typeof hourS != \"number\" || typeof minuteS != \"number\") return null;\n const timeS = new Date(year, month, day, hourS, minuteS);\n const timestampS = timeS.getTime();\n let timestampD = 0;\n if(timestampS >= timestamp){\n timestampD = timestampS - timestamp;\n } else {\n timestampD = (timestampS + 86400000) - timestamp;\n }\n let oldscheduled = context.scheduled;\n if(!oldscheduled.some(element => JSON.stringify(element.schedule) == JSON.stringify(execute))){\n const newtimer = setTimeout(()=>{\n let newscheduled = context.scheduled;\n const deleteindex = newscheduled.indexOf(newschedule);\n newscheduled.splice(deleteindex,1);\n node.send({payload:newschedule.schedule});\n context.scheduled = newscheduled;\n },timestampD);\n const newschedule = {\n runtime: timestampD,\n timer: newtimer,\n schedule: execute\n };\n oldscheduled.push(newschedule);\n context.scheduled = oldscheduled;\n }\n});\nconst sendscheduled = context.scheduled;\nmsg.topic = \"scheduled\";\nconst newmsg = sendscheduled.map(item => {\n return {schedule:item.schedule,runtime:item.runtime}\n});\nmsg.payload = newmsg;\nreturn msg;","outputs":1,"noerr":0,"x":970,"y":180,"wires":[["cd6382ee.28d1c8"]]},{"id":"a0ba0058.4cf528","type":"function","z":"dc275690.07356","name":"get next schedule item","func":"const schedule = flow.get(\"schedule\") || [];\nif(schedule.length === 0){\n msg.payload = \"no schedule yet\";\n return msg;\n}\nconst time = new Date();\nconst dayNames = [\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\",\"Saturday\",\"Sunday\"];\nlet day = time.getDay();\nif (day === 0) { day = 7; }\nlet hour = String(time.getHours());\nlet minute = String(time.getMinutes());\nif(hour.length == 1) hour = \"0\" + hour;\nif(minute.length == 1) minute = \"0\" + minute;\nlet hmtime = hour + \":\" + minute;\nlet found = false;\nlet nextindex = 0;\nfor(let a=0; a<7; a++){\n for(i=0;i<schedule.length;i++){\n if(hmtime < schedule[i].time){\n if(schedule[i].hasOwnProperty(\"days\")){\n if(schedule[i].days.includes(day)){\n nextindex = i;\n found = true;\n break;\n } else {\n continue;\n }\n } else {\n nextindex = i;\n found = true;\n break;\n }\n } else {\n continue;\n }\n }\n if(found){break;}\n if(a === 0){ hmtime = \"\"; }\n if (day < 7) {\n day += 1;\n } else {\n day = 1;\n }\n}\nmsg.payload = schedule[nextindex].item + \", \" + schedule[nextindex].command + \", \" + dayNames[day-1] + \", \" + schedule[nextindex].time;\nreturn msg;","outputs":1,"noerr":0,"x":1140,"y":360,"wires":[[]]},{"id":"7f1ec532.7fc8bc","type":"function","z":"dc275690.07356","name":"sort schedule and save to flow","func":"const oldschedule = msg.payload;\nlet newschedule = [];\noldschedule.forEach(element => {\n let newindex = null;\n if(newschedule.length > 0){\n for(i=0;i<newschedule.length-1;i++){\n if(element.time >= newschedule[i].time && element.time < newschedule[i+1].time){\n newindex = i+1;\n }\n }\n if(newindex !== null){\n newschedule.splice(newindex,0,element);\n } else if (element.time < newschedule[0].time){\n newschedule.splice(0,0,element);\n } else {\n newschedule.push(element);\n }\n } else {\n newschedule.push(element);\n }\n});\nflow.set(\"schedule\",newschedule);\nmsg.payload = newschedule;\nreturn msg;","outputs":1,"noerr":0,"x":670,"y":180,"wires":[["a0ba0058.4cf528","f476595.aa436a8"]]},{"id":"694a5ae3.e4714c","type":"inject","z":"dc275690.07356","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":"1","x":410,"y":360,"wires":[["a0ba0058.4cf528","cf9ed7a2.81f21"]]},{"id":"be194cd9.78b218","type":"switch","z":"dc275690.07356","name":"","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"debug","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":210,"y":180,"wires":[["6680ab33.f83b5c"],["3bf2be21.9401aa"]]},{"id":"8eb795c0.24899","type":"change","z":"dc275690.07356","name":"","rules":[{"t":"move","p":"payload","pt":"msg","to":"original","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"original.command","tot":"msg"},{"t":"set","p":"item","pt":"msg","to":"original.item","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1560,"y":180,"wires":[[]]},{"id":"cd6382ee.28d1c8","type":"switch","z":"dc275690.07356","name":"","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"scheduled","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":1150,"y":180,"wires":[["7e108ed9.acbd88"],["a0ba0058.4cf528","280402d5.27b2f6","ab8f82a4.0079b"]]},{"id":"6680ab33.f83b5c","type":"change","z":"dc275690.07356","name":"","rules":[{"t":"delete","p":"payload","pt":"msg"},{"t":"set","p":"payload.schedule","pt":"msg","to":"schedule","tot":"flow"},{"t":"set","p":"payload.scheduled","pt":"msg","to":"scheduled","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":400,"y":120,"wires":[[]]},{"id":"cf9ed7a2.81f21","type":"trigger","z":"dc275690.07356","op1":"[]","op2":"schedule","op1type":"json","op2type":"flow","duration":"1","extend":false,"units":"s","reset":"","bytopic":"all","name":"","x":760,"y":240,"wires":[["f476595.aa436a8"]]},{"id":"7e108ed9.acbd88","type":"change","z":"dc275690.07356","name":"","rules":[{"t":"set","p":"scheduled","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1330,"y":120,"wires":[[]]},{"id":"280402d5.27b2f6","type":"trigger","z":"dc275690.07356","op1":"","op2":"schedule","op1type":"nul","op2type":"flow","duration":"1","extend":false,"units":"s","reset":"","bytopic":"all","name":"","x":960,"y":120,"wires":[["f476595.aa436a8"]]},{"id":"ab8f82a4.0079b","type":"function","z":"dc275690.07356","name":"today?","func":"if (msg.payload.hasOwnProperty(\"days\")) {\n const date = new Date();\n let day = date.getDay();\n if (day === 0) { day = 7; }\n if (msg.payload.days.includes(day)) {\n return msg;\n } else {\n return null;\n }\n}\nreturn msg;","outputs":1,"noerr":0,"x":1350,"y":180,"wires":[["8eb795c0.24899"]]},{"id":"3bf2be21.9401aa","type":"function","z":"dc275690.07356","name":"validate input","func":"let errorMsg = \"\";\nif(!Array.isArray(msg.payload)) {\n errorMsg = \"msg.payload should be an array of schedule items\";\n node.warn(errorMsg)\n msg.payload = errorMsg;\n return [null, msg];\n}\nfor(i=0;i<msg.payload.length;i++){\n if(typeof msg.payload[i] !== \"object\") {\n errorMsg = \"each array item should be an object\";\n node.warn(errorMsg)\n msg.payload = errorMsg;\n return [null, msg];\n }\n if(!msg.payload[i].hasOwnProperty(\"item\") || !msg.payload[i].hasOwnProperty(\"command\") || !msg.payload[i].hasOwnProperty(\"time\")) {\n errorMsg = \"each array item should contain a item, a command and time property\";\n node.warn(errorMsg)\n msg.payload = errorMsg;\n return [null, msg];\n }\n if(typeof msg.payload[i].item !== \"string\") {\n errorMsg = \"the items in each schedule should be given as a string\";\n node.warn(errorMsg)\n msg.payload = errorMsg;\n return [null, msg];\n }\n if(typeof msg.payload[i].command !== \"string\" && typeof msg.payload[i].command !== \"number\" && typeof msg.payload[i].command !== \"boolean\") {\n errorMsg = \"the commands in each schedule should be given as a string or a number\";\n node.warn(errorMsg)\n msg.payload = errorMsg;\n return [null, msg];\n }\n if(!msg.payload[i].time.match(/[0-2]\\d\\:[0-5]\\d/g)) {\n errorMsg = \"the time should be in hh:mm 24 hour format\";\n node.warn(errorMsg)\n msg.payload = errorMsg;\n return [null, msg];\n }\n if(msg.payload[i].hasOwnProperty(\"days\")) {\n if(!Array.isArray(msg.payload[i].days)) {\n errorMsg = \"days should be given as an array of integers\";\n node.warn(errorMsg)\n msg.payload = errorMsg;\n return [null, msg];\n }\n for(let c=0; c<msg.payload[i].days.length; c++){\n if(typeof msg.payload[i].days[c] !== \"number\"){\n errorMsg = \"days should be given as integers of type number\";\n node.warn(errorMsg)\n msg.payload = errorMsg;\n return [null, msg];\n }\n if(msg.payload[i].days[c] < 1 || msg.payload[i].days[c] > 7){\n errorMsg = \"days should be in the range of 1-7\";\n node.warn(errorMsg)\n msg.payload = errorMsg;\n return [null, msg];\n }\n }\n }\n}\nreturn [msg, null];","outputs":2,"noerr":0,"x":400,"y":180,"wires":[["7f1ec532.7fc8bc"],["ead6c623.14ffd"]]},{"id":"ead6c623.14ffd","type":"trigger","z":"dc275690.07356","op1":"","op2":"1","op1type":"nul","op2type":"str","duration":"2","extend":false,"units":"s","reset":"","bytopic":"all","name":"","x":760,"y":300,"wires":[["a0ba0058.4cf528"]]}]
@lucasCFO
Copy link

Hey!
First of, thank you very much for your work! This was really missing, and I was searching for a similar node/flow for quite some time now!
Would it be possible to send the current state of all the items at the redeployment or restart of node-red? Maybe by using the persist node in your scheduler sub-flow? But I believe it would need some code modification?

@johanneskropf
Copy link
Author

@lucasCFO This would be complete rewrite. You can look at the functions in the subflow and you will see why. I would recommend building something like this outside the node with your own function node or subflow as this subflow has no concept of state or persistence but only knows events by design.

@ozdeadmeat
Copy link

With this scheduler, If I throw a bunch of tasks to the scheduler will it work through the task in order of transmission or will it process the next task in chronological order.

E.G.
[
{
"item": "Restart",
"command": "-Restart",
"time": "08:30"
},
{
"item": "Execute Update",
"command": "-update",
"time": "08:40"
}
]

So in this example, I would want the server to conduct a server restart then when the server comes back up my powershell script would re-schedule the tasks in that order. Would the Scheduler wait until 08:30 again the following day or would it detect that 08:40 is the next task and execute the -update command?

@johanneskropf
Copy link
Author

So in this example, I would want the server to conduct a server restart then when the server comes back up my powershell script would re-schedule the tasks in that order. Would the Scheduler wait until 08:30 again the following day or would it detect that 08:40 is the next task and execute the -update command?

If you set the schedule with an inject that is set to inject on deploy this inject will fire once nodered starts after a system reboot and the scheduler will than pick back up and execute the next schedule item once it's time. The subflow itself doesn't know order per se it just knows when to execute what once a schedule is set.
It actually doesn't matter in which order you put in a schedule as the node will sort the schedule from earliest to latest on input for a tidy debug message.
So the important part is to have the inject that sets the schedule set to inject once after deploy so that the scheduler picks back up after a restart of nodered.

@ozdeadmeat
Copy link

perfect, that is exactly what I was hoping it did. I have the configuration being set in a powershell script and the node-red flow gets its configuration from that file. Thanks for clarifying mate, this is exactly what I was looking for.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment