Circadian/Adaptive Lighting NodeRed

Hello all

I would like to share with you my implementation of circadian/adaptive lighting. It’s done with the use of NodeRed and some custom NR nodes.

The article consists of several parts:

  1. Motivation
  2. Circadian implementation
  3. Feeding lights implementation
  4. Maximum light mode implementation

Please note, while Circadian implementation is pretty universal, the way of controlling lights described in points #3 and #4 is strictly dependent on used components. Article considers using Shelly Dimmer and RGBW2 devices controlled over MQTT.

But of course it can be implemented for other devices.

If you are interested in implementation only, regardless of the reasons which pushed me to develop yet another approach for circadian lighting, you can skip the first part and jump to next post (when ready)

Motivation

Initially excited by a circadian lighting custom component I hit some flaws I couldn’t live with. At that time a new official component Adaptive Lighting was added to HA. I hoped for a more sophisticated and more configurable solution. Unfortunately I found AL just copied the shortcomings of its precursor.

Scientific divagations weren’t my first driver, those came somehow later while working on a solution. It all however started with the fact that both mentioned solutions turn lights to higher brightness yet during the night. I assume most people don’t care because they sleep at that time. But for someone who has to wake up at that time (ie to feed a child) - it must be disturbing. In fact, if your sleep must be interrupted, you don’t want to be shocked by an unnecessarily bright light.

Here is how brightness and warmth changes in the Circadian Lighting component (snapshot on 18th Jan 2021)

The fact that both components increase brightness starting from around midnight (not sure if it is midnight or nadir, today both are very close to each other) is far from what I would expect. Besides the fact it makes lights too intense, it makes it impossible to simulate a sunrise. Such a popular feature in some circles (Philips Hue). Gradual light intensity rise-up during the morning is key-feature IMO. Not because it’s kewl. Because it’s useful.

Let’s break down the warmth.
In current implementation of Circadian and Adaptive lighting components we can observe warmth starts to move at about 8am. In reality this is the time when the transition actually ends reaching blue shades.

In the real world, light turns into blue after golden hour. Rays of The Sun makes light a bit warmer (which depends on multiple factors like azimuth), but diffused light remains blue.

The integrations reach most blue shade at about noon and then decrease immediately after that. Again, there is no reason for that - light should remain bluish till the start of sunset.

The image above shows sun phases during the morning. Warm, red-ish light is expected when the Sun is between 0 and 6 degree above the horizon. You can expect up to 3500K at this phase. Before that, there is no much light but it’s obviously blue. After that - the light is blue too. During a day, the warmth of diffused light can reach 10k K.

I’ve seen around some complaints about colour not matching the real world lighting… I guess letting warmth change gradually the whole day is probably the root cause.

I posted those findings on our forum twice (for both mentioned components) but it seemed it didn’t get traction so I decided to look around how I can do it myself.

Other considerations

I’m not going for strictly scientific implementation. My goal is to follow day phases, and be able to modify light parameters to my daily cycle. For example currently in Europe we experience darkness at 5pm while we stay operating till midnight or even longer. I expect some dimming of light intensity after sunset but it must remain on a usable level until we go to sleep.

I also changed the approach of achieving night lighting. In my opinion, night lighting should be provided by circadian lighting itself. Not by explicit switches. it obvious, the system should allow to be forced, in order to turn chosen lights to maximum brightness when needed.

Last but not least, this is not the final feature-full solution. For sure it’s not a closed solution dependent on the original developer. Definitively it doesn’t support all possible light configurations. The project provides ready to use pieces and ideas you can use in you NR automatons.

10 Likes

Implementation

Because I like NodeRed, I decided to look around for the NodeRed based solution. I was ready to write the whole day cycle on my own. But started google first. I found a circadian node but it doesn’t report the data I expected. Also I needed something to “draw” the curve of light intensity and warmth.

Finally I have found two great NodeRed contributions:

SunPosition - it contains a lot of useful nodes, incl very complex roller-shutter/blinds control node taking into account a lot of sun attributes, incl azimuth. Initially I thought to use that somehow but I found it too complex to adapt quickly.

For this project I found sun-position node to be most useful. It provides all Sun events for the current day in a single bulk of data.
Note, that nodes from this package require your location to be configured to work. This part of configuration is not being exported with the code due to privacy concerns. Once profile is created, it’s ready to use with every node from the package. You have to do that on your own after importing my code provided below.

SplineCurve - I would love a bezier curve more, but spline does the job. Just to spoil it, you can configure it by just drawing a curve (almost)

Interestingly, documentation of spline-curve contains a picture of Circadian implementation. Unfortunately I found no source code for that. But it gave me an exact idea of how it can be done.

Day cycle

I started checking data generated by the sun-position node. I copied single bulk of data into a table:

It helped me a lot in taking decisions on how to split a day down into configurable parts. I tried two approaches, finally ending up with night, morning, day and evening. But in fact it can be split into as many parts as you want. It could be even a single part but we need to be in sync with sun events, right?

On top of that, one can add events based on fixed time. For example using midnight or other arbitrary point(s). TBH the longer I think about it the more confident I am about splitting night into 2 parts: before and after midnight. It’s because of scaling time and moving sunrise and sunset events during the year.

Anyway, currently I proposed 4 ranges:

  • night: from astronomicalDusk to astronomicalDawn

  • morning: from astronomicalDawn to goldenHourDawnEnd

  • day: from goldenHourDawnEnd to sunsetStart

  • evening: from sunsetStart to astronomicalDusk

The whole magic is done by basically two nodes with help of switch one. And you know what? Its hard to believe how simple it can be:

