UPDATE: V1.2.1 node-red-contrib-cron-plus scheduler (incl solar events and Timezone support)

So I had a bit play...

...And it looks fine :frowning:

A bit more play, I set time to 10:12 EDT...
image

...and it worked as expected...
image

So I'm gonna have to run this up on a Linux VM and do more testing however, I have no access to Apple products.

I'll let you know what i find.

Indeed, it's all a bit of a mess - Falsehoods programmers believe about time: @noahsussman: Infinite Undo :roll_eyes:

See also various other lists of "falsehoods" here - GitHub - kdeldycke/awesome-falsehood: 😱 Falsehoods Programmers Believe in :astonished: In fact, it's probably a good idea to never take anything for granted, even 2 + 2 will always be 4...

1 Like

Great scheduler, very powerful indeed
However, in my system where this could have been a candidate, I need various sunrise/sunset triggers as well. I am sure I could solve that need either pausing/starting or injecting/removing schedules at the right time using some other nodes providing sunrise/sunset info but it would really not simplify the logic in my flows

So for my flows I will continue the use of the BigTimer. I could see a blended mixture of both would be the ultimate

I will admit that I'd also like to see some "astronomical times" included. But I recognise that to do well does add some considerable complexity. After all, you do need to add/remove offsets to astronomical times in order for them to meet your needs.

But for the majority of uses, I'll be slowly switching to this node.

Yes, agreed, there has to be a reasonable limit. I do not want to clutter this thread but below is an example of my current configuration showing how our "simple" outdoor facade lamps are controlled due to various day types, months, alarm system state and other scenarios setting the actual schedules to be used (whats wrong with old fashioned simple on/off wall switches and some extra excercis???)

Actually, I think it is possible to pre-calculate the "astronomical times" including other criterias and then populate with the necessary schedules daily. In such way I would only have to use one single CRON+ node

Just wondering if something like this would be an easier interface for people to use within the node. It would eliminate the "need" for all the explanations

1 Like

When adding a new schedule via msg.payload -> input the newly added schedule is not listed in the node configuration (but it does work). It is therefore impossible to see all schedules added to the node

Partly true.

You can query the command status-all to get a list. This is demonstrated in the demo "Dynamic cron schedules / timers via dashboard control" I posted before.

@TotallyInformation suggested there was a way to communicate back to design time to show dynamically created cron entries but I have yet to begin looking into it (its in my brains TODO list)

Damm I keep misplacing my brain todo list...

1 Like

Steve, that's great, I was thinking visibility. Anyway, it works fine, I have tested a bit and I'm now confident about how to compile a schedule in a function node (based on the many conditions I have) and to push that to your CRON+ node

1 Like

He did :wink:

In the node's html file (which runs in the browser), you need to have a query that asks your node's js file for the data that has come in via msg. That can be in the form of an API call which are really easy to do - uibuilder has some examples at the end of the js file.

You will need to decide how to retain the data in the js file. You need an array or object that is created for each instance. This is easy enough, you just need to define that within the function that is specified in the RED.nodes.registerType(<modulename>, <function>) call. I always call my function nodeGo. Since that function is called whenever a new instance of the node is deployed, the data in it stays around unless you restart NR or redeploy the node. You can then add/remove entries to that variable when a new msg comes in.

Also, if you wanted to, you could make sent jobs retained as well by simply saving a copy of that variable to a JSON file after every update and pulling it back from file on startup.

I don't think that you can fully merge the incoming requests with the configuration data since I think that would result in the node instance being marked as changed and require redeploy. Though you may well think of other strategies that might work.

1 Like

So been playing a bit with this great node. In the following example I wanted to create a scheduler that should

  • send out "1" while sun is down
  • send out "0" while sun is up
  • having an offset of +/- 1 hour for both sunrise and sunset
  • automatically update schedules since they will change every day

Doesn't sound as a very strange need? Especially if you for instance would like to automatically control outdoor lights (or anything else dependent on the sun state)

Well, how to solve this with the CRON+ node???

Below is one way. The code in the function node is calculating the sunrise/sunset times based on the injected location. Then it configures the necessary cron expressions (7 in total needed) and injects those to the CRON+ node. In this example this happens every minute in the function node but it could very well be done just once per day (hence the RBE node in the flow). The rest of the code is just to create a status text showing the current sunrise/sunset times

So if anyone would ever need such a scheduler, the flow is below

