[Announce] Scheduler Subflow Node and Dashboard Ui Scheduler Flow (Updated)

Dashboard Ui Scheduler Flow

The simple input format makes it „easy“ to combine with the dashboard :see_no_evil: but fortunately i have done all the work already (needs node-red-ui-table):

[{"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"]]},{"id":"871730ad.fb9938","type":"ui_form","z":"909b8ee5.d1219","name":"","label":"add new item to schedule","group":"60c7705.396ef1","order":3,"width":0,"height":0,"options":[{"label":"Item","value":"item","type":"text","required":true,"rows":null},{"label":"Command","value":"command","type":"text","required":true,"rows":null},{"label":"Time","value":"time","type":"text","required":true,"rows":null},{"label":"Days","value":"days","type":"text","required":true,"rows":null}],"formValue":{"item":"","command":"","time":"","days":""},"payload":"","submit":"Add","cancel":"","topic":"","x":410,"y":1960,"wires":[["6a720e7a.6e2a4","81db4f5a.8a3928"]]},{"id":"6a720e7a.6e2a4","type":"change","z":"909b8ee5.d1219","name":"","rules":[{"t":"delete","p":"payload","pt":"msg"},{"t":"set","p":"payload.time","pt":"msg","to":"hh:mm","tot":"str"},{"t":"set","p":"payload.command","pt":"msg","to":"command","tot":"str"},{"t":"set","p":"payload.item","pt":"msg","to":"item","tot":"str"},{"t":"set","p":"payload.days","pt":"msg","to":"1,2,3,4,5,6,7","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":180,"y":1960,"wires":[["871730ad.fb9938"]]},{"id":"581b49e7.e6f32","type":"ui_table","z":"909b8ee5.d1219","group":"60c7705.396ef1","name":"","order":1,"width":"6","height":"4","columns":[],"outputs":1,"cts":true,"x":870,"y":1960,"wires":[["ceb6d7f2.a52738"]]},{"id":"81db4f5a.8a3928","type":"function","z":"909b8ee5.d1219","name":"add to schedule","func":"const ogSchedule = flow.get(\"schedule\") || [];\nlet schedule = JSON.parse(JSON.stringify(ogSchedule));\nlet command = msg.payload;\ncommand.item = command.item.trim();\ncommand.command = command.command.trim();\ncommand.time = command.time.trim();\ncommand.days = command.days.split(\",\");\ncommand.days = command.days.map(item => parseInt(item));\nif (command.command.match(/^[0-9]*\\.*[0-9]*$/g)) { command.command = Number(command.command); }\nif (command.command === \"true\") { command.command = true; }\nif (command.command === \"false\") { command.command = false; }\nlet newindex = null;\nif(schedule.length > 0){\n    for(i=0;i<schedule.length-1;i++){\n        if(command.time >= schedule[i].time && command.time < schedule[i+1].time){\n            newindex = i+1;\n        }\n    }\n    if(newindex !== null){\n        schedule.splice(newindex,0,command);\n    } else if (command.time < schedule[0].time){\n        schedule.splice(0,0,command);\n    } else {\n        schedule.push(command);\n    }\n} else {\n    schedule.push(command);\n}\nmsg.payload = schedule;\nlet 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}\nflow.set(\"schedule\", schedule);\nreturn [msg, null]; ","outputs":2,"noerr":0,"x":640,"y":1960,"wires":[["581b49e7.e6f32","94aee9bb.c90c5","5c8aa95c.467b18"],["e51e7f90.34a04"]]},{"id":"3630f498.db0044","type":"function","z":"909b8ee5.d1219","name":"remove from schedule","func":"if (msg.payload == \"Cancel\") return null;\nlet schedule = flow.get(\"schedule\");\nschedule.splice(msg.row, 1);\nflow.set(\"schedule\", schedule);\nmsg.payload = schedule;\nreturn msg;","outputs":1,"noerr":0,"x":1440,"y":1960,"wires":[["581b49e7.e6f32","5c8aa95c.467b18","94aee9bb.c90c5"]]},{"id":"ff571fa2.7edd18","type":"ui_toast","z":"909b8ee5.d1219","position":"dialog","displayTime":"3","highlight":"","sendall":true,"outputs":1,"ok":"OK","cancel":"Cancel","raw":false,"topic":"","name":"","x":1230,"y":1960,"wires":[["3630f498.db0044"]]},{"id":"ceb6d7f2.a52738","type":"change","z":"909b8ee5.d1219","name":"","rules":[{"t":"move","p":"payload","pt":"msg","to":"content","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"Delete this schedule?","tot":"str"},{"t":"set","p":"topic","pt":"msg","to":"Delete","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1040,"y":1960,"wires":[["ff571fa2.7edd18"]]},{"id":"5c8aa95c.467b18","type":"delay","z":"909b8ee5.d1219","name":"","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":870,"y":1900,"wires":[["23bf20db.964f98"]]},{"id":"980b8f97.d56ac","type":"ui_ui_control","z":"909b8ee5.d1219","name":"","events":"all","x":1240,"y":1900,"wires":[[]]},{"id":"23bf20db.964f98","type":"change","z":"909b8ee5.d1219","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"tab\":\"\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":1060,"y":1900,"wires":[["980b8f97.d56ac"]]},{"id":"80c05009.f852c8","type":"inject","z":"909b8ee5.d1219","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":170,"y":1900,"wires":[["6a720e7a.6e2a4"]]},{"id":"94aee9bb.c90c5","type":"subflow:dc275690.07356","z":"909b8ee5.d1219","name":"UiScheduler","env":[],"x":1150,"y":2020,"wires":[["84aef495.c47e5","c3ee2c7d.f6f47"],[]]},{"id":"1a559b1e.b6b25d","type":"inject","z":"909b8ee5.d1219","name":"","topic":"","payload":"schedule","payloadType":"flow","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":880,"y":2020,"wires":[["581b49e7.e6f32","94aee9bb.c90c5"]]},{"id":"3d22e53f.fbef12","type":"status","z":"909b8ee5.d1219","name":"","scope":["94aee9bb.c90c5"],"x":140,"y":2020,"wires":[["b4d08fa5.f43a98"]]},{"id":"b4d08fa5.f43a98","type":"ui_text","z":"909b8ee5.d1219","group":"60c7705.396ef1","order":2,"width":0,"height":0,"name":"","label":"next:","format":"{{msg.status.text}}","layout":"col-center","x":290,"y":2020,"wires":[]},{"id":"e51e7f90.34a04","type":"ui_toast","z":"909b8ee5.d1219","position":"dialog","displayTime":"3","highlight":"","sendall":true,"outputs":1,"ok":"OK","cancel":"","raw":false,"topic":"","name":"","x":630,"y":2020,"wires":[[]]},{"id":"c3ee2c7d.f6f47","type":"debug","z":"909b8ee5.d1219","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":1370,"y":2020,"wires":[]},{"id":"60c7705.396ef1","type":"ui_group","z":"","name":"Scheduler","tab":"b7237e1.8d991","order":1,"disp":false,"width":"6","collapse":false},{"id":"b7237e1.8d991","type":"ui_tab","z":"","name":"Scheduler","icon":"autorenew","order":2,"disabled":false,"hidden":false}]

This flow gives you this compact scheduler on the dashboard:

  • You can easily add objects to your schedule.
  • They get auto sorted by time of day for display in the table widget.
  • It shows you which object will be executed next and when.
  • It tries to validate your input
  • you can easily delete schedules by clicking on the entry in the table
  • if you enter a number (int or float) as a command in the ui it will be correctly parsed for the output as an actual js number type (same goes for booleans)

Have fun with it Johannes

8 Likes