Simulate Sunrise -- Help with RGB Lamp Sunrise Simulation

Hi everyone,

I have an RGB lamp (for example, an IKEA lamp), and I’m trying to create a sunrise effect at home to wake up in a pleasant way.

However, my current approach isn’t working as expected. Has anyone done something similar before? I’d love to code this myself and would appreciate any advice or suggestions.

Thanks in advance!

Simulate Sunrise

const currentTime = flow.get('current_time') || 0;
const totalTime = flow.get('total_time') || 1800; // 30 minutes
const brightnessSchedule = [
    { time: 0, brightness: 1, colorTemp: 2200 },
    { time: 300, brightness: 20, colorTemp: 2500 },
    { time: 600, brightness: 35, colorTemp: 2800 },
    { time: 720, brightness: 42, colorTemp: 3000 },
    { time: 840, brightness: 48, colorTemp: 3100 },
    { time: 900, brightness: 50, colorTemp: 3200 },
    { time: 1200, brightness: 60, colorTemp: 3500 },
    { time: 1500, brightness: 65, colorTemp: 3800 },
    { time: 1800, brightness: 70, colorTemp: 4000 }
];
const dawnToday = flow.get("dawnToday") || 0;
const now = Date.now();

if (currentTime >= totalTime || now > dawnToday + totalTime * 1000) {
    node.status({ fill: "green", shape: "dot", text: "Sunrise completed 🌞 Light OFF" });
    msg.payload = { service: "turn_off", entity_id: "light.main_door_light" };
    flow.set('current_time', 0);
    return msg;
}

let brightness = 1, colorTemp = 2200;
for (let i = 0; i < brightnessSchedule.length; i++) {
    if (currentTime >= brightnessSchedule[i].time) {
        brightness = brightnessSchedule[i].brightness;
        colorTemp = brightnessSchedule[i].colorTemp;
    }
}
flow.set('current_time', currentTime + 60);
msg.payload = { brightness: brightness, color_temp: colorTemp };
node.status({ fill: "blue", shape: "ring", text: `Brightness: ${brightness}%, Temp: ${colorTemp}K` });
node.debug(`🌅 Updating sunrise: Brightness ${brightness}%, Color Temp ${colorTemp}K`);
return msg;

Check Dawn Time

if (!msg.data || !msg.data.attributes || !msg.data.attributes.today || !msg.data.attributes.tomorrow) {
    node.error("⚠️ Missing 'today' or 'tomorrow' attribute in sensor.home_sun_dawn. Please check the sensor configuration.", msg);
    return null;
}

const dawnToday = new Date(msg.data.attributes.today).getTime();
const dawnTomorrow = new Date(msg.data.attributes.tomorrow).toLocaleString();
const now = Date.now();
const sunriseDuration = 30 * 60 * 1000;
const endSunriseTime = dawnToday + sunriseDuration;

flow.set("dawnToday", dawnToday);
flow.set("total_time", sunriseDuration / 1000);
flow.set("current_time", 0);

if (now >= dawnToday && now <= endSunriseTime) {
    const remainingMinutes = Math.round((endSunriseTime - now) / 60000);
    node.status({ fill: "green", shape: "dot", text: `🌅 Sunrise running | ${remainingMinutes} min left | Ends: ${new Date(endSunriseTime).toLocaleTimeString()}` });
    return msg;
} else {
    return null;
}

I’m the only one who wants to do something like this<?.

Most on HA that want this functionality use adaptive lighting. I use on most lights in my house.

perfectly - how I can do something like this in Nod Red

Is your question how adaptive works with node red? or how to recreate it in NR? To use adaptive you create a configuration for specific lights. When you create that config it will create switches for the config.

If the config is on it will change the lights as programmed, when it’s off the lights work as usual. There is an app link for the component on it’s git page. It will visualize the curve as you change the settings.

How to create it in NR

sun position · rdmtc/node-red-contrib-sun-position Wiki · GitHub Has it - does anyone know?