[{"id":"26c6c4d5.3a01e4","type":"subflow","name":"Sub Circadian NR","info":"","category":"","in":[{"x":60,"y":160,"wires":[{"id":"7d4ca80d.e23468"}]}],"out":[{"x":1120,"y":140,"wires":[{"id":"e64dbfa.021204","port":0}]},{"x":1120,"y":340,"wires":[{"id":"d9f0035a.28956","port":0}]},{"x":500,"y":500,"wires":[{"id":"667d6dd.334db94","port":0}]}],"env":[],"color":"#DDAA99"},{"id":"dc97f68a.35ea2","type":"switch","z":"26c6c4d5.3a01e4","name":"curve switch","property":"payload.times_of_day.active.name","propertyType":"msg","rules":[{"t":"eq","v":"night","vt":"str"},{"t":"eq","v":"morning","vt":"str"},{"t":"eq","v":"day","vt":"str"},{"t":"eq","v":"evening","vt":"str"}],"checkall":"true","repair":false,"outputs":4,"x":510,"y":240,"wires":[["ce91189c.8bc7d8","bd8db343.5462f8"],["ad9966f3.3fced","4db18fbe.9ffb6"],["c2d43d53.d7ddf8","838233f4.7ffed"],["eef1dc11.39342","2e9ef8a9.d7fd88"]]},{"id":"ce91189c.8bc7d8","type":"spline-curve","z":"26c6c4d5.3a01e4","name":"night curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.3},{"x":0.333,"y":0.037},{"x":0.477,"y":0},{"x":0.783,"y":0},{"x":1,"y":0}],"x":750,"y":80,"wires":[["e64dbfa.021204"]]},{"id":"ad9966f3.3fced","type":"spline-curve","z":"26c6c4d5.3a01e4","name":"morning curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.01},{"x":0.132,"y":0.01},{"x":0.269,"y":0.027},{"x":0.449,"y":0.124},{"x":0.676,"y":0.334},{"x":1,"y":0.75}],"x":760,"y":120,"wires":[["e64dbfa.021204"]]},{"id":"eef1dc11.39342","type":"spline-curve","z":"26c6c4d5.3a01e4","name":"evening curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.85},{"x":0.036,"y":0.78},{"x":0.176,"y":0.63},{"x":1,"y":0.3}],"x":760,"y":200,"wires":[["e64dbfa.021204"]]},{"id":"c2d43d53.d7ddf8","type":"spline-curve","z":"26c6c4d5.3a01e4","name":"daylight curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.744},{"x":0.03,"y":0.838},{"x":0.097,"y":0.941},{"x":0.183,"y":1},{"x":0.86,"y":0.995},{"x":0.943,"y":0.931},{"x":1,"y":0.85}],"x":760,"y":160,"wires":[["e64dbfa.021204"]]},{"id":"bd8db343.5462f8","type":"spline-curve","z":"26c6c4d5.3a01e4","name":"night curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.5},{"x":0.067,"y":0.368},{"x":0.133,"y":0.265},{"x":0.27,"y":0.171},{"x":0.333,"y":0.16},{"x":0.506,"y":0.124},{"x":0.606,"y":0}],"x":750,"y":280,"wires":[["d9f0035a.28956"]]},{"id":"4db18fbe.9ffb6","type":"spline-curve","z":"26c6c4d5.3a01e4","name":"morning curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0},{"x":0.42,"y":0.018},{"x":0.54,"y":0.051},{"x":0.587,"y":0.101},{"x":0.817,"y":0.675},{"x":1,"y":0.85}],"x":760,"y":320,"wires":[["d9f0035a.28956"]]},{"id":"2e9ef8a9.d7fd88","type":"spline-curve","z":"26c6c4d5.3a01e4","name":"evening curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.85},{"x":0.787,"y":0.598},{"x":1,"y":0.5}],"x":760,"y":400,"wires":[["d9f0035a.28956"]]},{"id":"838233f4.7ffed","type":"spline-curve","z":"26c6c4d5.3a01e4","name":"daylight curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.85},{"x":0.123,"y":0.927},{"x":0.5,"y":1},{"x":0.92,"y":0.904},{"x":1,"y":0.85}],"x":760,"y":360,"wires":[["d9f0035a.28956"]]},{"id":"667d6dd.334db94","type":"function","z":"26c6c4d5.3a01e4","name":"Times of Day","func":"var newmsg = JSON.parse(JSON.stringify(msg));\nnewmsg.payload.times_of_day = {};\n\nprocessTimeOfDay = function(nme, obj, name_start, name_end) {\n    var ret = {};\n    ret.name  = nme;\n    ret.start = obj.times[name_start].ts;\n    ret.end   = obj.times[name_end].ts;\n    ret.perc  = (obj.ts - ret.start) / (ret.end - ret.start);\n    \n    \n    // to cover range starting before midnight and ending after it.\n    if (ret.start > ret.end)\n    {\n        var a;\n\n        // before midnight\n        if (ret.start < obj.ts) \n        {\n            a = new Date(ret.end);\n            a.setUTCDate(a.getUTCDate() + 1);\n            ret.end = a.getTime();\n             \n             \n        }\n        else if (ret.end > obj.ts) \n        {\n            a = new Date(ret.start);\n            a.setUTCDate(a.getUTCDate() - 1);\n            ret.start = a.getTime();\n\n        }\n\n    }\n    \n    ret.perc  = (obj.ts - ret.start) / (ret.end - ret.start);\n    \n    obj.times_of_day[nme] = ret;\n    if (ret.start <= obj.ts && obj.ts < ret.end ) obj.times_of_day.active = ret;\n}\n\n\nprocessTimeOfDay('night', newmsg.payload, \"astronomicalDusk\", \"astronomicalDawn\");\nprocessTimeOfDay('morning', newmsg.payload, \"astronomicalDawn\", \"goldenHourDawnEnd\");\nprocessTimeOfDay('day', newmsg.payload, \"goldenHourDawnEnd\", \"sunsetStart\");\nprocessTimeOfDay('evening', newmsg.payload, \"sunsetStart\", \"astronomicalDusk\");\n\n\nreturn newmsg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":290,"y":240,"wires":[["dc97f68a.35ea2"]]},{"id":"e64dbfa.021204","type":"range","z":"26c6c4d5.3a01e4","minin":"0","maxin":"1","minout":"1","maxout":"100","action":"scale","round":true,"property":"payload","name":"","x":980,"y":140,"wires":[[]]},{"id":"d9f0035a.28956","type":"range","z":"26c6c4d5.3a01e4","minin":"0","maxin":"1","minout":"2000","maxout":"5500","action":"scale","round":true,"property":"payload","name":"","x":990,"y":340,"wires":[[]]},{"id":"7d4ca80d.e23468","type":"sun-position","z":"26c6c4d5.3a01e4","name":"","positionConfig":"26f98e1b.bd1812","rules":[],"onlyOnChange":"true","topic":"","outputs":1,"start":"","startType":"none","startOffset":0,"startOffsetType":"num","startOffsetMultiplier":60000,"end":"","endType":"none","endOffset":0,"endOffsetType":"num","endOffsetMultiplier":60000,"x":190,"y":160,"wires":[["667d6dd.334db94"]]},{"id":"26f98e1b.bd1812","type":"position-config","name":"","isValide":"true","longitude":"0","latitude":"0","angleType":"deg","timeZoneOffset":"99","timeZoneDST":"0","stateTimeFormat":"3","stateDateFormat":"12"},{"id":"7c36779e.82d778","type":"subflow:26c6c4d5.3a01e4","z":"98a83ef1.1c6e2","name":"","env":[],"x":350,"y":180,"wires":[["f5dfa977.44ef5"],["2dee4f11.beb6e"],["73ce5946.a872a"]],"outputLabels":["Bigthness [%]","Warmth [K]","Debug"],"icon":"node-red-node-suncalc/sun.png"}]

Sun-postion node is triggered by any input message. The function node TimesofDay creates mentioned time ranges from data received from sun-position node. It also selects current range and calculates the percentage of elapsed time for it.

The switch passes data retrieved from the function node to a matching SplineCurve node.

Because I wanted different curves for light intensity and warmth, I created 2 sets of curves.

The spline-curve node takes decimal values from range 0-1 outputting value based on the created curve (also range 0-1).

The tricky part is to define nice slopes. Because particular parts of the day have different lengths, the curve in the editor mustn’t look like generated result. The result might be compressed or expanded, affecting steepness of the slope. Here is how particular parts looks in editor (brightness):


See the end of this post to see how it does translate into Brightness curve.

