Two subflows I made: a timerange switch and a scheduler / dashboard integratable

Here is a flow that shows an example how to integrate into a dashboard (ui-table needs to be installed):

[{"id":"7cc13ada.b6712c","type":"subflow","name":"timerange","info":"Lets through or blocks a payload\nbased on a time range. This can\neither be configured through the\nenviroment variables in the node ui\nor as described below with a message\nthat has an override topic.\nIf in range the msg will be passed\nto the first output and otherwise\nto the second.\nThe start and stop time needs\nto be defined in an hh:mm format.\nThere is also a week array. The week\nstarts on monday so 4 for example is\nThursday. Payload will only be passed\non days that are in the array.\nOut of time range payloads will\nbe redirected to the second output.\nThe schedule can be overriden by injecting\na message with the topic of \"override\"\nthat contains a ```msg.payload``` object with the\nkeys of \"start\",\"stop\",\"days\" like\nthis:\n```\n{\n    \"start\": \"10:00\",\n    \"stop\": \"14:00\",\n    \"days\": [\n        1,\n        2,\n        3,\n        4,\n        5,\n        6,\n        7\n    ]\n}\n```\nStart and stop need to be strings in the hh:mm\nformat and days an array of numbers as\ndescribed above.\nThe override can be deleted by injecting a\nmsg.payload string \"reset\".","category":"","in":[{"x":100,"y":100,"wires":[{"id":"da7f7d3f.da5af"}]}],"out":[{"x":620,"y":60,"wires":[{"id":"694ce0e1.4bee58","port":0}]},{"x":620,"y":140,"wires":[{"id":"694ce0e1.4bee58","port":1}]}],"env":[{"name":"start","type":"str","value":"00:00","ui":{"icon":"font-awesome/fa-arrow-right","label":{"en-US":"from hh:mm"},"type":"input","opts":{"types":["str"]}}},{"name":"stop","type":"str","value":"00:00","ui":{"icon":"font-awesome/fa-circle","label":{"en-US":"until hh:mm"},"type":"input","opts":{"types":["str"]}}},{"name":"days","type":"json","value":"[1,2,3,4,5,6,7]","ui":{"icon":"font-awesome/fa-calendar","label":{"en-US":"days"},"type":"input","opts":{"types":["json"]}}}],"color":"#C7E9C0","inputLabels":["payload input"],"outputLabels":["in time range","out of time range"],"icon":"node-red/switch.svg","status":{"x":480,"y":200,"wires":[{"id":"1bc56c04.ca0a3c","port":0}]}},{"id":"694ce0e1.4bee58","type":"function","z":"7cc13ada.b6712c","name":"is in range?","func":"const schedule = flow.get(\"schedule\");\nlet start = env.get(\"start\");\nlet stop = env.get(\"stop\");\nlet days = env.get(\"days\");\nif(schedule !== undefined){\n    start = schedule.start;\n    stop = schedule.stop;\n    days = schedule.days;\n}\nconst time = new Date();\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;\nconst hmtime = hour + \":\" + minute;\nif(days.includes(day)){\n    if(start == stop){\n        return [msg, null];\n    } else if(start > stop){\n        if(hmtime >= start || hmtime < stop){\n            return [msg, null];\n        } else {\n            return [null, msg];\n        }\n    } else if(hmtime >= start && hmtime < stop){\n        return [msg, null];\n    } else {\n        return [null, msg];\n    }\n} else {\n    return null;\n}","outputs":2,"noerr":0,"x":450,"y":100,"wires":[[],[]]},{"id":"94dc9c00.0f576","type":"inject","z":"7cc13ada.b6712c","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":170,"y":200,"wires":[["1bc56c04.ca0a3c"]]},{"id":"1bc56c04.ca0a3c","type":"function","z":"7cc13ada.b6712c","name":"display rule","func":"const schedule = flow.get(\"schedule\");\nif(typeof schedule == \"object\"){\n    const start = schedule.start;\n    const stop = schedule.stop;\n    const days = String(schedule.days);\n    msg.payload = \"override: \" + start + \"-\" + stop + \"/\" + days;\n} else {\n    const start = env.get(\"start\");\n    const stop = env.get(\"stop\");\n    const days = String(env.get(\"days\"));\n    const override = false;\n    msg.payload = start + \"-\" + stop + \"/\" + days;\n}\nreturn msg;","outputs":1,"noerr":0,"x":350,"y":200,"wires":[[]]},{"id":"da7f7d3f.da5af","type":"function","z":"7cc13ada.b6712c","name":"check for override","func":"if(msg.topic == \"override\"){\n    flow.set(\"schedule\",msg.payload);\n    return [null, msg];\n} else if (msg.payload == \"reset\"){\n    let reset;\n    flow.set(\"schedule\",reset);\n    return [null, msg];\n} else {\n    return [msg, null];\n}","outputs":2,"noerr":0,"x":250,"y":100,"wires":[["694ce0e1.4bee58"],["1bc56c04.ca0a3c"]]},{"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```\npassing an empty array as a payload will\nreset the node.\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 oject 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```.","category":"","in":[{"x":80,"y":180,"wires":[{"id":"be194cd9.78b218"}]}],"out":[{"x":1300,"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":1180,"y":260,"wires":[{"id":"a0ba0058.4cf528","port":0}]}},{"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":790,"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();\nlet hour = String(time.getHours());\nlet minute = String(time.getMinutes());\nif(hour.length == 1) hour = \"0\" + hour;\nif(minute.length == 1) minute = \"0\" + minute;\nconst hmtime = hour + \":\" + minute;\nlet nextindex = 0;\nfor(i=0;i<schedule.length;i++){\n    if(hmtime < schedule[i].time){\n        nextindex = i;\n        break;\n    } else {\n        continue;\n    }\n}\nmsg.payload = schedule[nextindex].item + \",\" + schedule[nextindex].command + \",\" + schedule[nextindex].time;\nreturn msg;","outputs":1,"noerr":0,"x":1000,"y":260,"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":470,"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":270,"y":260,"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":230,"y":180,"wires":[["6680ab33.f83b5c"],["7f1ec532.7fc8bc"]]},{"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":1140,"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":970,"y":180,"wires":[["7e108ed9.acbd88"],["8eb795c0.24899","a0ba0058.4cf528","280402d5.27b2f6"]]},{"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":420,"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":600,"y":220,"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":1150,"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":780,"y":120,"wires":[["f476595.aa436a8"]]},{"id":"871730ad.fb9938","type":"ui_form","z":"909b8ee5.d1219","name":"","label":"add item to schedule","group":"60c7705.396ef1","order":1,"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}],"formValue":{"item":"","command":"","time":""},"payload":"","submit":"Add","cancel":"","topic":"","x":400,"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":"enter a time in the hh:mm format","tot":"str"},{"t":"set","p":"payload.command","pt":"msg","to":"enter a command","tot":"str"},{"t":"set","p":"payload.item","pt":"msg","to":"enter the item to switch","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":2,"width":"6","height":"6","columns":[],"outputs":1,"cts":true,"x":790,"y":1960,"wires":[["ceb6d7f2.a52738"]]},{"id":"81db4f5a.8a3928","type":"function","z":"909b8ee5.d1219","name":"add to schedule","func":"let schedule = flow.get(\"schedule\") || [];\nlet command = msg.payload;\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}\nflow.set(\"schedule\", schedule);\nmsg.payload = schedule;\nreturn msg;","outputs":1,"noerr":0,"x":620,"y":1960,"wires":[["581b49e7.e6f32","5c8aa95c.467b18","94aee9bb.c90c5"]]},{"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":1380,"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":1170,"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":980,"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":810,"y":1900,"wires":[["23bf20db.964f98"]]},{"id":"980b8f97.d56ac","type":"ui_ui_control","z":"909b8ee5.d1219","name":"","events":"all","x":1180,"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":1000,"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":"f9a69a7f.a1dc58","type":"inject","z":"909b8ee5.d1219","name":"","topic":"","payload":"schedule","payloadType":"flow","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":780,"y":2100,"wires":[["94aee9bb.c90c5"]]},{"id":"94aee9bb.c90c5","type":"subflow:dc275690.07356","z":"909b8ee5.d1219","name":"","env":[],"x":980,"y":2100,"wires":[["9b6a17c5.8a7ba8"],[]]},{"id":"dbbba4d6.d8ce7","type":"function","z":"909b8ee5.d1219","name":"format","func":"const days = msg.payload.days.split(\",\").map(item=>parseInt(item));\nconst schedule = {\n    start: \"00:00\",\n    stop: \"00:00\",\n    days: days\n}\nmsg.topic = \"override\";\nmsg.payload = schedule;\nreturn msg;","outputs":1,"noerr":0,"x":790,"y":2200,"wires":[["9b6a17c5.8a7ba8"]]},{"id":"7344979a.6fe778","type":"ui_form","z":"909b8ee5.d1219","name":"","label":"change active days","group":"60c7705.396ef1","order":4,"width":0,"height":0,"options":[{"label":"Weekdays","value":"days","type":"text","required":true,"rows":null}],"formValue":{"days":""},"payload":"","submit":"change","cancel":"","topic":"","x":570,"y":2200,"wires":[["dbbba4d6.d8ce7","92c6a919.059158","8518b153.c1f3d"]]},{"id":"9b6a17c5.8a7ba8","type":"subflow:7cc13ada.b6712c","z":"909b8ee5.d1219","name":"","x":1180,"y":2100,"wires":[["84aef495.c47e5","4f89467b.9f7d48"],[]]},{"id":"1056323f.824e8e","type":"ui_text","z":"909b8ee5.d1219","group":"60c7705.396ef1","order":3,"width":0,"height":0,"name":"","label":"active on:","format":"{{msg.payload.days}}","layout":"row-spread","x":1000,"y":2260,"wires":[]},{"id":"92c6a919.059158","type":"change","z":"909b8ee5.d1219","name":"","rules":[{"t":"delete","p":"payload","pt":"msg"},{"t":"set","p":"payload.days","pt":"msg","to":"comma separated day numbers","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":360,"y":2200,"wires":[["7344979a.6fe778"]]},{"id":"63e4763e.475348","type":"inject","z":"909b8ee5.d1219","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":170,"y":2200,"wires":[["92c6a919.059158"]]},{"id":"8518b153.c1f3d","type":"change","z":"909b8ee5.d1219","name":"","rules":[{"t":"change","p":"payload.days","pt":"msg","from":"1","fromt":"str","to":"Mo","tot":"str"},{"t":"change","p":"payload.days","pt":"msg","from":"2","fromt":"str","to":"Tu","tot":"str"},{"t":"change","p":"payload.days","pt":"msg","from":"3","fromt":"str","to":"We","tot":"str"},{"t":"change","p":"payload.days","pt":"msg","from":"4","fromt":"str","to":"Th","tot":"str"},{"t":"change","p":"payload.days","pt":"msg","from":"5","fromt":"str","to":"Fr","tot":"str"},{"t":"change","p":"payload.days","pt":"msg","from":"6","fromt":"str","to":"Sa","tot":"str"},{"t":"change","p":"payload.days","pt":"msg","from":"7","fromt":"str","to":"Su","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":820,"y":2260,"wires":[["1056323f.824e8e"]]},{"id":"4f89467b.9f7d48","type":"debug","z":"909b8ee5.d1219","name":"Output","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":1430,"y":2100,"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}]


You can add schedule items from the form and delete items by clicking on the table entry. You can change on which days of the week the schedule is active. The table items are always chronologically sorted no matter what order you add them.

Johannes

2 Likes