I thought this was worth looking at as an exercise.

There are I think three questions to answer

  1. how to get the dawn start and end time
  2. how to calculate the required brightness and colour at each action call to change the lamp
  3. how to run the lamp updates

Sun times: There are a number of ways to deal with each question. Since Home Assistant has the sun integration, the next dawn and sunrise times are available. There are also several contrib nodes available for sun position. However, I found a nice sun times module that does the work, and it is available as an npm package so can be loaded into Node-RED.

Using a function node, suncalc can be imported in the settings tab (as SunCalc or whatever you want) and then used in JavaScript. A simple call, with a date object for today, and latitude & longitude, will give an object with all the sun times as JS date objects. A nice extra feature of this module is the ability to add additional sun-time points by elevation. Civil dawn starts at -6 degrees, but nautical dawn is already ‘light’ from -12 degrees, so easy enough to set your own dawn at, say -8 degrees.

A little bit of code extracts the Unix millisecond time for ‘my dawn’ start and for sunrise start, and then the difference between them in minutes to use later. This can be generated anytime between midnight and dawn every morning, so going for 04:00 avoids being too early and hitting DST time changes, or too late and missing dawn.

This can be stored in context, but I have chosen to push the dawn start time to a Home Assistant entity sensor, so that I can also save the minutes (and everything else) in attributes, and then use this as a trigger for the ‘Time’ node.

Calculations: Sun (sky) brightness at dawn, from roughly the start of nautical dawn to full sunrise, can be approximated to a linear increase, so I would be happy to calculate this based on the minutes progression from the start of dawn to sunrise, as 0 to 100 percent of lamp brightness. I would also start by doing the same for colour change, as this makes it very easy to compute both values given the x-minutes into the dawn-to-sunrise time period (which I already have). I find JSONata a bit easier to code, so I have used this to generate an array, one entry per minute for the sunrise period, with linear progression for both brightness and colour between given start-end values.

Lamp action calls: I try to avoid loops in Node-RED since they can be very problematic. Node-RED flow programming focuses more on breaking a loop / iteration into parts, and it is very simple to split an array into individual messages. Then all it takes is a delay node set to rate-limit to one message per minute.

The WebSocket nodes ‘Time’ node (not the Time Entity node) is designed to trigger a new flow based on a future time taken from any other entity (state or property) so I can easily use my entity sensor created to hold the next dawn start time. Although this node is documented to accept Unix milliseconds, it does not appear to like them, so a bit of JSONata turns the Unix milliseconds into a timestamp string, which the Time node does accept.

When the Time node triggers at the start of (my) dawn, the period minutes in the attributes can be used to generate an array of minute changes, which can then be rate-limited to one per minute and used to update the lamp every minute giving a smooth transition.

There are, of course, many other ways to do this…

