Virtual sunrise / sunrise effect

I have four color bulbs (three in a lamp one above the other and another one above this lamp in a star) and want to create a sunrise effect. But looking for "sunrise" only shows threads regarding calculating the sunrise or sunset.
This effect should last 10-15 minutes. I´m thinking of an array for all 4 bulbs, which defines color and brightness of each bulb, changing from one state to the next every minute, so that I need 10-15 elements (or more to get a smoother effect).

I also have a WLED controller with a neopixel LED stripe. Is there a script for the WLED controller I could import? So far I have only played with the existing, build-in effects .

Any comments?

Well the first part of your flow could be using node-red-contrib-cron-plus node to set the sunrise/sunset time, and to deduct 15 minutes to enable the rest of the flow reach full/off brightness, like this;


Hi Paul,

the time, when this effect should start, is not the problem, but how to store the data for the 4 bulbs and how to generate a fading or brightening effect to simulate a sunrise (or sunset).

Some time ago I wrote this 'function' to gradually dim a bedside lamp (probably could have been achieved easier!).
The countdown gradually reduces the input to a Shelly dimmer until it's 1, meaning the lamp is off (Shelly brightness is 1 - 100).

var status = context.get('status') || { bright: 1, switchOn: false };
var ison, timer, bright, stat

// check if the input is to switch on/off or adjust brightness
if (msg.payload.on !== status.switchOn) {
    ison = msg.payload.on;
    node.status(msg.payload.on ? "Light turned on" : "OFF");

    if (ison === true) {
        status.switchOn = true;
        msg.payload = { "brightness": 50, "turn": "on" };
        status.bright = 50;
        context.set("status", status);
    } else if (ison === false) {
        context.set("status.switchOn", false);
        // Timer function to gradually reduce light level
        // The trailing value is the time in ms between each increment
        timer = setInterval(countdown, 100);

//else if (msg.payload.brightness !== undefined) {
else {
    bright = msg.payload.brightness;
    node.status("Brightness set to " + msg.payload.brightness + "%");
    msg.payload = { "brightness": bright };
    context.set("status.bright", bright);

function countdown() {
    let saved = context.get("status")
    bright = saved.bright
    stat = saved.switchOn

    if (stat === true) {

    if (bright <= 1) {
        node.status({ text: ("Light turned off") });
        msg.payload = { "turn": "off" };
        context.set("status.switchOn", false)
    } else {
        node.status({ text: ("Light dimming.. " + bright + "%") });
        msg.payload = ({ brightness: bright });
        context.set("status.bright", bright);

Hi @Paul-Reed

That is great!

I have the opposite problem where I want to fade up bulbs.

But with that I can at least make a start.

1 Like

Hi @stefan24,

Talking TASMOTA here
If they are smart bulbs - which you imply they are - you put them into a group and then rather than controlling each bulb, you control the group.

I am new to the idea but I have read about doing that.

Hope that helps.

Oh, and on a side note:
You could do a lot of what @Paul-Reed said on the bulb.
There are good and bad things both/either way.

But in summary:
You set the bulb to turn on and set it's maximum brightness/colour.
Then you make a rule that once it has got the time (from NTP) it fades up to that brightness at a speed.
You need the fade to be set to 1 rather than 0 (default) and the speed to determine how fast it happens.

First attempt:

[{"id":"07de6a14a5c043af","type":"comment","z":"72f946c2.13e94","name":"Sunrise Effekt","info":"","x":130,"y":2360,"wires":[]},{"id":"57622edf39310b2c","type":"function","z":"72f946c2.13e94","name":"function 1","func":"\nvar sunindex = flow.get(\"sunindex\");\n\nif (sunindex > 5)\n{sunindex = 0};\n\nnode.status({ text: (sunindex) });\n\nvar b1 = { payload: \"\" };\nvar b2 = { payload: \"\" };\nvar b3 = { payload: \"\" };\nvar b4 = { payload: \"\" };\n\nvar transition = \",\\\"transition\\\": 40\";\n\nvar sun = [\n    [[40, \"#ff0000\"], [0, \"#ff0000\"], [0, \"#ff0000\"], [0, \"#ff0000\"]],\n    [[70, \"#ff0000\"], [0, \"#ff0000\"], [0, \"#ff0000\"], [0, \"#ff0000\"]],\n    [[200, \"#ff0000\"], [100, \"#ff0000\"], [40, \"#ff0000\"], [0, \"#ff0000\"]],\n    [[100, \"#ff0000\"], [200, \"#ff0000\"], [150, \"#ff0000\"], [15, \"#ff0000\"]],\n    [[0, \"#ff0000\"], [100, \"#ff0000\"], [250, \"#ff8000\"], [100, \"#ff0000\"]],\n    [[0, \"#ff0000\"], [0, \"#ff0000\"], [50, \"#ff8000\"], [250, \"#ff8000\"]]\n    ];\n\nif (sun[sunindex][0][0] === 0) { b4.payload = \"{ \\\"state\\\": \\\"OFF\\\", \\\"brightness\\\": 0 ,\\\"color\\\": \\\"red\\\"}\" }\nelse { b4.payload = \"{ \\\"state\\\": \\\"ON\\\", \\\"brightness\\\":\" + sun[sunindex][0][0] + transition+\",\\\"color\\\": {\\\"hex\\\":\\\"\"+ sun[sunindex][0][1] + \"\\\"}}\" };\n\nif (sun[sunindex][1][0] === 0) { b3.payload = \"{ \\\"state\\\": \\\"OFF\\\", \\\"brightness\\\": 0 ,\\\"color\\\": \\\"red\\\"}\" }\nelse { b3.payload = \"{ \\\"state\\\": \\\"ON\\\", \\\"brightness\\\":\" + sun[sunindex][1][0] + transition +\",\\\"color\\\": {\\\"hex\\\":\\\"\" + sun[sunindex][1][1] + \"\\\"}}\" };\n\nif (sun[sunindex][2][0] === 0) { b2.payload = \"{ \\\"state\\\": \\\"OFF\\\", \\\"brightness\\\": 0 ,\\\"color\\\": \\\"red\\\"}\" }\nelse { b2.payload = \"{ \\\"state\\\": \\\"ON\\\", \\\"brightness\\\":\" + sun[sunindex][2][0] + transition +\",\\\"color\\\": {\\\"hex\\\":\\\"\" + sun[sunindex][2][1] + \"\\\"}}\" };\n\nif (sun[sunindex][3][0] === 0) { b1.payload = \"{ \\\"state\\\": \\\"OFF\\\", \\\"brightness\\\": 0 ,\\\"color\\\": \\\"red\\\"}\" }\nelse { b1.payload = \"{ \\\"state\\\": \\\"ON\\\", \\\"brightness\\\":\" + sun[sunindex][3][0] + transition +\",\\\"color\\\": {\\\"hex\\\":\\\"\" + sun[sunindex][3][1] + \"\\\"}}\" };\n\n\nflow.set(\"sunindex\",sunindex+1);\n\nreturn [b1,b2,b3,b4];\n","outputs":4,"noerr":0,"initialize":"","finalize":"","libs":[],"x":380,"y":2460,"wires":[["0f6e3b5f89dc6499","3d13c42bd56beafe"],["5e6656bd86ea4666","d6d14169633512d3"],["73bd888220bd3c52","15d5f5ba8ef2b6c9"],["72b559370e7ca723","cb368ec5f9ace70c"]]},{"id":"0d403b8b4c6ea88c","type":"inject","z":"72f946c2.13e94","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":2460,"wires":[["57622edf39310b2c"]]},{"id":"0f6e3b5f89dc6499","type":"debug","z":"72f946c2.13e94","name":"b1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":630,"y":2560,"wires":[]},{"id":"73bd888220bd3c52","type":"debug","z":"72f946c2.13e94","name":"b3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":630,"y":2640,"wires":[]},{"id":"5e6656bd86ea4666","type":"debug","z":"72f946c2.13e94","name":"b2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":630,"y":2600,"wires":[]},{"id":"72b559370e7ca723","type":"debug","z":"72f946c2.13e94","name":"b4","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":630,"y":2680,"wires":[]},{"id":"41172aea9e51ec90","type":"inject","z":"72f946c2.13e94","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":2400,"wires":[["29a0fad49ccff069"]]},{"id":"29a0fad49ccff069","type":"change","z":"72f946c2.13e94","name":"","rules":[{"t":"set","p":"sunindex","pt":"flow","to":"0","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":370,"y":2400,"wires":[[]]},{"id":"cb368ec5f9ace70c","type":"mqtt out","z":"72f946c2.13e94","name":"","topic":"zigbee2mqtt/Bulb-M-Col4/set","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"7f211795.773398","x":720,"y":2500,"wires":[]},{"id":"15d5f5ba8ef2b6c9","type":"mqtt out","z":"72f946c2.13e94","name":"","topic":"zigbee2mqtt/Bulb-M-Col3/set","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"7f211795.773398","x":720,"y":2460,"wires":[]},{"id":"d6d14169633512d3","type":"mqtt out","z":"72f946c2.13e94","name":"","topic":"zigbee2mqtt/Bulb-M-Col2/set","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"7f211795.773398","x":720,"y":2420,"wires":[]},{"id":"3d13c42bd56beafe","type":"mqtt out","z":"72f946c2.13e94","name":"","topic":"zigbee2mqtt/Bulb-M-Col1/set","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"7f211795.773398","x":720,"y":2380,"wires":[]},{"id":"7f211795.773398","type":"mqtt-broker","name":"Mosquitto","broker":"localhost","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}]

Transition does not work as expected and I don't like the colors right now.

(Dumb question)

Why not do it this way:

You are sending the same message to all the bulbs and the MQTT OUT node sets the topic so you don't need 4 outputs from the function node.


Sorry. Your example was with the sunindex of 0. As that changes the outputs change.
(maybe put a few different values in the inject nodes like this:


The colours:
Well, you control them with the code.

I digress to Paul's skills here.
I am trying to understand the code and re-write it for my needs.

Hi @Trying_to_learn,

you are right regarding the 4 outputs!

EDIT: I will put the topic in the function code and can really use one output!

And I saw, that with zigbee2mqtt there are some more effects:


Instead of setting a value (e.g. brightness) directly it is also possible to:

  • move: this will automatically move the value over time, to stop send value stop or 0.
  • step: this will increment/decrement the current value by the given one.

The direction of move and step can be either up or down, provide a negative value to move/step down, a positive value to move/step up. To do this send a payload like below to zigbee2mqtt/FRIENDLY_NAME/set

NOTE: brightness move/step will stop at the minimum brightness and won't turn on the light when it's off. In this case use brightness_move_onoff/brightness_step_onoff

  "brightness_move": -40, // Starts moving brightness down at 40 units per second
  "brightness_move": 0, // Stop moving brightness
  "brightness_step": 40 // Increases brightness by 40
  "color_temp_move": 60, // Starts moving color temperature up at 60 units per second
  "color_temp_move": "stop", // Stop moving color temperature
  "color_temp_step": 99, // Increase color temperature by 99

Effect (enum)

Triggers an effect on the light (e.g. make light blink for a few seconds). Value will not be published in the state. It's not possible to read (/get) this value. To write (/set) a value publish a message to topic zigbee2mqtt/FRIENDLY_NAME/set with payload {"effect": NEW_VALUE}. The possible values are: blink, breathe, okay, channel_change, finish_effect, stop_effect.

Like Madness once sung: "Tomorrow's just another day!"

Alas you are zigbee and I am tasmota. Not sure they are.... compatible.

(See my other post I edited it as I maybe goofed)

But your test injecting part has a suggestion allowing better testing.

(ah, here is what I mean)

[{"id":"41172aea9e51ec90","type":"inject","z":"d188b95f33e5f7e4","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0","payloadType":"num","x":170,"y":1340,"wires":[["29a0fad49ccff069"]]},{"id":"fd52a1a935981b63","type":"inject","z":"d188b95f33e5f7e4","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"5","payloadType":"num","x":170,"y":1380,"wires":[["29a0fad49ccff069"]]},{"id":"a79869753b7a8507","type":"comment","z":"d188b95f33e5f7e4","name":"add more inject nodes here as needed","info":"","x":250,"y":1410,"wires":[]},{"id":"29a0fad49ccff069","type":"change","z":"d188b95f33e5f7e4","name":"","rules":[{"t":"set","p":"sunindex","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":410,"y":1380,"wires":[[]]}]

That too is a good idea.

EDIT again

Maybe not a good idea.

That would mean you would have to send 4 message out the one output every time.
Having 4 outputs is better in that you send out the 4 message together rather then one after the other.

I'm not sure if there are any benefits to either way.

Again: Sorry I confused the issue.

Depending on the type of bulb - there is also a transition property which can be used to set both the target brightness and color over (transition) time.

This function might be useful, you can set a slew rate and tell it to automatically send intermediate values, so if the value were currently on 10 and you passed in the value 60 then you can tell it what rate to ramp the output from 10 to 60 and at what rate to send the values.

// Limits the slew rate incoming payload values
// optionally sending intermediate values at specified rate
let maxRate = 5;         // max slew rate units/minute
let sendIntermediates = true;   // whether to send intermediate values
let period = 30000;          // period in millisecs to send new values (if sendIntermediates)
let jumpThreshold = 100;   // if the step asked for is more that this then goes immediately to that value

var newValue = Number(msg.payload);
var timer = context.get('timer') || 0;
// check the value is  a number
if (!isNaN(newValue) && isFinite(newValue)) {
    var target = msg.payload;
    context.set('target', target);
    // set last value to new one if first time through
    var lastValue = context.get('lastValue');
    if (typeof lastValue == "undefined" || lastValue === null) {
        lastValue = newValue;
        context.set('lastValue', newValue);
    // calc new value
    msg.payload = calcOutput();
    // stop the timer
    if (timer) {
        context.set('timer', null);
    // restart it if required to send intermediate values
    if (sendIntermediates) {
        timer = setInterval(function(){
            // the timer has run down calculate next value and send it
            var newValue = calcOutput();
            if (newValue != context.get('lastValueSent')) {
                context.set('lastValueSent', newValue);
                node.send({payload: newValue});
        context.set('timer', timer);
    context.set('lastValueSent', msg.payload);
} else {
    // payload is not a number so ignore it
    // also stop the timer as we don't know what to send any more
    if (timer) {
        context.set('timer', null);
    msg = null;
return msg;

// determines the required output value
function calcOutput() {
    var lastValue = context.get('lastValue');
    var target = context.get('target');
    // set to current value if first time through or step > threshold
    if (typeof lastValue == "undefined" || lastValue === null) lastValue = target;
    var now = new Date();
    var lastTime = context.get('lastTime') || now;
    // limit value to last value +- rate * time
    var maxDelta = (now.getTime() - lastTime.getTime()) * maxRate / (60 * 1000);
    if (Math.abs(target - lastValue) > jumpThreshold) {
        // step > threshold so go there imediately
        newValue = target;
    } else if (target > lastValue) {
        newValue = Math.min( lastValue + maxDelta, target);
    } else {
        newValue = Math.max( lastValue - maxDelta, target);
    context.set('lastValue', newValue);
    context.set('lastTime', now);   
    return newValue;

next idea, next function:

[{"id":"07de6a14a5c043af","type":"comment","z":"72f946c2.13e94","name":"Sunrise Effect 2.0","info":"","x":130,"y":2380,"wires":[]},{"id":"57622edf39310b2c","type":"function","z":"72f946c2.13e94","name":"function 1","func":"var sunindex = flow.get(\"sunindex\");\n\nvar b1 = { payload: \"\", topic: \"\" };\nvar loop = { payload: \"\", delay: 0 , max: 0};\n\nvar sun = [\n// Init (0)\n    [4, 1000, \"{ \\\"state\\\": \\\"OFF\\\", \\\"brightness\\\": 0}\"],\n    [3, 1000, \"{ \\\"state\\\": \\\"OFF\\\", \\\"brightness\\\": 0}\"],\n    [2, 1000, \"{ \\\"state\\\": \\\"OFF\\\", \\\"brightness\\\": 0}\"],\n    [1, 1000, \"{ \\\"state\\\": \\\"OFF\\\", \\\"brightness\\\": 0}\"],\n // Start (4)\n    [4, 5000, \"{ \\\"brightness\\\": 1, \\\"color\\\": { \\\"hex\\\": \\\"#ff0000\\\"},\\\"state\\\": \\\"ON\\\"}\"],\n    [3, 5000, \"{ \\\"brightness\\\": 1, \\\"color\\\": { \\\"hex\\\": \\\"#ff0000\\\"},\\\"state\\\": \\\"ON\\\"}\"],\n    [2, 5000, \"{ \\\"brightness\\\": 1, \\\"color\\\": { \\\"hex\\\": \\\"#ff0000\\\"},\\\"state\\\": \\\"ON\\\"}\"],\n    [1, 5000, \"{ \\\"brightness\\\": 1, \\\"color\\\": { \\\"hex\\\": \\\"#ff0000\\\"},\\\"state\\\": \\\"ON\\\"}\"],\n    [4, 5000, \"{ \\\"brightness\\\": 254, \\\"transition\\\": 1}\"],\n    [3, 5000, \"{ \\\"brightness\\\": 254, \\\"transition\\\": 10}\"],\n    [2, 5000, \"{ \\\"brightness\\\": 254, \\\"transition\\\": 100}\"],\n    [1, 5000, \"{ \\\"brightness\\\": 254, \\\"transition\\\": 1000}\"],\n\n];\n\nb1.payload = sun[sunindex][2];\nb1.topic = \"zigbee2mqtt/Bulb-M-Col\" + sun[sunindex][0]+\"/set\";\nloop.delay = sun[sunindex][1];\nloop.max = sun.length;\n\nnode.status({ text: (sunindex)});\n\nflow.set(\"sunindex\", sunindex + 1);\n\nreturn [loop,b1];\n","outputs":2,"noerr":0,"initialize":"","finalize":"","libs":[],"x":480,"y":2460,"wires":[["af77ba837398a171"],["c7361dd345875808"]]},{"id":"0d403b8b4c6ea88c","type":"inject","z":"72f946c2.13e94","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0","payloadType":"num","x":90,"y":2460,"wires":[["e253b80bb540f7de"]]},{"id":"3d13c42bd56beafe","type":"mqtt out","z":"72f946c2.13e94","name":"","topic":"","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"7f211795.773398","x":790,"y":2460,"wires":[]},{"id":"e253b80bb540f7de","type":"change","z":"72f946c2.13e94","name":"sunindex=0","rules":[{"t":"set","p":"sunindex","pt":"flow","to":"0","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":250,"y":2460,"wires":[["57622edf39310b2c"]]},{"id":"748e4a8bb778a033","type":"delay","z":"72f946c2.13e94","name":"","pauseType":"delayv","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":780,"y":2380,"wires":[["57622edf39310b2c"]]},{"id":"af77ba837398a171","type":"switch","z":"72f946c2.13e94","name":"","property":"sunindex","propertyType":"flow","rules":[{"t":"lt","v":"max","vt":"msg"}],"checkall":"true","repair":false,"outputs":1,"x":650,"y":2380,"wires":[["748e4a8bb778a033"]]},{"id":"c7361dd345875808","type":"json","z":"72f946c2.13e94","name":"","property":"payload","action":"","pretty":false,"x":650,"y":2460,"wires":[["3d13c42bd56beafe"]]},{"id":"7f211795.773398","type":"mqtt-broker","name":"Mosquitto","broker":"localhost","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}]

Only two outputs (the second one for a custom delay time for each effect), which makes it easier to generate effects or color changes for each bulb, without touching the others.

Array consists of:

    [3, 5000, "{ \"brightness\": 1, \"color\": { \"hex\": \"#ff0000\"},\"state\": \"ON\"}"],
    [3, 5000, "{ \"brightness\": 254, \"transition\": 10}"],

Bulb number (for the MQTT topic), Delay time in ms until the next command is executed, string for the bulb. The current entries are for testing only, the sunrise effect is not quite realistic!!
All effects are started automatically through the delay loop (here once initiated via the inject node).

Maybe my cheap zigbee bulbs (Megos, 10 EUR) are not the best with a big range of brightness, but it's fun to test this!

But I can use transition only for color change or brightness change, not both!?

I don't think you need the JSON node, do you? The MQTT node handles objects for you.

You can simplify your flow by using a single function node

const numberOfBulbs = 3 // zero-based index
const transition = 150 // seconds
const brightness = 254

for (let bulb = 0; bulb <= numberOfBulbs; bulb++) {
    node.send({ topic: `zigbee2mqtt/Bulb-M-Col${bulb + 1}/set`, delay:0, payload:{brightness: 1, state: "ON" } })

for(let bulb=0;bulb<=numberOfBulbs;bulb++){
    node.send({ topic: `zigbee2mqtt/Bulb-M-Col${bulb + 1}/set`, delay: bulb * transition * 1000, payload:{brightness, transition}})

No need for flow loops and double outputs.

* 1000 is for demo only (to make it shorter) - add a 0 to match the transition time in seconds.

Transition in color is possible too at the same time, but this depends on the type of bulb. Read the zigbee2mqtt device documentation where it is explained what your bulb supports or not.

@Colin You are right!

Aaah, I was searching for something like that, but did not know, where to start the search.

Reading the docs, I found an additional node.done() command with an example which should be used together with node.send:

If a Function node does asynchronous work with a message, the runtime will not automatically know when it has finished handling the message.

To help it do so, the Function node should call node.done() at the appropriate time. This will allow the runtime to properly track messages through the system.

(from Writing Functions : Node-RED)

But since the delays are handled differently in your case, I will definitely stay at my approach.

And I want to send different colors and brightness values to the different bulbs (but this should be no problem when using my current array).

Thanks anyway for the hint to get cleaner / better code.

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