The result generated by curve nodes is scaled up to required range: 1-100 for brightness and 2000-5500 for warmth. Note that I chose 1 as the lower boundary for brightness. It’s because I don’t want to turn the light off. If I wanted I would choose a switch for that.

It’s easier to create curves hitting 0 value rather than 0.01. Depending on needs it might be done the opposite way of course.

As you can see from the image I implemented it as a subnode. It’s not mandatory approach, but I wanted to try this feature. It significantly reduces clutter. Also it allows to reuse this part of code just like another node (see image below).

I also wrote a simulator - something which allows me to generate whole day cycle curves during several minutes. I will get back to it later.

And here is how it is connected to sensors. Inject node just triggers the subnode.

[{"id":"26c6c4d5.3a01e4","type":"subflow","name":"Sub Circadian NR","info":"","category":"","in":[{"x":60,"y":160,"wires":[{"id":"7d4ca80d.e23468"}]}],"out":[{"x":1120,"y":140,"wires":[{"id":"e64dbfa.021204","port":0}]},{"x":1120,"y":340,"wires":[{"id":"d9f0035a.28956","port":0}]},{"x":500,"y":500,"wires":[{"id":"667d6dd.334db94","port":0}]}],"env":[],"color":"#DDAA99"},{"id":"dc97f68a.35ea2","type":"switch","z":"26c6c4d5.3a01e4","name":"curve switch","property":"payload.times_of_day.active.name","propertyType":"msg","rules":[{"t":"eq","v":"night","vt":"str"},{"t":"eq","v":"morning","vt":"str"},{"t":"eq","v":"day","vt":"str"},{"t":"eq","v":"evening","vt":"str"}],"checkall":"true","repair":false,"outputs":4,"x":510,"y":240,"wires":[["ce91189c.8bc7d8","bd8db343.5462f8"],["ad9966f3.3fced","4db18fbe.9ffb6"],["c2d43d53.d7ddf8","838233f4.7ffed"],["eef1dc11.39342","2e9ef8a9.d7fd88"]]},{"id":"ce91189c.8bc7d8","type":"spline-curve","z":"26c6c4d5.3a01e4","name":"night curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.3},{"x":0.333,"y":0.037},{"x":0.477,"y":0},{"x":0.783,"y":0},{"x":1,"y":0}],"x":750,"y":80,"wires":[["e64dbfa.021204"]]},{"id":"ad9966f3.3fced","type":"spline-curve","z":"26c6c4d5.3a01e4","name":"morning curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.01},{"x":0.132,"y":0.01},{"x":0.269,"y":0.027},{"x":0.449,"y":0.124},{"x":0.676,"y":0.334},{"x":1,"y":0.75}],"x":760,"y":120,"wires":[["e64dbfa.021204"]]},{"id":"eef1dc11.39342","type":"spline-curve","z":"26c6c4d5.3a01e4","name":"evening curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.85},{"x":0.036,"y":0.78},{"x":0.176,"y":0.63},{"x":1,"y":0.3}],"x":760,"y":200,"wires":[["e64dbfa.021204"]]},{"id":"c2d43d53.d7ddf8","type":"spline-curve","z":"26c6c4d5.3a01e4","name":"daylight curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.744},{"x":0.03,"y":0.838},{"x":0.097,"y":0.941},{"x":0.183,"y":1},{"x":0.86,"y":0.995},{"x":0.943,"y":0.931},{"x":1,"y":0.85}],"x":760,"y":160,"wires":[["e64dbfa.021204"]]},{"id":"bd8db343.5462f8","type":"spline-curve","z":"26c6c4d5.3a01e4","name":"night curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.5},{"x":0.067,"y":0.368},{"x":0.133,"y":0.265},{"x":0.27,"y":0.171},{"x":0.333,"y":0.16},{"x":0.506,"y":0.124},{"x":0.606,"y":0}],"x":750,"y":280,"wires":[["d9f0035a.28956"]]},{"id":"4db18fbe.9ffb6","type":"spline-curve","z":"26c6c4d5.3a01e4","name":"morning curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0},{"x":0.42,"y":0.018},{"x":0.54,"y":0.051},{"x":0.587,"y":0.101},{"x":0.817,"y":0.675},{"x":1,"y":0.85}],"x":760,"y":320,"wires":[["d9f0035a.28956"]]},{"id":"2e9ef8a9.d7fd88","type":"spline-curve","z":"26c6c4d5.3a01e4","name":"evening curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.85},{"x":0.787,"y":0.598},{"x":1,"y":0.5}],"x":760,"y":400,"wires":[["d9f0035a.28956"]]},{"id":"838233f4.7ffed","type":"spline-curve","z":"26c6c4d5.3a01e4","name":"daylight curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.85},{"x":0.123,"y":0.927},{"x":0.5,"y":1},{"x":0.92,"y":0.904},{"x":1,"y":0.85}],"x":760,"y":360,"wires":[["d9f0035a.28956"]]},{"id":"667d6dd.334db94","type":"function","z":"26c6c4d5.3a01e4","name":"Times of Day","func":"var newmsg = JSON.parse(JSON.stringify(msg));\nnewmsg.payload.times_of_day = {};\n\nprocessTimeOfDay = function(nme, obj, name_start, name_end) {\n    var ret = {};\n    ret.name  = nme;\n    ret.start = obj.times[name_start].ts;\n    ret.end   = obj.times[name_end].ts;\n    ret.perc  = (obj.ts - ret.start) / (ret.end - ret.start);\n    \n    \n    // to cover range starting before midnight and ending after it.\n    if (ret.start > ret.end)\n    {\n        var a;\n\n        // before midnight\n        if (ret.start < obj.ts) \n        {\n            a = new Date(ret.end);\n            a.setUTCDate(a.getUTCDate() + 1);\n            ret.end = a.getTime();\n             \n             \n        }\n        else if (ret.end > obj.ts) \n        {\n            a = new Date(ret.start);\n            a.setUTCDate(a.getUTCDate() - 1);\n            ret.start = a.getTime();\n\n        }\n\n    }\n    \n    ret.perc  = (obj.ts - ret.start) / (ret.end - ret.start);\n    \n    obj.times_of_day[nme] = ret;\n    if (ret.start <= obj.ts && obj.ts < ret.end ) obj.times_of_day.active = ret;\n}\n\n\nprocessTimeOfDay('night', newmsg.payload, \"astronomicalDusk\", \"astronomicalDawn\");\nprocessTimeOfDay('morning', newmsg.payload, \"astronomicalDawn\", \"goldenHourDawnEnd\");\nprocessTimeOfDay('day', newmsg.payload, \"goldenHourDawnEnd\", \"sunsetStart\");\nprocessTimeOfDay('evening', newmsg.payload, \"sunsetStart\", \"astronomicalDusk\");\n\n\nreturn newmsg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":290,"y":240,"wires":[["dc97f68a.35ea2"]]},{"id":"e64dbfa.021204","type":"range","z":"26c6c4d5.3a01e4","minin":"0","maxin":"1","minout":"1","maxout":"100","action":"scale","round":true,"property":"payload","name":"","x":980,"y":140,"wires":[[]]},{"id":"d9f0035a.28956","type":"range","z":"26c6c4d5.3a01e4","minin":"0","maxin":"1","minout":"2000","maxout":"5500","action":"scale","round":true,"property":"payload","name":"","x":990,"y":340,"wires":[[]]},{"id":"7d4ca80d.e23468","type":"sun-position","z":"26c6c4d5.3a01e4","name":"","positionConfig":"26f98e1b.bd1812","rules":[],"onlyOnChange":"true","topic":"","outputs":1,"start":"","startType":"none","startOffset":0,"startOffsetType":"num","startOffsetMultiplier":60000,"end":"","endType":"none","endOffset":0,"endOffsetType":"num","endOffsetMultiplier":60000,"x":190,"y":160,"wires":[["667d6dd.334db94"]]},{"id":"26f98e1b.bd1812","type":"position-config","name":"","isValide":"true","longitude":"0","latitude":"0","angleType":"deg","timeZoneOffset":"99","timeZoneDST":"0","stateTimeFormat":"3","stateDateFormat":"12"},{"id":"bfbe267f.ea05f8","type":"inject","z":"98a83ef1.1c6e2","name":"","props":[{"p":"payload"}],"repeat":"60","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":170,"y":480,"wires":[["ecf00c0c.9acbc"]]},{"id":"ecf00c0c.9acbc","type":"subflow:26c6c4d5.3a01e4","z":"98a83ef1.1c6e2","name":"","env":[],"x":390,"y":480,"wires":[["ef0a208b.3ff8e8"],["f5e1d08b.a8cd58"],[]],"outputLabels":["Bigthness [%]","Warmth [K]","Debug"],"icon":"node-red-node-suncalc/sun.png"},{"id":"ef0a208b.3ff8e8","type":"ha-entity","z":"98a83ef1.1c6e2","name":"Gain [%]","server":"6cdd0bc8.b8e434","version":1,"debugenabled":false,"outputs":1,"entityType":"sensor","config":[{"property":"name","value":"nr_circadian_gain"},{"property":"device_class","value":""},{"property":"icon","value":"mdi:brightness-percent"},{"property":"unit_of_measurement","value":"%"}],"state":"payload","stateType":"msg","attributes":[],"resend":true,"outputLocation":"","outputLocationType":"none","inputOverride":"allow","outputOnStateChange":false,"outputPayload":"","outputPayloadType":"str","x":620,"y":440,"wires":[["fd265ff2.b898f8"]]},{"id":"f5e1d08b.a8cd58","type":"ha-entity","z":"98a83ef1.1c6e2","name":"Warmth [K]","server":"6cdd0bc8.b8e434","version":1,"debugenabled":false,"outputs":1,"entityType":"sensor","config":[{"property":"name","value":"nr_circadian_warmth"},{"property":"device_class","value":""},{"property":"icon","value":"mdi:brightness-6"},{"property":"unit_of_measurement","value":"K"}],"state":"payload","stateType":"msg","attributes":[],"resend":true,"outputLocation":"payload","outputLocationType":"msg","inputOverride":"allow","outputOnStateChange":false,"outputPayload":"","outputPayloadType":"str","x":630,"y":520,"wires":[["13779c9.87835e3"]]},{"id":"6cdd0bc8.b8e434","type":"server","name":"Home Assistant"}]