I can't avoid comparing this with what I'm currently using, the BigTimer node. Would the CRON+ simplify things? Well, one thing, you can reduce the number of nodes in the flow but with the cost of more complex code in function nodes. For more advanced needs, the complexity will be rather high. If we look at the code that is needed to define the cron expressions in this example it is rather obvious that you have to be extremely careful when editing those on top of having a very good understanding in how cron syntax works

EDIT: Nobody is perfect, found some bugs in my expressions. The problem appeared when adjusting the minutes, had to include a check that they did not "fall outside" of the allowed range 0-59. Updated expressions and flow below, this looks as if it is now working fine

let srh = times.sunrise.getHours();
let srm = times.sunrise.getMinutes();
let ssh = times.sunset.getHours();
let ssm = times.sunset.getMinutes();

let expr1 = '*/1 0-'+srh.toString()+' * * *';
let expr2 = '0-'+srm.toString()+'/1 '+(srh+1).toString()+' * * *';
let expr3 = '*/1 '+ssh.toString()+'-23'+' * * *';
let expr4 = ssm.toString()+'-59/1 '+(ssh-1).toString()+' * * *';

let expr5 = '*/1 '+(srh+2).toString()+'-'+(ssh-2).toString()+' * * *';
let expr6 = '';
if(srm<59){
    expr6 = (srm+1).toString()+'-59/1 '+(srh+1).toString()+' * * *';
}
else{
    expr6 = '0';
}
let expr7 = '';
if(ssm>0){
    expr7 = '0-'+(ssm-1).toString()+'/1 '+(ssh-1).toString()+' * * *';
}
else{
    expr7 = '0';
}

Will CRON+ help in improving visibility, what is happening and when? Partly I think. It's great that the next action is presented in the status text. It is also great with the "automated" descriptions like description: "Every 1 minutes, minutes 0 through 5 past the hour, between 08:00 AM and 08:59 AM"

In BigTimer the visibility is of course already very clear by default, just open the config dialog and you have it all in front of you

[{"id":"4d2c5558.05101c","type":"cronplus","z":"60f50465.20a53c","name":"","outputField":"payload","timeZone":"","options":[],"x":590,"y":1890,"wires":[["16db2d1.ccc4cd3"]]},{"id":"1f535f29.5da5d1","type":"inject","z":"60f50465.20a53c","name":"remove-all","topic":"","payload":"{\"command\":\"remove-all\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":310,"y":1860,"wires":[["4d2c5558.05101c"]]},{"id":"16db2d1.ccc4cd3","type":"debug","z":"60f50465.20a53c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":760,"y":1890,"wires":[]},{"id":"a8262ceb.6743f","type":"inject","z":"60f50465.20a53c","name":"status-all","topic":"","payload":"{\"command\":\"status-all\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":310,"y":1910,"wires":[["4d2c5558.05101c"]]},{"id":"cbe17617.b6c9a8","type":"function","z":"60f50465.20a53c","name":"SunRiseSet","func":"var SunCalc = global.get('SunCalc');\nvar loc = msg.payload.split(';');\nvar times = SunCalc.getTimes(new Date(), loc[0], loc[1]);\nvar sunriseStr = [times.sunrise.getHours().toString(), times.sunrise.getMinutes().toString()];\nvar sunsetStr = [times.sunset.getHours().toString(), times.sunset.getMinutes().toString()];\n\nfunction updateNodeStatus(txt) {\n    node.status({\n    \ttext : txt\n    });\n}\n\nif (sunsetStr[0].length<2){\n    sunsetStr[0]='0'+sunsetStr[0];\n}\nif (sunsetStr[1].length<2){\n    sunsetStr[1]='0'+sunsetStr[1];\n}\nvar sunset = sunsetStr[0]+':'+sunsetStr[1];\n\nif (sunriseStr[0].length<2){\n    sunriseStr[0]='0'+sunriseStr[0];\n}\nif (sunriseStr[1].length<2){\n    sunriseStr[1]='0'+sunriseStr[1];\n}\nvar sunrise = sunriseStr[0]+':'+sunriseStr[1];\n\nlet info = 'sunUp:'+sunrise+' '+'sunDown:'+sunset;\nupdateNodeStatus(info);\n\nlet srh = times.sunrise.getHours();\nlet srm = times.sunrise.getMinutes();\nlet ssh = times.sunset.getHours();\nlet ssm = times.sunset.getMinutes();\n\nlet expr1 = '*/1 0-'+srh.toString()+' * * *';\nlet expr2 = '0-'+srm.toString()+'/1 '+(srh+1).toString()+' * * *';\nlet expr3 = '*/1 '+ssh.toString()+'-23'+' * * *';\nlet expr4 = ssm.toString()+'-59/1 '+(ssh-1).toString()+' * * *';\n\nlet expr5 = '*/1 '+(srh+2).toString()+'-'+(ssh-2).toString()+' * * *';\nlet expr6 = '';\nif(srm<59){\n    expr6 = (srm+1).toString()+'-59/1 '+(srh+1).toString()+' * * *';\n}\nelse{\n    expr6 = '0';\n}\nlet expr7 = '';\nif(ssm>0){\n    expr7 = '0-'+(ssm-1).toString()+'/1 '+(ssh-1).toString()+' * * *';\n}\nelse{\n    expr7 = '0';\n}\n\nmsg.payload = [\n    {\n        'command': 'add',\n        'name': 'one',\n        'expression': expr1,\n        'payload': '1',\n        'type': 'num',\n    },\n    {\n        'command': 'add',\n        'name': 'two',\n        'expression': expr2,\n        'payload': '1',\n        'type': 'num',\n    },\n    {\n        'command': 'add',\n        'name': 'three',\n        'expression': expr3,\n        'payload': '1',\n        'type': 'num',\n    },\n    {\n        'command': 'add',\n        'name': 'four',\n        'expression': expr4,\n        'payload': '1',\n        'type': 'num',\n    },\n    {\n        'command': 'add',\n        'name': 'five',\n        'expression': expr5,\n        'payload': '0',\n        'type': 'num',\n   },\n    {\n        'command': 'add',\n        'name': 'six',\n        'expression': expr6,\n        'payload': '0',\n        'type': 'num',\n    },\n    {\n        'command': 'add',\n        'name': 'seven',\n        'expression': expr7,\n        'payload': '0',\n        'type': 'num',\n    }\n]\n\nreturn msg;\n","outputs":"1","noerr":0,"x":600,"y":2080,"wires":[["38f04baa.dbf894"]]},{"id":"3ded6583.1a2eaa","type":"inject","z":"60f50465.20a53c","name":"","topic":"","payload":"59.4200;18.0000","payloadType":"str","repeat":"60","crontab":"","once":true,"onceDelay":"","x":340,"y":2080,"wires":[["cbe17617.b6c9a8"]]},{"id":"e9e43b93.b18398","type":"debug","z":"60f50465.20a53c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":760,"y":1990,"wires":[]},{"id":"38f04baa.dbf894","type":"rbe","z":"60f50465.20a53c","name":"","func":"rbe","gap":"","start":"","inout":"out","property":"payload","x":440,"y":1990,"wires":[["4d2c5558.05101c","e9e43b93.b18398"]]}]
2 Likes

