Central Heating/Home Control schedule. Suggestions please

Thanks Paul, much appreciated. I see the flow, looks good, and will analyse tomorrow to try to completely understand.

I am looking at options at the moment. I have come to an impasse. I am not averse to using a .csv file, just the modification of 24*7 times every time I want to make a small change, hence the 'drive' towards an event driven schedule. The flow looks good and the addition of the File in the format you have shown makes it even easier rather than passing the Dynamic Schedules from a function node!

As I say, it is the defining of the desired temperature on a redeploy or resetting of the computer as the flow restarts which is stumping me at the moment.

As far as I can see (or understand) cronplus will only set the data when the allotted time is reached, it is the passing of the data between cron times that I am trying to understand.

Why not save the current state in a file context (after any change in state) and reload on startup with an inject node? I have possibly missed where you have explained why this does not work but it is a simple way of keeping track.

1 Like

Thank you both! I am going to have a play because the combination is almost what I am looking for.

The last scenario I envisage is after, say a Power Cut, where it things may be off for, say, several hours. There could have been a change in 'requirements' during that time, so it may be that I need to read the Dynamic Schedule on Power Up.

Thinking out loud, Paul's file format makes things easier, I could grab the cron time and create a table of time and Desired Temp and read from that, but then, why use cronplus, I could just read the table events and compare with current time, which means the current Desired data is available all of the time..

At the end of the day, I feel that I have again been trying to make things way more complicated than they need to be!!

Thank you both for your time, effort and ideas. Such a good and helpful Community reside here!!

May take a few days, but I will post a flow here.

An alternative is to do what (if I remember correctly) bigtimer can do, which is to repeat an output every minute, so that on restart the latest state is updated within 1 minute. Repeating every minute between times can be done in the schedule if I remember correctly, though I would have to remind myself exactly how.

Thank you Colin.

Yes, I have seen that, but rightly or wrongly, I prefer to try and write my own code using the Basic Node-RED nodes. Then if a Node is no longer supported, I don't end up having to write it later. It also means I keep my brain active, I would say learn, but come tomorrow, I may have forgotten it. Retention is becoming more of a problem!!:rofl:

1 Like

OK, I thought at the start you said you wanted to use cronplus but didn't know how to get round this issue.

By the way, I have found the ramp thermostat node absolutely solid. I don't use the t/stat, just the ramping feature.

I came to cronplus to replace ramp-thermostat to make things event driven, but saw the reset/restart 'problems' that would affect this particular application. Glad ramp-thermostat is solid, probably me, I just needed the time feature and had to fudge the ramp by putting an added time to ramp over 1 minute. I will return to that after I get this routine going.

Thhis routine reads in the file in the format Paul suggested. I can get the desired times and temperatures into the relevant locations, I am just trying to get the event time < time now < event+1 time match to the correct temperatures trapping the first and last events (00:00, 23:59). I am happy plodding away doing this, learnt some more JS in the process! Basically, just a glutton for punishment.

I am using cron plus since long. I am loading my schedules from csv files.
one is attached fyi.

my flow looks like this