The code of this part seems to be obvious. Period of triggering subnode is up to your preferences. I have it set to 1 min. But 5 or 10 minnutes is good enough too IMO. Especially if your lights can apply smooth transitions to requested changes.

Presented earlier brightness curves translate to the result shown on picture below. It doesn’t look as fancy as regular sinuses. But it really doesn’t matter since it works well. At the end it’s not a graphs competition :wink:

As you can see I achieved following things:

  1. Brightness during most of night (especially its second part) is dimmed to minimum
  2. Brightness and warmth both increases fast during sunrise to almost maximum values
  3. Brightness and warmth starts decreasing with sunset, but the process is slow providing enough proper light for normal evening live.
  4. You can see drop of warmth from 2500K to 2000K after midnight. It’s because we found yet warmer light during this part of night more comfortable

Here is mini-graph code for completion:

type: 'custom:mini-graph-card'
name: Circadian NR
show:
  icon: false
  labels: false
font_size: 80
font_size_header: 10
hour24: true
hours_to_show: 24
points_per_hour: 60
line_width: 2
lower_bound: 1
upper_bound: 100
smoothing: true
entities:
  - entity: sensor.nr_circadian_gain
    name: Brightness NR
  - entity: sensor.nr_circadian_warmth
    name: Warmth NR
    show_state: true
    y_axis: secondary
2 Likes

reserved for feeding lights implementation

reserved for maximum light mode implementation

I’ve finally got around to looking at this. I’ve imported the second json flow but I’ve no idea how to implement it! I’ve only recently started with Node-RED and have done a total of 2 flows.

1st:
image

2nd:

Thanks!

and, how do I add these to HA’s Node-RED add-on?


I consoled into the node red container and ran

npm install node-red-contrib-spline-curve
npm install node-red-contrib-sun-position

but still see this:

Installed:

npm install node-red-contrib-sun-position

and added position-config to palette.

Now only have error:

[position-config:26f98e1b.bd1812] Error: Latitude and Longitude is wrong!

you need to click on suposition node and configure your location.
the location is not being exported due to privacy concerns (see docs of that node)

btw I can see you are jnstalling extentions manually? I did it from NodeRed pallete

All this is new! I followed your links and ran the npm commands. Did not know what a “palette” was. I did find it and was able to get rid of the final error.

So for sunposition, do I have to edit it each time I use it in a flow or is there a way to edit the node permanently?

image

What about this?

Afaik it stores it in kind of a profile. All nodes from this package uses the same location profile. If it is set by default for each node - I don’t remember. Easy to test though.

Considering custom integration you are asking for things which are not related to this project but to configuration of NR in HA environment. You need to have installed NodeRed companion component from HACS
obrazek
as well as Home assistant socket extension to NR, added from palette:
obrazek

Wow, how did this stay in the dark for 2 months. Thanks for all your thoughts and effort put into this!
Now it’s even harder to choose which implementation to use! Changed to adaptive lightning and enjoyed the new options (detect changes to stop transition and so on) but I also felt, that values are off some times.
Gonna check this one out when I find time!

Ok, I’ve installed NR companion component in HACS and the new NR integration in HA.
and I have node-red-contrib-home-assistant-websocket installed
image
I’m having these recurring messages in NR log:

[ 'payload' ]
26 Mar 09:24:17 - [error] [ha-entity:Gain [%]] Sensor update attempted without connection to server.
26 Mar 09:24:17 - [error] [ha-entity:Warmth [K]] Sensor update attempted without connection to server.

Have you followed installation instructions of companion component? I mean have you added integration in HA/Configuration/Integrations?

Yes.

image

Resolved.

