Show and tell: sleep dimmer (lighting / audio / whatever)

Here is a function that fades out a light at bedtime, where you can set three values (in red):

I'm sure this is possible using only core nodes, but I am using Node-RED to learn a bit more about coding. For example, I wasn't sure how to add a delay into a loop, so I googled for possible ways to do this and came up with this way using a function that seems to call itself:

var startval = msg.payload;
var dimtime = msg.dimtime * 1000;
var initialdelay = msg.initialdelay * 1000;
var steptime = dimtime / startval;
node.send(msg);

setTimeout(function(){
    var i = startval;
    function f() {
        if (i>0) {
            i--;
            msg.payload = i;
            node.send(msg);
            node.status({fill:"green", shape:"ring", text:"fading"});
        } else {
            node.status();
            return;
        }
        setTimeout( f, steptime );
    }
    f();
},initialdelay)

node.status({fill:"blue", shape:"ring", text:"Initial delay"});

Here it is with some sample values:

[{"id":"164e4745.06ee19","type":"function","z":"705d403b.f2b8","name":"Fade Out","func":"var startval = msg.payload;\nvar dimtime = msg.dimtime * 1000;\nvar initialdelay = msg.initialdelay * 1000;\nvar steptime = dimtime / startval;\n\n\n\n\nnode.send(msg);\n\n// Initial Delay\nsetTimeout(function(){\n    var i = startval;\n    function f() {\n        if (i>0) {\n            i--;\n            msg.payload = i;\n            node.send(msg);\n            node.status({fill:\"green\", shape:\"ring\", text:\"fading\"});\n        } else {\n            node.status();\n            return;\n        }\n        setTimeout( f, steptime );\n    }\n    f();\n},initialdelay)\n\n\nnode.status({fill:\"blue\", shape:\"ring\", text:\"Initial delay\"});\n","outputs":1,"noerr":0,"x":2960,"y":360,"wires":[["784f9ddd.3bd7d4"]]},{"id":"784f9ddd.3bd7d4","type":"debug","z":"705d403b.f2b8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":3110,"y":360,"wires":[]},{"id":"d28c7544.d904a8","type":"change","z":"705d403b.f2b8","name":"Dim settings","rules":[{"t":"set","p":"payload","pt":"msg","to":"10","tot":"num"},{"t":"set","p":"initialdelay","pt":"msg","to":"10","tot":"num"},{"t":"set","p":"dimtime","pt":"msg","to":"30","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":2810,"y":360,"wires":[["164e4745.06ee19"]]},{"id":"f5495c9d.55dd5","type":"inject","z":"705d403b.f2b8","name":"","topic":"home/bedroom/light/circuit2level","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":2660,"y":360,"wires":[["d28c7544.d904a8"]]}]

I've added more detailed status text, see below.

var startval = msg.payload;
var dimtime = msg.dimtime * 1000;
var initialdelay = msg.initialdelay * 1000;
var steptime = dimtime / startval;

node.send(msg);

setTimeout(function(){
    var i = startval;
    function f() {
        if (i>0) {
            i--;
            msg.payload = i;
            node.send(msg);
            var fadetxt = "Fading " + (i*steptime/1000).toString() + "s, " + i.toString() + "/" + startval.toString();
            node.status({fill:"green", shape:"ring", text:fadetxt});
        } else {
            node.status({});
            return;
        }
        setTimeout( f, steptime );
    }
    f();
},initialdelay)

node.status({fill:"blue", shape:"ring", text:"Delay " + msg.initialdelay.toString() + "s"});

One thing to be aware. What happens if you click inject a second time before the fading ends?

Hi @cflurin - I asked about that in general discussion forum here, planning to implement a msg.reset. Cheers!

Updated version:

  • saves the timer to global context using the ID of the function node as its name (to allow for later reset)
  • allows for a msg.reset which cancels the sleep timer
  • if repeat message is sent, it restarts the sleep timer
  • some basic config checking before it runs

Here it is. Thanks to @cflurin and @dceejay for help!

[{"id":"e785b98.f8a2e48","type":"comment","z":"705d403b.f2b8","name":"Version 2","info":"var startval = 10;\nmsg.payload = startval;\nnode.send(msg);\n\n// Initial Delay\nsetTimeout(function(){\n    node.status({fill:\"blue\", shape:\"ring\", text:\"fade: initial delay\"});\n},3000)\n\nvar i = startval, howManyTimes = startval;\nfunction f() {\n    i--;\n    msg.payload = i;\n    node.send(msg);\n    node.status({fill:\"green\", shape:\"ring\", text:\"fading\"});\n    if( i > 0 ){\n        \n    }\n    setTimeout( f, 3000 );\n}\nf();\n\n\nnode.status({});","x":3000,"y":600,"wires":[]},{"id":"5de7f0a0.12d5d","type":"debug","z":"705d403b.f2b8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":3550,"y":660,"wires":[]},{"id":"6d99187c.9f9c28","type":"change","z":"705d403b.f2b8","name":"Dim settings","rules":[{"t":"set","p":"payload","pt":"msg","to":"100","tot":"str"},{"t":"set","p":"initialdelay","pt":"msg","to":"2","tot":"str"},{"t":"set","p":"dimtime","pt":"msg","to":"10","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":3190,"y":660,"wires":[["6c188de8.225534"]]},{"id":"1ac26656.5e6aba","type":"inject","z":"705d403b.f2b8","name":"","topic":"home/bedroom/light/circuit2level","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":3020,"y":660,"wires":[["6d99187c.9f9c28"]]},{"id":"6c188de8.225534","type":"function","z":"705d403b.f2b8","name":"Fade Out","func":"// Check for correct incoming values\nif (!msg.reset &&\n    (!msg.payload ||\n     !msg.dimtime ||\n     !msg.initialdelay)) {\n    node.warn(\"Fader not correctly configured\");\n    return;\n}\n\nvar thisnodeid = \"fader_\" + node.id;\n\n// Get incoming values\nvar startval = Number(msg.payload); // just in case incoming was a string\nvar dimtime = msg.dimtime * 1000;\nvar initialdelay = msg.initialdelay * 1000;\nvar steptime = dimtime / startval;\n\n// If we got a msg.reset, cancel timer, clear status,\n//  clear function from memory, and don't restart\nif (msg.reset) {\n    var id = global.get(thisnodeid);\n    clearTimeout(id);\n    node.status({});\n    global.set(thisnodeid,\"\");\n    return;\n}\n\n// if global.get('id') has an object, then it's running\n// and if it's running then clear the timer, clear the memory,\n//  then continue (i.e. start over with new fader)\nif (global.get('id') !== '') {\n    var id = global.get(thisnodeid);\n    clearTimeout(id);\n    node.status({});\n    global.set(thisnodeid,\"\");\n}\n\n// Set initial light level\nmsg.payload = startval; // (use startval in case input was string)\nnode.send(msg);\n\nvar id = setTimeout(function(){\n    var i = startval;\n    function f() {\n        if (i>0) {\n            i--;\n            msg.payload = i;\n            node.send(msg);\n            var fadetxt = \"Fading \" + (i*steptime/1000).toString() + \"s, \" + i.toString() + \"/\" + startval.toString();\n            node.status({fill:\"green\", shape:\"ring\", text:fadetxt});\n        } else {\n            node.status({});\n            global.set(thisnodeid,\"\");\n            return;\n        }\n        id = setTimeout( f, steptime );\n        global.set(thisnodeid,id);\n    }\n    f();\n},initialdelay)\nglobal.set(thisnodeid,id);\n\nnode.status({fill:\"blue\", shape:\"ring\", text:\"Delay \" + msg.initialdelay.toString() + \"s\"});","outputs":1,"noerr":0,"x":3380,"y":660,"wires":[["5de7f0a0.12d5d"]]},{"id":"b9023014.57439","type":"inject","z":"705d403b.f2b8","name":"clear timeout","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":3210,"y":600,"wires":[["65ccb00c.011ef"]]},{"id":"65ccb00c.011ef","type":"change","z":"705d403b.f2b8","name":"set msg.reset to \"yes\"","rules":[{"t":"set","p":"reset","pt":"msg","to":"yes","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":3400,"y":600,"wires":[["6c188de8.225534"]]}]

You shouldn't need to use a global context... just use a local context inside the node - otherwise you can only use 1 copy of you node or it will conflict with itself.

@dceejay The function creates one variable per copy (it's named based on the ID of the node) although agree that this should definitely be scoped to node, not global. Will change it and update here later. Cheers, Mat