[{"id":"70489757.9ad0d8","type":"cronplus","z":"4e969c7c.f77dc4","name":"Heating","outputField":"payload","timeZone":"","persistDynamic":true,"commandResponseMsgOutput":"fanOut","outputs":2,"options":[],"x":260,"y":800,"wires":[["e381ec2d.78eb2","31a8f2ab7e3d71ec"],["4fec987f.72b318"]]},{"id":"5355c793.e8c0b8","type":"ui_template","z":"4e969c7c.f77dc4","group":"8784088a.4ec318","name":"","order":8,"width":16,"height":22,"format":"<style type=\"text/css\">\n.t1  {border-collapse:collapse;border-spacing:0;border-color:#ccc;}\n.t1 td{font-family:Arial, sans-serif;font-size:14px;padding:8px 3px;border-style:solid;border-width:0px;overflow:hidden;word-break:normal;border-top-width:1px;border-bottom-width:1px;border-color:#ccc;color:#333;background-color:#fff;}\n.t1 th{font-family:Arial, sans-serif;font-size:14px;font-weight:normal;padding:10px 5px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;border-color:#ccc;color:#333;background-color:#f0f0f0;}\n.t1 .t1-36r9{background-color:#000000;color:#ffffff;text-align:left;vertical-align:top}\n.t1 .t1-x2zo{background-color:#c0c0c0;font-size:small;text-align:left;vertical-align:top}\n.t1 .t1-o73e{background-color:#c0c0c0;font-size:small;text-align:center;vertical-align:top}\n.t1 .t1-5qt9{font-size:small;text-align:left;vertical-align:top}\n.t1 .t1-5qta{font-size:small;text-align:center;vertical-align:middle}\n</style>\n<table class=\"t1\">\n  <tr>\n    <th class=\"t1-36r9\" colspan=\"9\">CRON Schedules</th>\n  </tr>\n  <tr>\n    <td class=\"t1-x2zo\">Name</td>\n    <td class=\"t1-x2zo\">Expression</td>\n    <td class=\"t1-x2zo\">Payload</td>\n    <td class=\"t1-x2zo\">Description</td>\n    <td class=\"t1-x2zo\">Next run</td>\n    <td class=\"t1-x2zo\">State</td>\n    <!-- <td class=\"t1-o73e\">Delete</td>\n    <td class=\"t1-o73e\">Pause</td>\n    <td class=\"t1-o73e\">Resume</td> -->\n  </tr>\n  <tr  ng-repeat=\"item in msg.payload\">\n    <td class=\"t1-5qt9\">{{item.config.name}}</td>\n    <td class=\"t1-5qt9\" style=\"white-space: nowrap\" nowrap>{{item.config.expression}}</td>\n    <td class=\"t1-5qt9\">{{item.config.payload}}</td>\n    <td class=\"t1-5qt9\">{{item.status.description}}</td>\n    <td class=\"t1-5qt9\">{{item.status.nextDescription}}</td>\n    <td class=\"t1-5qta\"><i class=\"fa fa-{{item.status.isRunning ? 'play' : 'pause'}}\"> </i></td>\n\n   <!-- <td class=\"t1-5qta\">\n       <md-button class=\"md-raised\"\n        ng-click=\"send([[{payload:{command:'remove', name: item.config.name}}, {payload:{command:'list-all'}}]]);\"> <i class=\"fa fa-trash\"> </i></md-button>\n   </td>\n   <td class=\"t1-5qta\">\n       <md-button class=\"button\" \n       ng-click=\"send([[{payload:{command:'pause', name: item.config.name}}, {payload:{command:'list-all'}}]]);\"> <i class=\"fa fa-pause\"> </i></md-button>\n   </td>\n   <td class=\"t1-5qta\">\n       <md-button class=\"button\" \n       ng-click=\"send([[{payload:{command:'start', name: item.config.name}}, {payload:{command:'list-all'}}]]);\"> <i class=\"fa fa-play\"> </i></md-button>\n   </td> -->\n\n  </tr>\n\n </tbody>\n</table>\n\n<!--<table id=\"table\" class=\"table table-striped table-responsive-md btn-table\" >-->\n<!--     <tr>-->\n<!--        <th>Name</th> -->\n<!--        <th>Expression</th> -->\n<!--        <th>Payload</th> -->\n<!--        <th>Description</th>-->\n<!--        <th>Next run</th>-->\n<!--        <th>State</th>-->\n<!--        <th>Delete</th>-->\n<!--        <th>Pause</th>-->\n<!--        <th>Resume</th>-->\n<!-- </tr>-->\n<!-- <tbody>-->\n<!-- <tr ng-repeat=\"item in msg.payload\">-->\n<!--   <td >{{item.config.name}}</td>-->\n<!--   <td style=\"white-space: nowrap\" nowrap>{{item.config.expression}}</td>-->\n<!--   <td >{{item.config.payload}}</td>-->\n<!--   <td >{{item.status.description}}</td>-->\n<!--   <td >{{item.status.nextDescription}}</td>-->\n<!--   <td >{{item.status.isRunning ? 'Running' : 'Paused'}}</td>-->\n<!--   <td>-->\n<!--       <md-button class=\"md-raised\"-->\n<!--        ng-click=\"send([[{payload:{command:'remove', name: item.config.name}}, {payload:{command:'list-all'}}]]);\"> <i class=\"fa fa-trash\"> </i></md-button>-->\n<!--   </td>-->\n<!--   <td>-->\n<!--       <md-button class=\"button\" -->\n<!--       ng-click=\"send([[{payload:{command:'pause', name: item.config.name}}, {payload:{command:'list-all'}}]]);\"> <i class=\"fa fa-pause\"> </i></md-button>-->\n<!--   </td>-->\n<!--   <td>-->\n<!--       <md-button class=\"button\" -->\n<!--       ng-click=\"send([[{payload:{command:'start', name: item.config.name}}, {payload:{command:'list-all'}}]]);\"> <i class=\"fa fa-play\"> </i></md-button>-->\n<!--   </td>-->\n<!-- </tr>-->\n<!-- </tbody>-->\n<!--</table>-->\n\n","storeOutMessages":false,"fwdInMessages":false,"resendOnRefresh":false,"templateScope":"local","x":820,"y":960,"wires":[[]]},{"id":"3b9c3e39.b0eaf2","type":"function","z":"4e969c7c.f77dc4","name":"status-all","func":"if(!msg.payload || !msg.payload.result || !msg.payload.result.length){\n    msg.payload = [];\n    return msg;\n}\nmsg.payload = msg.payload.result;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":620,"y":960,"wires":[["5355c793.e8c0b8"]]},{"id":"3dba72ab.a9c45e","type":"ui_button","z":"4e969c7c.f77dc4","name":"Status all-dynamic","group":"8784088a.4ec318","order":2,"width":4,"height":1,"passthru":true,"label":"Heizprofilstatus","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"","payloadType":"str","topic":"status-all","topicType":"str","x":510,"y":200,"wires":[["8ed2d296.e5771"]]},{"id":"59f8dec0.63298","type":"ui_button","z":"4e969c7c.f77dc4","name":"Remove all dynamic","group":"8784088a.4ec318","order":4,"width":4,"height":1,"passthru":true,"label":"Heizprofil löschen","tooltip":"","color":"","bgcolor":"","icon":"","payload":"1","payloadType":"str","topic":"remove-all-dynamic","x":220,"y":280,"wires":[["93efb1f9.b065f","9c5dd9ca.052698"]]},{"id":"fe876335.8d72c","type":"ui_button","z":"4e969c7c.f77dc4","name":"Load all dynamic","group":"8784088a.4ec318","order":3,"width":4,"height":1,"passthru":true,"label":"Heizprofil laden","tooltip":"","color":"","bgcolor":"","icon":"","payload":"","payloadType":"date","topic":"","x":230,"y":340,"wires":[["bee78fa3.da3b6","ce5507d1.7ea898"]]},{"id":"ce186cd3.00ed4","type":"function","z":"4e969c7c.f77dc4","name":"","func":"msg.payload = {\"command\":\"describe\",\n               \"expressionType\":\"solar\",\n               \"location\":\"48.38157356491496 10.437845885753632\",\n               \"solarType\":\"selected\",\n               \"solarEvents\":\"sunrise,sunset\",\n               \"timeZone\":\"Europe/Berlin\"\n              };\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":480,"y":100,"wires":[["6e94c237.fde8dc"]]},{"id":"be95831c.56938","type":"inject","z":"4e969c7c.f77dc4","name":"at 00:01","props":[{"p":"payload"}],"repeat":"","crontab":"01 00 * * *","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":260,"y":80,"wires":[["ce186cd3.00ed4"]]},{"id":"4fec987f.72b318","type":"switch","z":"4e969c7c.f77dc4","name":"","property":"payload.command.command","propertyType":"msg","rules":[{"t":"eq","v":"describe","vt":"str"},{"t":"eq","v":"status-all","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":470,"y":920,"wires":[["8cf018fb.860648"],["3b9c3e39.b0eaf2"]]},{"id":"8cf018fb.860648","type":"function","z":"4e969c7c.f77dc4","name":"get sr + ss items","func":"\nvar sunrise = msg.payload.result.eventTimes.find(e => {\n    return e.event == \"sunrise\"\n})\n\nvar sunset = msg.payload.result.eventTimes.find(e => {\n    return e.event == \"sunset\"\n})\n\nmsg.payload = {\n    sunrise : sunrise.time,\n    sunset : sunset.time\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":640,"y":880,"wires":[["eddd2a47.4c59d8","698182eb.6b7f3c"]]},{"id":"3c9082b6.02527e","type":"moment","z":"4e969c7c.f77dc4","name":"timestamp","topic":"","input":"","inputType":"msg","inTz":"Europe/Berlin","adjAmount":0,"adjType":"days","adjDir":"add","format":"x","locale":"de-DE","output":"","outputType":"msg","outTz":"Europe/Berlin","x":970,"y":840,"wires":[["89731ed5.1eaa","89878eaf.1f54e"]]},{"id":"14a096a8.7cfa79","type":"moment","z":"4e969c7c.f77dc4","name":"timestamp","topic":"","input":"","inputType":"msg","inTz":"Europe/Berlin","adjAmount":0,"adjType":"days","adjDir":"add","format":"x","locale":"de-DE","output":"","outputType":"msg","outTz":"Europe/Berlin","x":970,"y":920,"wires":[["57a09140.7e8e1","c7211032.45a01"]]},{"id":"b009ed30.c3d08","type":"ui_dropdown","z":"4e969c7c.f77dc4","name":"","label":"Heizungsprofile","tooltip":"","place":"Select option","group":"8784088a.4ec318","order":1,"width":0,"height":0,"passthru":false,"multiple":false,"options":[{"label":"Standard","value":1,"type":"num"},{"label":"Gäste","value":2,"type":"num"},{"label":"DG 18 Grad","value":3,"type":"num"},{"label":"Urlaub","value":4,"type":"num"}],"payload":"","topic":"","x":500,"y":40,"wires":[["37fd8f27.715"]]},{"id":"37fd8f27.715","type":"change","z":"4e969c7c.f77dc4","name":"","rules":[{"t":"set","p":"profilenummer","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":710,"y":40,"wires":[["9cb0f7c5.74fd78"]]},{"id":"93efb1f9.b065f","type":"delay","z":"4e969c7c.f77dc4","name":"20 ms","pauseType":"delay","timeout":"20","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"outputs":1,"x":270,"y":220,"wires":[["3dba72ab.a9c45e"]]},{"id":"ec3a1a4e.dfa0e8","type":"link in","z":"4e969c7c.f77dc4","name":"Status","links":["a21f9d7c.8aa0b","3448e26.76c571e"],"x":55,"y":220,"wires":[["93efb1f9.b065f"]]},{"id":"5f636878.66aff8","type":"file in","z":"4e969c7c.f77dc4","name":"","filename":"/home/pi/Heizungsprofile/Standard.txt","format":"lines","chunk":false,"sendError":false,"encoding":"none","x":570,"y":480,"wires":[["13483ad5.b67f65"]]},{"id":"33b32bc9.a11024","type":"json","z":"4e969c7c.f77dc4","name":"","property":"payload","action":"","pretty":true,"x":1030,"y":540,"wires":[["4a9fce4d.54d75","a21f9d7c.8aa0b","3f76705c.b36e5"]]},{"id":"4a9fce4d.54d75","type":"debug","z":"4e969c7c.f77dc4","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1190,"y":540,"wires":[]},{"id":"13483ad5.b67f65","type":"switch","z":"4e969c7c.f77dc4","name":"","property":"payload","propertyType":"msg","rules":[{"t":"nempty"}],"checkall":"true","repair":false,"outputs":1,"x":910,"y":540,"wires":[["33b32bc9.a11024"]]},{"id":"3b6dab5.be4ea54","type":"file in","z":"4e969c7c.f77dc4","name":"","filename":"/home/pi/Heizungsprofile/Gaeste.txt","format":"lines","chunk":false,"sendError":false,"encoding":"none","x":560,"y":520,"wires":[["13483ad5.b67f65"]]},{"id":"cdc555ba.a9b5e8","type":"file in","z":"4e969c7c.f77dc4","name":"","filename":"/home/pi/Heizungsprofile/DG-18-Grad.txt","format":"lines","chunk":false,"sendError":false,"encoding":"none","x":580,"y":560,"wires":[["13483ad5.b67f65"]]},{"id":"a2ee7cd9.983b3","type":"file in","z":"4e969c7c.f77dc4","name":"","filename":"/home/pi/Heizungsprofile/Urlaub.txt","format":"lines","chunk":false,"sendError":false,"encoding":"none","x":560,"y":600,"wires":[["13483ad5.b67f65"]]},{"id":"bee78fa3.da3b6","type":"switch","z":"4e969c7c.f77dc4","name":"flow profilenummer","property":"profilenummer","propertyType":"flow","rules":[{"t":"eq","v":"1","vt":"num"},{"t":"eq","v":"2","vt":"num"},{"t":"eq","v":"3","vt":"num"},{"t":"eq","v":"4","vt":"num"}],"checkall":"true","repair":false,"outputs":4,"x":250,"y":540,"wires":[["5f636878.66aff8"],["3b6dab5.be4ea54"],["cdc555ba.a9b5e8"],["a2ee7cd9.983b3"]]},{"id":"a21f9d7c.8aa0b","type":"link out","z":"4e969c7c.f77dc4","name":"load done","links":["ec3a1a4e.dfa0e8"],"x":1135,"y":500,"wires":[]},{"id":"89731ed5.1eaa","type":"change","z":"4e969c7c.f77dc4","name":"","rules":[{"t":"set","p":"sunrise","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1190,"y":820,"wires":[["e4da9af1.a16978"]]},{"id":"57a09140.7e8e1","type":"change","z":"4e969c7c.f77dc4","name":"","rules":[{"t":"set","p":"sunset","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1180,"y":940,"wires":[[]]},{"id":"89878eaf.1f54e","type":"mqtt out","z":"4e969c7c.f77dc4","name":"","topic":"sun/rise","qos":"0","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"99603ece.52e2a","x":1160,"y":860,"wires":[]},{"id":"c7211032.45a01","type":"mqtt out","z":"4e969c7c.f77dc4","name":"","topic":"sun/set","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"99603ece.52e2a","x":1160,"y":900,"wires":[]},{"id":"21386ae0.1c6dd6","type":"function","z":"4e969c7c.f77dc4","name":"","func":"delete msg.topic;\nlet room = msg.payload.room;\nlet type = msg.payload.type;\nlet temp = msg.payload.value;\nmsg.payload = msg.time + \": \" + room + \" \" + type + \" \" + temp;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":820,"y":720,"wires":[["c1463448.066898"]]},{"id":"59b54d7f.fed3d4","type":"ui_toast","z":"4e969c7c.f77dc4","position":"top right","displayTime":"3","highlight":"","sendall":true,"outputs":0,"ok":"OK","cancel":"","raw":false,"topic":"","name":"Soll-Temperaturwechsel","x":1230,"y":720,"wires":[]},{"id":"9c5dd9ca.052698","type":"link out","z":"4e969c7c.f77dc4","name":"","links":["f1362681.3482f8"],"x":635,"y":280,"wires":[]},{"id":"8ed2d296.e5771","type":"link out","z":"4e969c7c.f77dc4","name":"","links":["f1362681.3482f8"],"x":635,"y":200,"wires":[]},{"id":"3f76705c.b36e5","type":"link out","z":"4e969c7c.f77dc4","name":"","links":["f1362681.3482f8"],"x":1135,"y":580,"wires":[]},{"id":"f1362681.3482f8","type":"link in","z":"4e969c7c.f77dc4","name":"CRON PLUS","links":["3f76705c.b36e5","6e94c237.fde8dc","8ed2d296.e5771","9c5dd9ca.052698"],"x":95,"y":800,"wires":[["70489757.9ad0d8"]]},{"id":"eddd2a47.4c59d8","type":"change","z":"4e969c7c.f77dc4","name":"sunrise","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.sunrise","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":820,"y":840,"wires":[["3c9082b6.02527e"]]},{"id":"698182eb.6b7f3c","type":"change","z":"4e969c7c.f77dc4","name":"sunset","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.sunset","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":810,"y":920,"wires":[["14a096a8.7cfa79"]]},{"id":"6e94c237.fde8dc","type":"link out","z":"4e969c7c.f77dc4","name":"","links":["f1362681.3482f8"],"x":635,"y":100,"wires":[]},{"id":"9cb0f7c5.74fd78","type":"link out","z":"4e969c7c.f77dc4","name":"profil out","links":["3bbc34a9.cfd8bc","e767c620.4102b8"],"x":875,"y":40,"wires":[]},{"id":"e767c620.4102b8","type":"link in","z":"4e969c7c.f77dc4","name":"load dynamic","links":["9cb0f7c5.74fd78","bdc9d93.69b7f28"],"x":55,"y":340,"wires":[["fe876335.8d72c"]]},{"id":"b1d3a5cd.458c88","type":"link in","z":"4e969c7c.f77dc4","name":"remove all dynamic","links":["63c75b60.30bb04"],"x":55,"y":280,"wires":[["59f8dec0.63298"]]},{"id":"a35ed99c.1f34c8","type":"link out","z":"4e969c7c.f77dc4","name":"","links":["12480e97.570921"],"x":915,"y":760,"wires":[]},{"id":"1b9cda61.b7a3f6","type":"comment","z":"4e969c7c.f77dc4","name":"to room-router --->","info":"","x":1170,"y":760,"wires":[]},{"id":"c1463448.066898","type":"delay","z":"4e969c7c.f77dc4","name":"","pauseType":"rate","timeout":"4","timeoutUnits":"seconds","rate":"1","nbRateUnits":"2","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"outputs":1,"x":1000,"y":720,"wires":[["59b54d7f.fed3d4"]]},{"id":"a0d6c44.a702438","type":"moment","z":"4e969c7c.f77dc4","name":"HH:mm","topic":"","input":"xyz","inputType":"flow","inTz":"Europe/Berlin","adjAmount":0,"adjType":"days","adjDir":"add","format":"HH:mm","locale":"de-DE","output":"time","outputType":"msg","outTz":"Europe/Berlin","x":660,"y":720,"wires":[["21386ae0.1c6dd6"]]},{"id":"defdf4d8.0e4288","type":"change","z":"4e969c7c.f77dc4","name":"no topic","rules":[{"t":"delete","p":"topic","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":660,"y":760,"wires":[["0336b18870e612c7"]]},{"id":"3448e26.76c571e","type":"link out","z":"4e969c7c.f77dc4","name":"","links":["ec3a1a4e.dfa0e8"],"x":635,"y":340,"wires":[]},{"id":"ce5507d1.7ea898","type":"delay","z":"4e969c7c.f77dc4","name":"10 ms","pauseType":"delay","timeout":"10","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"outputs":1,"x":470,"y":340,"wires":[["3448e26.76c571e"]]},{"id":"b1013812.06a738","type":"link in","z":"4e969c7c.f77dc4","name":"cron init","links":["35ca8d8a.b96ce2","e51bdc1c.bb49a"],"x":295,"y":120,"wires":[["ce186cd3.00ed4","494b9af3.0911c4"]]},{"id":"494b9af3.0911c4","type":"delay","z":"4e969c7c.f77dc4","name":"","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"outputs":1,"x":260,"y":180,"wires":[["3dba72ab.a9c45e"]]},{"id":"e381ec2d.78eb2","type":"function","z":"4e969c7c.f77dc4","name":"if cron true","func":"if (global.get('cron') === true){\n    node.status({fill:\"green\",shape:\"ring\",text:\"true\"});\n    return msg;\n}\nelse {\n    node.status({fill:\"red\",shape:\"ring\",text:\"false\"});\n}\nreturn null;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":490,"y":740,"wires":[["a0d6c44.a702438","defdf4d8.0e4288"]]},{"id":"a1dcf39b.e4e5c","type":"function","z":"4e969c7c.f77dc4","name":"","func":"let day = 1000*60*60*24;\nlet sr = parseInt (flow.get('sunrise'));\nlet ss = parseInt(flow.get('sunset'));\nlet dif = (ss - sr);\n//msg.topic = \"debug\"  \n//msg.payload = dif;\n//node.send(msg);\nlet dauer_tag = runtime_str(dif);\ndif = day - dif;\nlet dauer_nacht = runtime_str(dif);\nflow.set('tag', dauer_tag);\nflow.set('nacht', dauer_nacht);\nmsg.payload = dauer_tag + \" : \" + dauer_nacht; \nreturn msg;\n\n//----------------------------------\n\n//  HH:MM:SS\n\nfunction runtime_str(ms){\n    let t = ms / 1000;\n    let h = (Math.floor(t / 3600)).toString();\n    let m = (Math.floor(t%3600 / 60)).toString();\n    let s = (Math.floor(t%3600 % 60)).toString();\n//    let r = h + \":\" + (\"0\" + m).slice(-2) + \":\" + (\"0\" + s).slice(-2);\n    let r = h + \":\" + (\"0\" + m).slice(-2);    \n    return r;  \n}\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1580,"y":820,"wires":[["21b33ac3.087866"]]},{"id":"e4da9af1.a16978","type":"delay","z":"4e969c7c.f77dc4","name":"","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"outputs":1,"x":1400,"y":820,"wires":[["a1dcf39b.e4e5c"]]},{"id":"21b33ac3.087866","type":"debug","z":"4e969c7c.f77dc4","name":"","active":false,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":1730,"y":820,"wires":[]},{"id":"0336b18870e612c7","type":"function","z":"4e969c7c.f77dc4","name":"","func":"let t = JSON.stringify(msg.payload); \nnode.status({fill:\"green\", shape:\"ring\", text:t});\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":820,"y":760,"wires":[["a35ed99c.1f34c8"]]},{"id":"8518d8882da9f81e","type":"function","z":"4e969c7c.f77dc4","name":"store CurrentSoll in global","func":"if (msg.payload.type === \"soll\") {\n    let room = \"CurrentSoll_\" + msg.payload.room;\n    let value = parseInt(msg.payload.value);\n    global.set(room,value);\n    let t = msg.time + \":\" + room + \" \" + value + \" soll\";\n    node.status({fill:\"blue\",shape:\"ring\",text:t});    \n}\nreturn null;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":670,"y":660,"wires":[[]]},{"id":"31a8f2ab7e3d71ec","type":"moment","z":"4e969c7c.f77dc4","name":"HH:mm","topic":"","input":"","inputType":"date","inTz":"Europe/Berlin","adjAmount":0,"adjType":"days","adjDir":"add","format":"HH:mm","locale":"de-DE","output":"time","outputType":"msg","outTz":"Europe/Berlin","x":480,"y":660,"wires":[["8518d8882da9f81e"]]},{"id":"8784088a.4ec318","type":"ui_group","name":"CRON","tab":"9920c734.9de2a8","order":3,"disp":false,"width":"16","collapse":false},{"id":"99603ece.52e2a","type":"mqtt-broker","broker":"10.0.0.43","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":4,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"9920c734.9de2a8","type":"ui_tab","name":"CRON Plus","icon":"dashboard","order":22,"disabled":false,"hidden":false}]

this my standard profile file:
Standard.txt (6.7 KB)

1 Like

I think similar to you, and like to avoid full-dependence on 3rd party stuff because it probably won't have great long term support.
But I also started migrating my household heat to microcontrollers and NodeRed management.
For the scheduling side, I did end up going with a contrib node that has been very reliable and works great: node-red-contrib-ui-time-scheduler .

I build a schedule and save it to file (home or vacation/away), and it sends a numeric output to a change node that sets a global variable, which is read elsewhere and compared to temp sensor values. Between the scheduler and the change node that sets the global variable, there's a user input box that allows a user to override the schedule if cold or leaving and want to turn the heat down.
The vacation file is loaded by UI button that trips a whole bunch of actions if we go on vacation (kills water heater, well pump, loads vacation heat sched, etc).

I'm not sure that will help with the "last state" issue (not 100% sure I understand it), but if you have it write a file with every schedule change, it can then reload that same file. Without that file output method, I found it would wipe it's schedule with every redeploy.

Regarding the last-state issue, do you mean last state of the schedule?
I would expect that you'd want the system to read the current environment parameters, combined with the current schedule state, and set heat control according to that.
IE, upon restore after crash or power outage, it gets all the temp readings, gets the current state of the burner or contactor, then processes whether or not it should demand heat or not.

UI Scheduler Node:

Edit: Sorry, still writing, not sure how I posted it...

1 Like

Thanks you everyone, @juntiedt, I will have a look at your flow later - visitors over the weekend!

Literally, just finished my version of a flow and it looks as though it may work!! Not saying it is clean, but had some problems getting indexing correct in the for loop, hence the defining of the time variable before the if statement. Cleaning up will be done next week. Time, as usual will tell if I have cracked it!!

EDIT: This seemed to work at the time for HW, but testing shows problems - wrong parameters in loop. NOT WORKING!!
Deleted flow...

Times files...
CHt.imes.txt (103 Bytes)
HWt.imes.txt (49 Bytes)
(Remove .txt from file name)

Comments welcome. (Fingers crossed for midnight rollover!!)

I knew there was easier logic!! Decrement the loop!!

[{"id":"0f754cfa5d2f11b3","type":"tab","label":"Heating Event driven switch","disabled":false,"info":"","env":[]},{"id":"e55ff2a0097c1601","type":"comment","z":"0f754cfa5d2f11b3","name":"filepath","info":"// file path with / at the end\nvar filepath = \"/mnt/array1/NodeRed/AutoBackups/\"; // This is the path\nvar localfilepath = \"/home/pi/node-red-storage/\";\n//\"220129_nodered_backup.zip\"","x":310,"y":60,"wires":[]},{"id":"d22324c7f6658b15","type":"inject","z":"0f754cfa5d2f11b3","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"30","topic":"","payload":"","payloadType":"date","x":140,"y":100,"wires":[["236e0b92934aa646","f4961ee17e09efa4"]]},{"id":"236e0b92934aa646","type":"file in","z":"0f754cfa5d2f11b3","name":"HW times file","filename":"/home/colin/node-red-storage/HWt.imes","filenameType":"str","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":310,"y":100,"wires":[["2c5e99851d92fd90"]]},{"id":"2c5e99851d92fd90","type":"csv","z":"0f754cfa5d2f11b3","name":"","sep":",","hdrin":true,"hdrout":"none","multi":"one","ret":"\\n","temp":"","skip":"0","strings":true,"include_empty_strings":"","include_null_values":"","x":450,"y":100,"wires":[["90934bf9f347ac05"]]},{"id":"347f7f877f9020c9","type":"catch","z":"0f754cfa5d2f11b3","name":"","scope":null,"uncaught":false,"x":620,"y":60,"wires":[["1835d45f1fd96af9"]]},{"id":"1835d45f1fd96af9","type":"debug","z":"0f754cfa5d2f11b3","name":"debug 98","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":760,"y":60,"wires":[]},{"id":"90934bf9f347ac05","type":"join","z":"0f754cfa5d2f11b3","name":"","mode":"custom","build":"array","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"1","count":"","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":570,"y":100,"wires":[["a7c6c2c983db796b"]]},{"id":"e314060fc6833351","type":"debug","z":"0f754cfa5d2f11b3","name":"debug 99","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1060,"y":160,"wires":[]},{"id":"a7c6c2c983db796b","type":"change","z":"0f754cfa5d2f11b3","name":"","rules":[{"t":"set","p":"#:(storeInFile)::timesHW","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":730,"y":100,"wires":[["2a322bc99fb6d87c"]]},{"id":"2a322bc99fb6d87c","type":"function","z":"0f754cfa5d2f11b3","name":"set HW deg","func":"delete msg.payload;\ndelete msg.topic;\ndelete msg.filename;\ndelete msg.columns;\n\nlet length;\nlet setHours;\nlet setMinutes;\nlet eventTime;\nlet timeEvent;\nlet payload = global.get('timesHW', \"storeInFile\");\nlet tempDesired = global.get('tempDesiredHW', \"storeInFile\") || 0;\n\n//Get current day start and end timestamp\nlet dateNow = Date.now();\nlet timeFrom = new Date(dateNow).setHours(0, 0, 0, 0);\nlet timeTo = new Date(dateNow).setHours(23, 59, 59, 0)\n//msg.payload = timeTo; //dateNow + \" + \" + timeFrom + \" + \" + timeTo;\n//msg.timeFrom = timeFrom;\n//msg.timeTo = timeTo;\n\n//let i=3;\n\n//Add offset, iterate until file time < current time\nfor (let i = payload.length - 1; i >= 0; i--) {\n    eventTime = payload[i].time;\n    //    msg.eventTime = eventTime\n    setHours = eventTime.slice(0, eventTime.indexOf(\":\"));\n    setMinutes = eventTime.slice(eventTime.indexOf(\":\") + 1);\n    timeEvent = new Date(timeFrom).setHours(setHours, setMinutes, 0, 0); //Date from midnight this morning\n    msg.timeEvent = timeEvent;\n//    msg.payload = timeFrom + \" + \" + timeEvent + \" + \" + timeTo;\n    msg.index = i;\n\n\n    tempDesired = payload[i].desired;\n    msg.tempDesired = tempDesired;\n    msg.dateNow = dateNow;\n    //    msg.timeThen = timeThen;\n\n    if (dateNow > timeEvent) {\n        global.set('tempDesiredHW', tempDesired, 'storeInFile');\n        node.status({ text: timeEvent + \" | \" + tempDesired });\n        return msg;\n    }\n\n}\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":910,"y":100,"wires":[["b1e54be8e7b858ad"]]},{"id":"b1e54be8e7b858ad","type":"debug","z":"0f754cfa5d2f11b3","name":"debug 100","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1070,"y":100,"wires":[]},{"id":"f4961ee17e09efa4","type":"file in","z":"0f754cfa5d2f11b3","name":"CH times file","filename":"/home/colin/node-red-storage/CHt.imes","filenameType":"str","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":310,"y":140,"wires":[["4d488e485025ed7f"]]},{"id":"4d488e485025ed7f","type":"csv","z":"0f754cfa5d2f11b3","name":"","sep":",","hdrin":true,"hdrout":"none","multi":"one","ret":"\\n","temp":"","skip":"0","strings":true,"include_empty_strings":"","include_null_values":"","x":450,"y":140,"wires":[["392fde4e9b6b416b"]]},{"id":"392fde4e9b6b416b","type":"join","z":"0f754cfa5d2f11b3","name":"","mode":"custom","build":"array","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"1","count":"","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":570,"y":140,"wires":[["22639536baf79828"]]},{"id":"22639536baf79828","type":"change","z":"0f754cfa5d2f11b3","name":"","rules":[{"t":"set","p":"#:(storeInFile)::timesCH","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":730,"y":140,"wires":[["dd79303f44f035c0"]]},{"id":"dd79303f44f035c0","type":"function","z":"0f754cfa5d2f11b3","name":"set CH deg","func":"delete msg.payload;\ndelete msg.topic;\ndelete msg.filename;\ndelete msg.columns;\n\nlet length; \nlet setHours;\nlet setMinutes;\nlet eventTime;\nlet timeEvent;\nlet payload = global.get('timesCH', \"storeInFile\");\nlet tempDesired = global.get('tempDesiredCH', \"storeInFile\")||0;\n\n//Get current day start and end timestamp\nlet dateNow = Date.now();\nlet timeFrom = new Date(dateNow).setHours(0, 0, 0, 0);\nlet timeTo = new Date(dateNow).setHours(23, 59, 59, 0)\n//msg.payload = timeTo; //dateNow + \" + \" + timeFrom + \" + \" + timeTo;\n//msg.timeFrom = timeFrom;\n//msg.timeTo = timeTo;\n\n//let i=3;\n\n//Add offset, iterate until file time < current time\nfor (let i = payload.length - 1; i >= 0; i--) {\n    eventTime = payload[i].time;\n//    msg.eventTime = eventTime\n    setHours = eventTime.slice(0, eventTime.indexOf(\":\"));\n    setMinutes = eventTime.slice(eventTime.indexOf(\":\") + 1);\n    timeEvent = new Date(timeFrom).setHours(setHours,setMinutes,0,0); //Date from midnight this morning\n    msg.timeEvent = timeEvent;\n//    msg.payload = timeFrom + \" + \" + timeEvent + \" + \" + timeTo;\n    msg.index = i;\n\n\n    tempDesired = payload[i].desired;\nmsg.tempDesired = tempDesired;\nmsg.dateNow = dateNow;\n//    msg.timeThen = timeThen;\n\n    if (dateNow > timeEvent){\n        global.set('tempDesiredCH', tempDesired, 'storeInFile');\n        node.status({ text: timeEvent + \" | \" + tempDesired });\n        return msg;  \n    }\n\n}\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":910,"y":160,"wires":[["e314060fc6833351"]]}]
1 Like

@juntiedt Had a look at the flow, and it may suffer from the problem described.

After a Power Loss (say 2 hours), if the desired temperature increases during that time, it will not be recognised until the next Cron event from the Dynamic Schedule. For a Heating system that takes a while to heat up, this could be a long time, especially if this occurs over a period where the heating is off.

Similarly, if the desired temperature decreases during that time, the heating system may be on until the next cron event.

Hence my desire to update regularly as with the boiler using lower temperatures for Condensing Mode, the thermal lag can be very slow.

if you loose power your boiler or heating system will also be down in most cases.
I am saving my cron data into memory which is persisted. if you restart after a power loss you can simulate the last cron event depending of the temperatur of your boiler or heating system. Not perfectly but closer to what you are looking for.

As I think I posted earlier if the dynamic schedule is set to repeat the action every minute then that problem goes away.

Sorry, I missed that. Are you just injecting into cronplus every minute to enable this?

If so, then I have missed a bit of understanding with regard to cronplus and need to go experiment.

Here are a couple of examples of triggering cron jobs every so often between two times

Thank you Colin, I get forget those 'hidden gems' within CRON. I keep thinking (and must change my mindset) that CRON triggers single events. Having done most of my programming in Machine Code, or Assembler, these higher level functions take some getting used to!

this is ok if your RPI still has power. If the controller is down this does not work.

Well obviously if node-red is not running it can't control anything. However, when node red is started the cron node will send the current required state at the next repeat interval. Without the repeating output configured the cron node will not send anything till the next event time.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.