26 Mar 11:24:20 - [info] Started flows
26 Mar 11:24:25 - [info] [server:Home Assistant] Connecting to http://supervisor/core
26 Mar 11:24:25 - [info] [server:Home Assistant] Connecting to http://supervisor/core
26 Mar 11:24:25 - [info] [server:Home Assistant] Connected to http://supervisor/core
26 Mar 11:24:25 - [info] [server:Home Assistant] Connected to http://supervisor/core
26 Mar 11:24:25 - [debug] [server:Home Assistant] States Loaded
26 Mar 11:24:25 - [debug] [server:Home Assistant] Integration: loaded
26 Mar 11:24:25 - [debug] [server:Home Assistant] HA State: running
26 Mar 11:24:25 - [debug] [server:Home Assistant] Services Loaded
26 Mar 11:24:25 - [debug] [server:Home Assistant] States Loaded
26 Mar 11:24:25 - [debug] [server:Home Assistant] Services Loaded
26 Mar 11:24:25 - [debug] [server:Home Assistant] Integration: loaded
26 Mar 11:24:25 - [debug] [ha-entity:Gain [%]] Registering sensor with HA
26 Mar 11:24:25 - [debug] [ha-entity:Warmth [K]] Registering sensor with HA
26 Mar 11:24:25 - [debug] [server:Home Assistant] HA State: running

What I did:
After reading zachowj/node-red-contrib-home-assistant-websocket: Node-RED integration with Home Assistant Core (github.com) I found:
image
image
and had to update the 2nd Home Assistant configuration node by checking the option I use the Home Assistant Add-on
image

There are a few things you have to know:

  1. setting brightness and warmth using service call is only possible when turning on (and maybe toggle). This is limitation of HA. So you cannot send color/warmth when light is turned off. It means it must be send imediatelly when turning such light on (using HA as well as using switch). It requires more complex flow I’m not able to provide right now since I don’t use service calls.
  2. what do you send to service call might depends on device which receive the data. For example some devices expects kelvins while others mireds. Some accepts brightness in scale 0-255, others in percentage (brightness_pct: 0-100)I don’t know if HA does needed conversions on the fly.

here are two methods how to pass data to light: to service call and to mqtt to feed Shelly lights.
Both methods are pretty similar. In function nodes, I prepare partial data (warmth and brightness) to join them together into single message using join node. Such message is being send to service call node or to mqtt.

in case of service call, the solution would be a bit different. There should be intermediate routine which:

  • would prevent sending message to turn_on service call when light must remain turned off
  • would send light data (brightness and warmth) immediately when light is turned on.
    I could think about how to program it in NR is kind of flexible way. But currently have no ready to use solution. I’m afraid the solution might depends on a way how one uses/controls devices

Sending data to service call which turns on hue light.
What light attributes are allowed to be set is described in service call node description (if light domain is selected). Those data are prepared in function nodes.

[{"id":"593741bd.173c78","type":"api-call-service","z":"98a83ef1.1c6e2","name":"","server":"6cdd0bc8.b8e434","version":1,"debugenabled":true,"service_domain":"light","service":"turn_on","entityId":"light.hue_strip_1","data":"","dataType":"jsonata","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":1230,"y":480,"wires":[[]]},{"id":"478ff358.47fe54","type":"function","z":"98a83ef1.1c6e2","name":"Brightness_pct","func":"var brightness = msg.payload;\nvar newmsg = \n{\n    payload: {\n        data: {\n            brightness_pct: Math.round(brightness)\n        }\n    }\n    \n}\nreturn newmsg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":840,"y":440,"wires":[["abf62555.fead48"]]},{"id":"1f4839ae.37a826","type":"function","z":"98a83ef1.1c6e2","name":"WW CW","func":"var colortemp = msg.payload;\nvar newmsg = \n{\n    payload: {\n        data: {\n            kelvin: colortemp\n        }\n    }\n    \n}\nreturn newmsg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":820,"y":520,"wires":[["abf62555.fead48"]]},{"id":"abf62555.fead48","type":"join","z":"98a83ef1.1c6e2","name":"","mode":"custom","build":"merged","property":"payload.data","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":true,"timeout":"0","count":"1","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":1020,"y":480,"wires":[["593741bd.173c78"]]},{"id":"6cdd0bc8.b8e434","type":"server","name":"Home Assistant"}]

Sendiong data to mqtt. Note this operation is not related to turn on. It just sends light attributes to mqtt topic of Shelly device. Shelly accepts it even if light is turned off. It’s a feature specific to Shelly devices.

[{"id":"140856f7.304c01","type":"function","z":"98a83ef1.1c6e2","name":"Gain","func":"var brightness = msg.payload;\nvar newmsg = \n{\n    payload: {\n        gain: Math.round(brightness)\n    }\n    \n}\nreturn newmsg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":790,"y":200,"wires":[["8ae60ddb.9494"]]},{"id":"15af478b.e8a9c8","type":"function","z":"98a83ef1.1c6e2","name":"WW CW","func":"var colortemp = msg.payload.state;\nvar newmsg = \n{\n    payload: {\n        red: Math.round((1000000 / colortemp - 153) * 0.734870317),\n        green: Math.round((500 - (1000000 / colortemp)) * 0.734870317),\n        blue: 0 /*,\n        gain: Math.round(100 * (rgb[0] + rgb[1] + rgb[2]) / 765)*/\n    }\n    \n}\nreturn newmsg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":800,"y":280,"wires":[["8ae60ddb.9494"]]},{"id":"8ae60ddb.9494","type":"join","z":"98a83ef1.1c6e2","name":"","mode":"custom","build":"merged","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":true,"timeout":"0","count":"1","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":970,"y":240,"wires":[["1a7aa105.1b7207","a191d3de.555048","dfd11efd.9dfc7","7df35fa.98b24a","31faf075.a69f18","a7a0d45f.d9a96"]]},{"id":"1a7aa105.1b7207","type":"mqtt out","z":"98a83ef1.1c6e2","name":"light-hall1","topic":"shellies/light-hall1/color/0/set","qos":"1","retain":"","broker":"7d8a84d.aa3f57c","x":1170,"y":240,"wires":[]},{"id":"7d8a84d.aa3f57c","type":"mqtt-broker","name":"","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":""}]

Thank you. After some research I had come up with this:

Now I will add join and begin thinking about the initial light turned-on my motion and how to get %/K into it…

I think I got it…


Nope.

This seems to do it…

