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

Hello,
In a recent thread (Set time function on dashboard) I was involved in a discussion about scheduling things from the ui and before this in a thread about time ranges. As much out of pure curiosity and mental exercise as also personal need I created two little subflows which both only use plain javascript and core nodes.
First the a timerange switch which does what it says, routing the payload based on time of day and day of week:
Screenshot 2020-04-10 at 13.58.49


just the subflow:

[{"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"]]}]

the example flow:

[{"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":"dee7e75c.8a26b8","type":"subflow:7cc13ada.b6712c","z":"58cf3979.df1d7","name":"","env":[],"x":420,"y":1360,"wires":[["bbd5e002.081ca8"],["683015fa.21207c"]]},{"id":"eff3b51e.d8d6c","type":"inject","z":"58cf3979.df1d7","name":"example override schedule","topic":"override","payload":"{\"start\":\"10:00\",\"stop\":\"12:00\",\"days\":[1,2,3,4,5]}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":190,"y":1300,"wires":[["dee7e75c.8a26b8"]]},{"id":"bbd5e002.081ca8","type":"debug","z":"58cf3979.df1d7","name":"In Range","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":620,"y":1320,"wires":[]},{"id":"683015fa.21207c","type":"debug","z":"58cf3979.df1d7","name":"Out of Range","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":630,"y":1400,"wires":[]},{"id":"a9b2a0ab.f2174","type":"inject","z":"58cf3979.df1d7","name":"test it","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":250,"y":1360,"wires":[["dee7e75c.8a26b8"]]},{"id":"71dbe726.6142b8","type":"inject","z":"58cf3979.df1d7","name":"","topic":"","payload":"reset","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":250,"y":1420,"wires":[["dee7e75c.8a26b8"]]}]

Second the scheduler which can send as many different payloads at specified times as you want based on a list of events in the input schedule:
Screenshot 2020-04-10 at 14.05.37


just the subflow:

[{"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"]]}]

the example flow:

[{"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":"9efa4c09.d71258","type":"subflow:dc275690.07356","z":"58cf3979.df1d7","name":"","x":420,"y":1660,"wires":[["da1e5c0c.e41a68"],["134699f3.4f471e"]]},{"id":"30924aea.0d164e","type":"inject","z":"58cf3979.df1d7","name":"example schedule","topic":"","payload":"[{\"item\":\"test_item_1\",\"command\":\"ON\",\"time\":\"06:00\"},{\"item\":\"test_item_2\",\"command\":\"ON\",\"time\":\"08:30\"},{\"item\":\"test_item_1\",\"command\":\"OFF\",\"time\":\"06:45\"}]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":230,"y":1600,"wires":[["9efa4c09.d71258"]]},{"id":"b0183f13.749bb8","type":"inject","z":"58cf3979.df1d7","name":"","topic":"","payload":"debug","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":270,"y":1660,"wires":[["9efa4c09.d71258"]]},{"id":"bba48799.4e2358","type":"inject","z":"58cf3979.df1d7","name":"reset","topic":"","payload":"[]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":270,"y":1720,"wires":[["9efa4c09.d71258"]]},{"id":"da1e5c0c.e41a68","type":"debug","z":"58cf3979.df1d7","name":"scheduled_payload","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":610,"y":1620,"wires":[]},{"id":"134699f3.4f471e","type":"debug","z":"58cf3979.df1d7","name":"schedule_debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":600,"y":1700,"wires":[]}]

I have been using https://flows.nodered.org/node/node-red-contrib-time-range-switch and https://flows.nodered.org/node/node-red-contrib-timerswitch for those tasks but as much as I loved the simplicity something was missing and I didn't need some other things.
Im fully aware of such great nodes as https://flows.nodered.org/node/node-red-contrib-cron-plus and https://flows.nodered.org/node/node-red-contrib-bigtimer and im reinventing the weel a little bit :see_no_evil:
The timerswitch was missing the option to override from a message as schedules could only be set from the ui and on the timerange switch I was missing the days to be an option.
Those subflows were written to be simple and alow easy setting of the range/schedule through payload objects. This also makes them very easy to use together with the dashboard or reading a schedule from a database or file.
The timerange switched is based on the javascript dateobject properties and the scheduler uses setTimeout().
The info tab of each subflow gives information about how it works and how the message objects have to be formatted for the node.
Maybe somebody finds them useful.
Tell me what you think of them or if you have criticism, bugs or enhancements.

Stay healthy and have fun, Johannes

Edit had the wrong screenshots first

1 Like

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

small update to the time range switch. It now shows weekday abbreviations in the info instead of numbers

[{"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).replace(/1/g,\"Mo\").replace(/2/g,\"Tu\").replace(/3/g,\"We\").replace(/4/g,\"Th\").replace(/5/g,\"Fr\").replace(/6/g,\"Sa\").replace(/7/g,\"Su\");\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\")).replace(/1/g,\"Mo\").replace(/2/g,\"Tu\").replace(/3/g,\"We\").replace(/4/g,\"Th\").replace(/5/g,\"Fr\").replace(/6/g,\"Sa\").replace(/7/g,\"Su\");\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"]]}]

image

2 Likes

Feel free to add these to the flows.nodered.org site... look very useful thanks.

1 Like

Especially the purple :star_struck:

Done:
https://flows.nodered.org/flow/fce3ff5ca0df010ab2d4e692f2e50b4e
and https://flows.nodered.org/flow/f6ddf18d9c0b36a5f0131189af22f928

3 Likes

This topic was automatically closed after 60 days. New replies are no longer allowed.