[{"id":"68503e970850910b","type":"function","z":"70e9a03d9a03cec2","name":"Get dawn and sunrise","func":"// use the following to add my own dawn between nautical and civil //\nSunCalc.addTime(-8, \"dawnchorus\", \"darkenough\");\n\n// get suntimes for my location for V today   V Lat  V Long  //\nconst suntimes = SunCalc.getTimes(new Date(), 51.5, -0.1);\n\n// pick out my dawn start time, period to sunrise, timezone offset //\nlet dawntime = {\"dawn\": suntimes.dawnchorus.getTime(),\n    \"rise\": suntimes.sunrise.getTime(),\n    \"start\": suntimes.dawn.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}),\n    \"offs\": new Date().getTimezoneOffset()};\n// add in dawn to sunrise time in minutes //\ndawntime.mins = Math.round((dawntime.rise - dawntime.dawn)/60000);\n\nmsg.payload = dawntime;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[{"var":"SunCalc","module":"suncalc"}],"x":340,"y":100,"wires":[["433c562d8fabfe91"]]},{"id":"813d6848ff91a2c1","type":"inject","z":"70e9a03d9a03cec2","name":"Setup for dawn","props":[],"repeat":"","crontab":"00 04 * * *","once":false,"onceDelay":0.1,"topic":"","x":140,"y":100,"wires":[["68503e970850910b"]]},{"id":"4632ee250586c413","type":"debug","z":"70e9a03d9a03cec2","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1180,"y":180,"wires":[]},{"id":"2dc4627e3e9f8fbb","type":"change","z":"70e9a03d9a03cec2","name":"Calc brightness & colour","rules":[{"t":"set","p":"payload","pt":"msg","to":"(\t    $mins:=data.attributes.minutes;\t    $color:=[2200, 4000];\t    $bright:=[0, 100];\t    \t    $cdif:=$color[1]-$color[0];\t    $bdif:=$bright[1]-$bright[0];\t\t    [0..$mins].(\t        $inc:=$/$mins;\t        {\"minute\": $,\t        \"brightness\": $ceil($bright[0] + $bdif*$inc),\t        \"colorTemp\": $round($color[0] + $cdif*$inc,-1)}\t    )\t)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":630,"y":180,"wires":[["9613d4edbc3b659a"]]},{"id":"9613d4edbc3b659a","type":"split","z":"70e9a03d9a03cec2","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","property":"payload","x":830,"y":180,"wires":[["572df05291613702"]]},{"id":"572df05291613702","type":"delay","z":"70e9a03d9a03cec2","name":"","pauseType":"rate","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"","rateUnits":"minute","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":1010,"y":180,"wires":[["4632ee250586c413"]]},{"id":"4e275837cbecee7c","type":"inject","z":"70e9a03d9a03cec2","name":"Reset","props":[{"p":"reset","v":"true","vt":"bool"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":1010,"y":140,"wires":[["572df05291613702"]]},{"id":"dcf2ab61317a88a4","type":"ha-time","z":"70e9a03d9a03cec2","name":"Dawn Trigger","server":"","version":4,"exposeAsEntityConfig":"","entityId":"sensor.my_dawn_start","property":"state","offset":"0","offsetType":"num","offsetUnits":"minutes","randomOffset":false,"repeatDaily":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"},{"property":"topic","propertyType":"msg","value":"","valueType":"triggerId"}],"sunday":true,"monday":true,"tuesday":true,"wednesday":true,"thursday":true,"friday":true,"saturday":true,"ignorePastDate":true,"x":410,"y":180,"wires":[["2dc4627e3e9f8fbb"]]},{"id":"433c562d8fabfe91","type":"ha-sensor","z":"70e9a03d9a03cec2","name":"My Dawn TS","entityConfig":"73da203c223e3a60","version":0,"state":"$fromMillis(payload.dawn)","stateType":"jsonata","attributes":[{"property":"start","value":"payload.start","valueType":"msg"},{"property":"minutes","value":"payload.mins","valueType":"msg"},{"property":"unixms","value":"payload.dawn","valueType":"msg"}],"inputOverride":"allow","outputProperties":[],"x":530,"y":100,"wires":[[]],"server":""},{"id":"73da203c223e3a60","type":"ha-entity-config","server":"","deviceConfig":"","name":"SC My Dawn","version":6,"entityType":"sensor","haConfig":[{"property":"name","value":"My Dawn Start"},{"property":"icon","value":""},{"property":"entity_picture","value":""},{"property":"entity_category","value":""},{"property":"device_class","value":""},{"property":"unit_of_measurement","value":""},{"property":"state_class","value":""}],"resend":false,"debugEnabled":false}]

Here is the flow in testing - I added extra hours to the returned ‘next dawn’ as this was now in the past…

2 Likes

Easy mode: Use Philips Hue lights, they have a built-in Sunrise effect :stuck_out_tongue:

1 Like

I know you said you want to code this yourself, but have you tried this node?

1 Like

Looks great, I didn’t know it existed… Thank you. I’ll see if anyone has any more ideas. I’d love to hear them. I love and want to do most things with Node Red.