[{"id":"84cd7c8a.87e12","type":"tab","label":"Office Motion","disabled":false,"info":""},{"id":"7575c78d.594b18","type":"server-state-changed","z":"84cd7c8a.87e12","name":"Office Motion","server":"c879ac61.99cd1","version":1,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"binary_sensor.office_motion","entityidfiltertype":"exact","outputinitially":true,"state_type":"str","haltifstate":"on","halt_if_type":"str","halt_if_compare":"is","outputs":2,"output_only_on_state_change":true,"for":0,"forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"x":70,"y":340,"wires":[["fb69f1b4.33664"],["60bda597.2ab52c"]]},{"id":"22e89a35.d0c2a6","type":"server-state-changed","z":"84cd7c8a.87e12","name":"My PC is Active","server":"c879ac61.99cd1","version":1,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"binary_sensor.my_pc_is_active","entityidfiltertype":"exact","outputinitially":true,"state_type":"str","haltifstate":"on","halt_if_type":"str","halt_if_compare":"is","outputs":2,"output_only_on_state_change":true,"for":0,"forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"x":80,"y":220,"wires":[["fb69f1b4.33664"],["60bda597.2ab52c"]]},{"id":"a1fc2bee.e19838","type":"api-call-service","z":"84cd7c8a.87e12","name":"Turn ON","server":"c879ac61.99cd1","version":1,"debugenabled":false,"service_domain":"light","service":"turn_on","entityId":"light.office_lights","data":"","dataType":"json","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":620,"y":260,"wires":[[]]},{"id":"8422fb5d.b696b8","type":"api-call-service","z":"84cd7c8a.87e12","name":"Turn OFF","server":"c879ac61.99cd1","version":1,"debugenabled":false,"service_domain":"light","service":"turn_off","entityId":"light.office_lights","data":"","dataType":"jsonata","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":620,"y":320,"wires":[[]]},{"id":"18313c35.b29354","type":"api-current-state","z":"84cd7c8a.87e12","name":"Office lights on?","server":"c879ac61.99cd1","version":1,"outputs":2,"halt_if":"on","halt_if_type":"str","halt_if_compare":"is","override_topic":false,"entity_id":"light.office_lights","state_type":"str","state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","blockInputOverrides":false,"x":380,"y":280,"wires":[["2dcd70bc.62cf"],[]]},{"id":"2dcd70bc.62cf","type":"function","z":"84cd7c8a.87e12","name":"format gain,warmth","func":"    var newMsg =  {\n        payload: {\n            \"brightness\": {\n                \"brightness_pct\": flow.get('nr_circadian_gain')\n            },\n            \"data\": {\n                \"kelvin\": flow.get('nr_circadian_warmth')\n            }\n        }\n    }\n    return newMsg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":630,"y":200,"wires":[["a1fc2bee.e19838"]]},{"id":"c8da6ec6.d02e","type":"inject","z":"84cd7c8a.87e12","name":"every 5 minutes","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"300","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":90,"y":280,"wires":[["18313c35.b29354"]]},{"id":"fb69f1b4.33664","type":"api-current-state","z":"84cd7c8a.87e12","name":"Office lights off?","server":"c879ac61.99cd1","version":1,"outputs":2,"halt_if":"on","halt_if_type":"str","halt_if_compare":"is_not","override_topic":false,"entity_id":"light.office_lights","state_type":"str","state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","blockInputOverrides":false,"x":380,"y":200,"wires":[["2dcd70bc.62cf"],[]]},{"id":"da40f5c2.95f488","type":"comment","z":"84cd7c8a.87e12","name":"Office Lighting","info":"Motion Controlled with Circadian color, brightness adjustment","x":70,"y":140,"wires":[]},{"id":"60bda597.2ab52c","type":"trigger-state","z":"84cd7c8a.87e12","name":"Are both off?","server":"c879ac61.99cd1","exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityid":"binary_sensor.my_pc_is_active","entityidfiltertype":"exact","debugenabled":false,"constraints":[{"targetType":"this_entity","targetValue":"","propertyType":"current_state","comparatorType":"is","comparatorValueDatatype":"str","comparatorValue":"off","propertyValue":"new_state.state"},{"targetType":"entity_id","targetValue":"binary_sensor.office_motion","propertyType":"current_state","comparatorType":"is","comparatorValueDatatype":"str","comparatorValue":"off","propertyValue":"new_state.state"}],"outputs":2,"customoutputs":[],"outputinitially":true,"state_type":"str","x":370,"y":360,"wires":[["8422fb5d.b696b8"],[]]},{"id":"c879ac61.99cd1","type":"server","name":"Home Assistant","legacy":false,"addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true}]

Have no idea if your example is working :wink: Sorry had no time to check. But I can see you are polling light states. Which isn’t goo way. especially it will cause delays.

But in the meantime I prepared one proof of concepts. I found great custom NR node which helps in queuing the last known light attributes: https://flows.nodered.org/node/node-red-contrib-queue-gate
BTW those custom nodes (thousands of them) makes NR so easy to use. In most cases you don’t need invent a wheel. Just to find suitable node which likely already exists.

The queue-gate in example below is configured to store only one, most recent message. it’s fed with circadian data generated all the time. If light is turned off, the gate is being closed but messages are still being stored.
When light is turned ON,this event is being intercepted by change state node and sends “open” request to queue gate node. In response the most recently stored light attributes are being sent to the light.

Light updated by the light state:

Note, the code bellow doesn’t contain circadian part. It started from function nodes which prepare json data (the same as in examples published earlier)