You can query the command status-all

Would it possible to include the timezone in the output as well ?

Additional "issue"

If you inject {"command":"status-all"} - this produces msg.payload with an object, which means you can not distinguish between a payload value and the status.

Let say I connect the cron node to a light, the light expects On or Off, cron node outputs payload:true/false, inject the status-all and I will get an error from the light.

And in the config part of the status-all output, it would be nice to see where it outputs the payload to (ie. if something set to something other than payload, you won't know)

eg:

{"name":"Off","expression":"0 23 * * *","payload":{"key":"schedule", "value":true},"type":"bool","limit":null}

A Cron event has an additional property called (off the top of my head) timestamp. This is not present in a command response.

Not ideal but works 100% of the time.

Hi thanks for you kind words and ideas. I already have code to generate sunrise / sunset times based on lon/lat but yet to figure out how it fits in nicely in the multiple schedule list.

One thought is to add a drop-down (per row) to specify if a entry is ...

  • Cron expression
  • Sunrise
  • Sunset
    When Sunrise/Sunset is selected, instead of entering an expression you enter Lon/lat

Thoughts?

As for visibility of dynamically created schedules, I am thinking of showing them read-only in the list?

Not ideal but works 100% of the time.

Consider a second output for status only output ?

I did consider it at the time but forget now why I didn't - I think a second output can be added (dynamically / as an option) right? (I.e. maintain compatibility with existing flows but permit new flows to chose to send status results out of 2nd output)

1 Like

0 6 * * Tue expr \( `date +\%s` / 604800 + 1 \) \% 2

The above expression would fire a *nix cron every second Tuesday in odd weeks (+0 for even week). Unfortunately this will not work in the node due to the expression part.
Is there an option to make this work?

Expressions ar not evaluated. I haven't given it any thought.

Is this a real requirement or academic?

I'm certain with a mix of the dynamic nature of this node and perhaps a little bit of boiler plate you could achieve this.

In other words, do you think the effort would be worth the benefits if I was to support expressions?