[{"id":"96efe85.7f23e98","type":"server-state-changed","z":"98a83ef1.1c6e2","name":"hue_strip_1: on/off","server":"6cdd0bc8.b8e434","version":1,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"light.hue_strip_1","entityidfiltertype":"exact","outputinitially":false,"state_type":"str","haltifstate":"on|off","halt_if_type":"re","halt_if_compare":"is","outputs":2,"output_only_on_state_change":true,"for":0,"forType":"num","forUnits":"minutes","ignorePrevStateNull":false,"ignorePrevStateUnknown":false,"ignorePrevStateUnavailable":false,"ignoreCurrentStateUnknown":false,"ignoreCurrentStateUnavailable":false,"x":1170,"y":600,"wires":[["d5d28817.243c48"],[]]},{"id":"d5d28817.243c48","type":"change","z":"98a83ef1.1c6e2","name":"","rules":[{"t":"set","p":"topic","pt":"msg","to":"control","tot":"str"},{"t":"change","p":"payload","pt":"msg","from":"on","fromt":"str","to":"open","tot":"str"},{"t":"change","p":"payload","pt":"msg","from":"off","fromt":"str","to":"queue","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1170,"y":670,"wires":[["b85f90a0.00b32"]]},{"id":"b85f90a0.00b32","type":"q-gate","z":"98a83ef1.1c6e2","name":"","controlTopic":"control","defaultState":"queueing","openCmd":"open","closeCmd":"close","toggleCmd":"toggle","queueCmd":"queue","defaultCmd":"default","triggerCmd":"trigger","flushCmd":"flush","resetCmd":"reset","peekCmd":"","dropCmd":"","statusCmd":"","maxQueueLength":"1","keepNewest":true,"qToggle":false,"persist":false,"x":1170,"y":740,"wires":[["2913c215.aa181e"]]},{"id":"2913c215.aa181e","type":"api-call-service","z":"98a83ef1.1c6e2","name":"hue_strip1: update","server":"6cdd0bc8.b8e434","version":1,"debugenabled":false,"service_domain":"light","service":"turn_on","entityId":"light.hue_strip_1","data":"","dataType":"jsonata","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":1390,"y":740,"wires":[[]]},{"id":"1421226e.e2792e","type":"function","z":"98a83ef1.1c6e2","name":"Brightness_pct","func":"var brightness = msg.payload;\nvar newmsg = \n{\n    payload: {\n        data: {\n            brightness_pct: Math.round(brightness)\n        }\n    }\n    \n}\nreturn newmsg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":760,"y":710,"wires":[["34762637.eb48da"]]},{"id":"8119586f.c84e18","type":"function","z":"98a83ef1.1c6e2","name":"WW CW","func":"var colortemp = msg.payload;\nvar newmsg = \n{\n    payload: {\n        data: {\n            kelvin: colortemp\n        }\n    }\n    \n}\nreturn newmsg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":740,"y":790,"wires":[["34762637.eb48da"]]},{"id":"34762637.eb48da","type":"join","z":"98a83ef1.1c6e2","name":"","mode":"custom","build":"merged","property":"payload.data","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"0","count":"2","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":950,"y":740,"wires":[["b85f90a0.00b32"]]},{"id":"6cdd0bc8.b8e434","type":"server","name":"Home Assistant"}]

Thanks! I will have a look at that. In the meantime, I’ve been busy exploring and learning. Here’s my Circadian Lighting Flow…

[{"id":"e812678d.651e28","type":"subflow","name":"Sub Circadian NR","info":"","category":"","in":[{"x":60,"y":160,"wires":[{"id":"aa4b86d8.b17118"}]}],"out":[{"x":1120,"y":140,"wires":[{"id":"40de744.1e2ca8c","port":0}]},{"x":1120,"y":340,"wires":[{"id":"c26bdcca.eb483","port":0}]},{"x":500,"y":500,"wires":[{"id":"a330b219.92e4e","port":0}]}],"env":[],"color":"#DDAA99"},{"id":"5de89261.4fb97c","type":"switch","z":"e812678d.651e28","name":"curve switch","property":"payload.times_of_day.active.name","propertyType":"msg","rules":[{"t":"eq","v":"night","vt":"str"},{"t":"eq","v":"morning","vt":"str"},{"t":"eq","v":"day","vt":"str"},{"t":"eq","v":"evening","vt":"str"}],"checkall":"true","repair":false,"outputs":4,"x":510,"y":240,"wires":[["ba17c1e6.e6f58","840431b7.eeefc"],["e5bb5a04.a33948","d4d99259.d0d7e"],["7c0f0774.90adf8","6ac1af7.dddb15"],["23b260f9.19f78","a907f5f4.0bf488"]]},{"id":"ba17c1e6.e6f58","type":"spline-curve","z":"e812678d.651e28","name":"night curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.3},{"x":0.333,"y":0.037},{"x":0.477,"y":0},{"x":0.783,"y":0},{"x":1,"y":0}],"x":750,"y":80,"wires":[["40de744.1e2ca8c"]]},{"id":"e5bb5a04.a33948","type":"spline-curve","z":"e812678d.651e28","name":"morning curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.01},{"x":0.132,"y":0.01},{"x":0.269,"y":0.027},{"x":0.449,"y":0.124},{"x":0.676,"y":0.334},{"x":1,"y":0.75}],"x":760,"y":120,"wires":[["40de744.1e2ca8c"]]},{"id":"23b260f9.19f78","type":"spline-curve","z":"e812678d.651e28","name":"evening curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.85},{"x":0.036,"y":0.78},{"x":0.176,"y":0.63},{"x":1,"y":0.3}],"x":760,"y":200,"wires":[["40de744.1e2ca8c"]]},{"id":"7c0f0774.90adf8","type":"spline-curve","z":"e812678d.651e28","name":"daylight curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.744},{"x":0.03,"y":0.838},{"x":0.097,"y":0.941},{"x":0.183,"y":1},{"x":0.86,"y":0.995},{"x":0.943,"y":0.931},{"x":1,"y":0.85}],"x":760,"y":160,"wires":[["40de744.1e2ca8c"]]},{"id":"840431b7.eeefc","type":"spline-curve","z":"e812678d.651e28","name":"night curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.5},{"x":0.067,"y":0.368},{"x":0.133,"y":0.265},{"x":0.27,"y":0.171},{"x":0.333,"y":0.16},{"x":0.506,"y":0.124},{"x":0.606,"y":0}],"x":750,"y":280,"wires":[["c26bdcca.eb483"]]},{"id":"d4d99259.d0d7e","type":"spline-curve","z":"e812678d.651e28","name":"morning curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0},{"x":0.42,"y":0.018},{"x":0.54,"y":0.051},{"x":0.587,"y":0.101},{"x":0.817,"y":0.675},{"x":1,"y":0.85}],"x":760,"y":320,"wires":[["c26bdcca.eb483"]]},{"id":"a907f5f4.0bf488","type":"spline-curve","z":"e812678d.651e28","name":"evening curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.85},{"x":0.787,"y":0.598},{"x":1,"y":0.5}],"x":760,"y":400,"wires":[["c26bdcca.eb483"]]},{"id":"6ac1af7.dddb15","type":"spline-curve","z":"e812678d.651e28","name":"daylight curve","output_key":"","input_key":"payload.times_of_day.active.perc","points":[{"x":0,"y":0.85},{"x":0.123,"y":0.927},{"x":0.5,"y":1},{"x":0.92,"y":0.904},{"x":1,"y":0.85}],"x":760,"y":360,"wires":[["c26bdcca.eb483"]]},{"id":"a330b219.92e4e","type":"function","z":"e812678d.651e28","name":"Times of Day","func":"var newmsg = JSON.parse(JSON.stringify(msg));\nnewmsg.payload.times_of_day = {};\n\nprocessTimeOfDay = function(nme, obj, name_start, name_end) {\n    var ret = {};\n    ret.name  = nme;\n    ret.start = obj.times[name_start].ts;\n    ret.end   = obj.times[name_end].ts;\n    ret.perc  = (obj.ts - ret.start) / (ret.end - ret.start);\n    \n    \n    // to cover range starting before midnight and ending after it.\n    if (ret.start > ret.end)\n    {\n        var a;\n\n        // before midnight\n        if (ret.start < obj.ts) \n        {\n            a = new Date(ret.end);\n            a.setUTCDate(a.getUTCDate() + 1);\n            ret.end = a.getTime();\n             \n             \n        }\n        else if (ret.end > obj.ts) \n        {\n            a = new Date(ret.start);\n            a.setUTCDate(a.getUTCDate() - 1);\n            ret.start = a.getTime();\n\n        }\n\n    }\n    \n    ret.perc  = (obj.ts - ret.start) / (ret.end - ret.start);\n    \n    obj.times_of_day[nme] = ret;\n    if (ret.start <= obj.ts && obj.ts < ret.end ) obj.times_of_day.active = ret;\n}\n\n\nprocessTimeOfDay('night', newmsg.payload, \"astronomicalDusk\", \"astronomicalDawn\");\nprocessTimeOfDay('morning', newmsg.payload, \"astronomicalDawn\", \"goldenHourDawnEnd\");\nprocessTimeOfDay('day', newmsg.payload, \"goldenHourDawnEnd\", \"sunsetStart\");\nprocessTimeOfDay('evening', newmsg.payload, \"sunsetStart\", \"astronomicalDusk\");\n\n\nreturn newmsg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":290,"y":240,"wires":[["5de89261.4fb97c"]]},{"id":"40de744.1e2ca8c","type":"range","z":"e812678d.651e28","minin":"0","maxin":"1","minout":"1","maxout":"100","action":"scale","round":true,"property":"payload","name":"","x":980,"y":140,"wires":[[]]},{"id":"c26bdcca.eb483","type":"range","z":"e812678d.651e28","minin":"0","maxin":"1","minout":"2000","maxout":"5500","action":"scale","round":true,"property":"payload","name":"","x":990,"y":340,"wires":[[]]},{"id":"aa4b86d8.b17118","type":"sun-position","z":"e812678d.651e28","name":"","positionConfig":"31826811.22a458","rules":[],"onlyOnChange":"true","topic":"","outputs":1,"start":"","startType":"none","startOffset":0,"startOffsetType":"num","startOffsetMultiplier":60000,"end":"","endType":"none","endOffset":0,"endOffsetType":"num","endOffsetMultiplier":60000,"x":190,"y":160,"wires":[["a330b219.92e4e"]]},{"id":"31826811.22a458","type":"position-config","name":"Home Location","isValide":"true","longitude":"0","latitude":"0","angleType":"deg","timeZoneOffset":99,"timeZoneDST":0,"stateTimeFormat":"3","stateDateFormat":"12"},{"id":"7d2eacb.1469454","type":"tab","label":"Circadian Lighting","disabled":false,"info":""},{"id":"355e10bb.c79f1","type":"inject","z":"7d2eacb.1469454","name":"1 minute","props":[{"p":"payload"}],"repeat":"20","crontab":"","once":true,"onceDelay":"5","topic":"","payload":"","payloadType":"date","x":100,"y":100,"wires":[["a5d40275.dde32"]]},{"id":"a5d40275.dde32","type":"subflow:e812678d.651e28","z":"7d2eacb.1469454","name":"","env":[],"x":270,"y":100,"wires":[["3bad727.5d6028e"],["65269387.2b052c"],[]],"outputLabels":["Bigthness [%]","Warmth [K]","Debug"],"icon":"node-red-node-suncalc/sun.png"},{"id":"3bad727.5d6028e","type":"ha-entity","z":"7d2eacb.1469454","name":"Gain [%]","server":"c879ac61.99cd1","version":1,"debugenabled":false,"outputs":1,"entityType":"sensor","config":[{"property":"name","value":"nr_circadian_brightness"},{"property":"device_class","value":""},{"property":"icon","value":"mdi:brightness-percent"},{"property":"unit_of_measurement","value":"%"}],"state":"payload","stateType":"msg","attributes":[],"resend":true,"outputLocation":"","outputLocationType":"none","inputOverride":"allow","outputOnStateChange":false,"outputPayload":"","outputPayloadType":"str","x":460,"y":60,"wires":[["478ff358.47fe54"]]},{"id":"65269387.2b052c","type":"ha-entity","z":"7d2eacb.1469454","name":"Warmth [K]","server":"c879ac61.99cd1","version":1,"debugenabled":false,"outputs":1,"entityType":"sensor","config":[{"property":"name","value":"nr_circadian_kelvin"},{"property":"device_class","value":""},{"property":"icon","value":"mdi:brightness-6"},{"property":"unit_of_measurement","value":"K"}],"state":"payload","stateType":"msg","attributes":[],"resend":true,"outputLocation":"","outputLocationType":"none","inputOverride":"allow","outputOnStateChange":false,"outputPayload":"","outputPayloadType":"str","x":470,"y":120,"wires":[["1f4839ae.37a826"]]},{"id":"c1c1a71e.5f6308","type":"comment","z":"7d2eacb.1469454","name":"Circadian Lighting","info":"","x":110,"y":40,"wires":[]},{"id":"2d33c174.39af5e","type":"actionflows_in","z":"7d2eacb.1469454","name":"Circadian in","priority":"50","links":[],"scope":"global","x":730,"y":40,"wires":[["c7af27c0.989e48"]]},{"id":"670bb06f.b5652","type":"actionflows_out","z":"7d2eacb.1469454","name":"Circadian out","links":[],"x":1030,"y":40,"wires":[]},{"id":"478ff358.47fe54","type":"function","z":"7d2eacb.1469454","name":"Brightness","func":"var brightness = msg.payload;\nglobal.set('nr_circadian_brightness',brightness)\nvar newmsg = \n{\n    payload: {\n        data: {\n            brightness_pct: Math.round(brightness)\n        }\n    }\n    \n}\nreturn newmsg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":610,"y":80,"wires":[["c7af27c0.989e48","1732d37e.89d2ad"]]},{"id":"1f4839ae.37a826","type":"function","z":"7d2eacb.1469454","name":"Kelvin","func":"var kelvin = msg.payload;\nglobal.set('nr_circadian_kelvin',kelvin)\nvar newmsg = \n{\n    payload: {\n        data: {\n            kelvin: kelvin\n        }\n    }\n    \n}\nreturn newmsg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":630,"y":120,"wires":[["c7af27c0.989e48","1732d37e.89d2ad"]]},{"id":"471bbb4.79ca144","type":"actionflows_in","z":"7d2eacb.1469454","name":"Smooth Circadian in","priority":"50","links":[],"scope":"global","x":710,"y":200,"wires":[["1732d37e.89d2ad"]]},{"id":"f0f28a0.914b578","type":"actionflows_out","z":"7d2eacb.1469454","name":"Smooth Circadian out","links":[],"x":1040,"y":200,"wires":[]},{"id":"c7af27c0.989e48","type":"join","z":"7d2eacb.1469454","name":"","mode":"custom","build":"merged","property":"payload.data","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":true,"timeout":"","count":"2","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":870,"y":80,"wires":[["670bb06f.b5652"]]},{"id":"63fccc95.3fddd4","type":"inject","z":"7d2eacb.1469454","name":"10","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":"5","topic":"","payload":"10","payloadType":"num","x":470,"y":200,"wires":[["d4d75762.69e478"]]},{"id":"d4d75762.69e478","type":"function","z":"7d2eacb.1469454","name":"","func":"var transition = msg.payload;\nvar newmsg = \n{\n    payload: {\n        data: {\n            transition: Math.round(transition)\n        }\n    }\n    \n}\nreturn newmsg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":620,"y":160,"wires":[["1732d37e.89d2ad"]]},{"id":"1732d37e.89d2ad","type":"join","z":"7d2eacb.1469454","name":"","mode":"custom","build":"merged","property":"payload.data","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":true,"timeout":"","count":"3","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":870,"y":120,"wires":[["f0f28a0.914b578","e2316f53.c115f"]]},{"id":"e2316f53.c115f","type":"debug","z":"7d2eacb.1469454","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1040,"y":100,"wires":[]},{"id":"c879ac61.99cd1","type":"server","name":"Home Assistant","legacy":false,"addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true}]

which I’m using in Office, Bedroom and Bathroom.
Bathroom was tricky as those LED lights require minimum 12% to turn on…