January 27, 2020By Jason Cronquist← Back to Blog

Circadian Lights: Node Red tutorial


Tutorial: Circadian Color Temperature Automation

Requirements

In order to run this automation, you will need:

  • Node-Red

    • node-red-contrib-home-assistant-websocket (plugin)
    • node-red-contrib-spline-curve (plugin)
  • Home Assistant
  • Color Temperature Smart Bulb
  • Recommendation: Hue White Ambiance

    • [Optional] Smart Button
  • Recommendation: Hue Dimmer Switch

    • [Optional] Dedicated hardware to run Node-Red and Home Assistant
  • Recommendation: Raspberry Pi

Home Assistant

We’ll use Home Assistant’s state management to keep track of the light properties. This way we can easily access them asynchronously in any flow at any time. In other words, we want to separate the light cycle from the implementation so it can be easily applied in any automation.

In your Home Assistant configuration folder, create a new file called input_number.yaml, and add the following yaml text.

x_in_color_curve:
    name: Input X in Color Curve
    min: 0.0
    max: 1.0
    step: 0.001

circadian_kelvin:
    name: Circadian Kelvin
    min: 2200
    max: 6500
    step: 1

circadian_brightness:
    name: Circadian Brightness
    min: 0
    max: 255
    step: 1

Notes: The xincolorcurve inputnumber is not necessary for the automation, but it allows Home Assistant to see the input to our curve, which I like to display on my Lovelace UI.

Link this file to your configuration.yaml file by adding the following line.

input_number: !include input_number.yaml

Now create a file called input_select.yaml and add the yaml text below. The light_state will be used to determine whether we should apply the circadian cycle to our lights or allow them to be controlled manually. This allows us to account for our exceptions (where the automated lights should not be applied).

light_state:                                                                     
    name: Light State                                                            
    icon: mdi:alarm-light                                                        
    options:                                                                     
        - Circadian                                                              
        - Manual

color_curve:
    name: Color Curve
    icon: mdi:alarm-light
    options:
        - sunrise
        - sunset
        - daylight
        - evening
        - nighttime

Note: color_curve is not necessary for the automation, but I like to have Home Assistant track it so I can see which curve is currently being used from the Lovelace UI.

This file should also be linked to from the configuration.yaml file. To do so add the following line to configuration.yaml.

input_select: !include input_select.yaml

You can now restart Home Assistant so your changes will be picked up.

Node-Red

The heart of the automation will be driven by Node-Red. Every heart needs a beat to drive it, so let’s create one. Set this up in it’s own flow and use link-out-nodes and link-in-nodes to drive your various automations.

heartbeat
heartbeat

[{"id":"b698650f.2ee1e8","type":"tab","label":"Automation Control","disabled":false,"info":""},{"id":"b2dc3f75.fe8ef","type":"inject","z":"b698650f.2ee1e8","name":"30 Second Heartbeat","topic":"","payload":"","payloadType":"date","repeat":"30","crontab":"","once":false,"onceDelay":0.1,"x":140,"y":160,"wires":[["35db42fe.28d3ce","e370369c.bd1d58"]]},{"id":"35db42fe.28d3ce","type":"delay","z":"b698650f.2ee1e8","name":"","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":340,"y":100,"wires":[["ae0e149e.3dea88"]]},{"id":"ae0e149e.3dea88","type":"link out","z":"b698650f.2ee1e8","name":"Apply","links":["5005e71e.410a28","77410c.6b5fbef4","79382a3d.3e7d14","a4e02385.c2118","f1263e06.266ef"],"x":455,"y":100,"wires":[]},{"id":"e370369c.bd1d58","type":"link out","z":"b698650f.2ee1e8","name":"Update","links":["7e8d293f.212408"],"x":295,"y":200,"wires":[]},{"id":"ea2223f4.c56bf","type":"server-state-changed","z":"b698650f.2ee1e8","name":"","server":"407b5317.e98f2c","version":1,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"input_select.light_state","entityidfiltertype":"exact","outputinitially":false,"state_type":"str","haltifstate":"","halt_if_type":"str","halt_if_compare":"is","outputs":1,"output_only_on_state_change":true,"x":250,"y":60,"wires":[["ae0e149e.3dea88"]]},{"id":"407b5317.e98f2c","type":"server","z":"","name":"Home Assistant","legacy":false,"hassio":false,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true}]

A heartbeat is generated once every 30 seconds. The heartbeat first drives all the update functions to calculate the current light settings. After a 5 second delay, the heartbeat then drives the update of all the lights you want on the automation.

Alternatively, if there is a change to the input_select.light_state entity in Home Assistant, we want to immediately apply the current color settings.

In a new flow tab, we will create the logic that defines the color temperature based on time of day. You can also place any color calculations you have for other automatons in this tab as well. How you organize your flows is ultimately up to you though (Please share your thoughts in the comments!).

colortemp-automation
colortemp-automation

[{"id":"71a51c34.06e2c4","type":"function","z":"b87e11cb.ab0c3","name":"Evaluate Schedule","func":"const hour      = 3600000;\nconst half_hour = 1800000;\n\nconst now = msg.payload.ts;\nconst curveSchedule = msg.payload.curveSchedule;\n\nvar start, end, colorCurve;\nfor(var i=1; i < curveSchedule.length; i++){\n    if(now < curveSchedule[i].ts){\n        colorCurve = curveSchedule[i-1].colorCurve;\n        start = curveSchedule[i-1].ts;\n        end = curveSchedule[i].ts;\n        break;\n    }\n}\n\nconst range = end - start;\nconst curveInput = (now - start)/range;\n\nmsg.payload.lightData = {\n    colorCurve: colorCurve,\n    now: now,\n    start: start,\n    end: end,\n    curveInput: (curveInput < 0.0)? \n            0.0 \n        : \n        (curveInput > 1.0)? \n            1.0\n        : \n            curveInput\n}\n\nmsg.payload.continue = \"true\";\n\nreturn msg;","outputs":1,"noerr":0,"x":370,"y":200,"wires":[["ef08eca3.057ba","2444310c.142a7e","d151a26d.17f31"]]},{"id":"ef08eca3.057ba","type":"switch","z":"b87e11cb.ab0c3","name":"Curve Schedule Switch","property":"payload.lightData.colorCurve","propertyType":"msg","rules":[{"t":"eq","v":"nighttime","vt":"str"},{"t":"eq","v":"sunrise","vt":"str"},{"t":"eq","v":"daylight","vt":"str"},{"t":"eq","v":"sunset","vt":"str"},{"t":"eq","v":"evening","vt":"str"}],"checkall":"true","repair":false,"outputs":5,"x":610,"y":200,"wires":[["3d6c3fd0.aa7b"],["e3d3519.bbb94b"],["5abb83ab.2cb7ec"],["3abe07a1.99a788"],["f5c27d65.4b37f"]]},{"id":"2444310c.142a7e","type":"api-call-service","z":"b87e11cb.ab0c3","name":"set_value: color_curve","server":"407b5317.e98f2c","version":1,"debugenabled":true,"service_domain":"input_select","service":"select_option","entityId":"input_select.color_curve","data":"{\"option\":\"{{payload.lightData.colorCurve}}\"}","dataType":"json","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":580,"y":260,"wires":[[]]},{"id":"d151a26d.17f31","type":"api-call-service","z":"b87e11cb.ab0c3","name":"set_value: x_in_color_curve","server":"407b5317.e98f2c","version":1,"debugenabled":false,"service_domain":"input_number","service":"set_value","entityId":"input_number.x_in_color_curve","data":"{\"value\": {{payload.lightData.curveInput}}}","dataType":"json","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":600,"y":320,"wires":[[]]},{"id":"d98d0205.1dc9b","type":"function","z":"b87e11cb.ab0c3","name":"Generate Curve Schedule","func":"const hour      = 3600000;\nconst halfHour  = 1800000;\nconst day       = hour*24;\n\nconst now = msg.payload.ts;\nconst now_time = new Date(now);\nconst eveningEndTime = new Date(\n    now_time.getFullYear(),\n    now_time.getMonth(),\n    now_time.getDate(),\n    19, 30, 0, 0\n);\n\nconst sunrise = msg.payload.times.sunrise.ts;\nconst sunset = msg.payload.times.sunset.ts;\nconst eveningEnd = eveningEndTime.getTime();\n\nconst schedule = [\n    {colorCurve: 'nighttime', ts: (sunset + halfHour)-day},\n    {colorCurve: 'sunrise', ts: sunrise - halfHour}, \n    {colorCurve: 'daylight', ts: sunrise + halfHour},\n    {colorCurve: 'sunset', ts: sunset - halfHour}\n];\n\nif(sunset+halfHour < eveningEnd)\n{\n    schedule.push({colorCurve: 'evening', ts: sunset + halfHour}); \n    schedule.push({colorCurve: 'nighttime', ts: eveningEnd});    \n}\nelse{\n    schedule.push({colorCurve: 'nighttime', ts: sunset + halfHour});\n}\n\nschedule.push({colorCurve: 'sunrise', ts: (sunrise - halfHour) + day});\nmsg.payload.curveSchedule = schedule;\nreturn msg;\n","outputs":1,"noerr":0,"x":150,"y":200,"wires":[["71a51c34.06e2c4"]]},{"id":"3d6c3fd0.aa7b","type":"spline-curve","z":"b87e11cb.ab0c3","name":"Night Curve","output_key":"payload.kelvin","input_key":"payload.lightData.curveInput","points":[{"x":0,"y":0},{"x":1,"y":0}],"x":850,"y":120,"wires":[["522b7816.519858"]]},{"id":"e3d3519.bbb94b","type":"spline-curve","z":"b87e11cb.ab0c3","name":"Sunrise Curve","output_key":"payload.kelvin","input_key":"payload.lightData.curveInput","points":[{"x":0,"y":0},{"x":0.14,"y":0.243},{"x":0.26,"y":0.936},{"x":0.383,"y":0.95},{"x":0.433,"y":0.853},{"x":0.486,"y":0.27},{"x":0.533,"y":0.136},{"x":0.623,"y":0.126},{"x":0.866,"y":0.2},{"x":0.996,"y":0.5}],"x":860,"y":160,"wires":[["9b4959a2.2024c8"]]},{"id":"5abb83ab.2cb7ec","type":"spline-curve","z":"b87e11cb.ab0c3","name":"Daylight Curve","output_key":"payload.kelvin","input_key":"payload.lightData.curveInput","points":[{"x":0,"y":0.5},{"x":0.226,"y":0.593},{"x":0.333,"y":0.713},{"x":0.503,"y":0.873},{"x":0.666,"y":0.706},{"x":0.77,"y":0.596},{"x":1,"y":0.5}],"x":860,"y":200,"wires":[["cea008ae.9d87f8"]]},{"id":"3abe07a1.99a788","type":"spline-curve","z":"b87e11cb.ab0c3","name":"Sunset Curve","output_key":"payload.kelvin","input_key":"payload.lightData.curveInput","points":[{"x":0.003,"y":0.5},{"x":0.166,"y":0.3},{"x":0.52,"y":0.146},{"x":0.783,"y":0.393},{"x":1,"y":0.25}],"x":860,"y":240,"wires":[["8766223.b0360e"]]},{"id":"f5c27d65.4b37f","type":"spline-curve","z":"b87e11cb.ab0c3","name":"Evening Curve","output_key":"payload.kelvin","input_key":"payload.lightData.curveInput","points":[{"x":0,"y":0.25},{"x":0.266,"y":0.253},{"x":0.496,"y":0.25},{"x":1,"y":0}],"x":861,"y":280,"wires":[["cdfedd36.abee6"]]},{"id":"1c4413dd.a4885c","type":"function","z":"b87e11cb.ab0c3","name":"Normalize Times","func":"const hour = 3600 * 1000;\nconst halfHour = hour / 2;\nconst day = hour * 24;\n\nconst now = new Date();\nconst dawn = new Date(msg.data.attributes.next_rising);\nconst dusk = new Date(msg.data.attributes.next_setting);\n\nvar sunrise, sunset;\nif(dawn.getDate() === now.getDate() ){\n    sunrise = dawn.getTime();\n}\nelse{\n    sunrise = dawn.getTime() - day;\n}\n\nif(dusk.getDate() === now.getDate() ){\n    sunset = dusk.getTime();\n}\nelse{\n    sunset = dusk.getTime() - day;\n}\n\nmsg.payload = {\n    ts: now.getTime(),\n    times:{\n        sunrise: {\n            ts: sunrise\n        },\n        sunset: {\n            ts: sunset\n        }\n    }\n}\n\nmsg.payload\n\nreturn msg;","outputs":1,"noerr":0,"x":390,"y":40,"wires":[["d98d0205.1dc9b"]]},{"id":"522b7816.519858","type":"spline-curve","z":"b87e11cb.ab0c3","name":"Night Bright","output_key":"payload.brightness","input_key":"payload.lightData.curveInput","points":[{"x":0,"y":0.5},{"x":0.246,"y":0.28},{"x":0.496,"y":0.24},{"x":0.856,"y":0.253},{"x":0.96,"y":0.353},{"x":1,"y":0.5}],"x":1050,"y":120,"wires":[["fb18f7b9.341cf8"]]},{"id":"9b4959a2.2024c8","type":"spline-curve","z":"b87e11cb.ab0c3","name":"Sunrise Bright","output_key":"payload.brightness","input_key":"payload.lightData.curveInput","points":[{"x":0,"y":0.5},{"x":0.176,"y":0.67},{"x":0.26,"y":0.936},{"x":0.306,"y":0.973},{"x":0.553,"y":0.996},{"x":0.996,"y":1}],"x":1040,"y":160,"wires":[["fb18f7b9.341cf8"]]},{"id":"cea008ae.9d87f8","type":"spline-curve","z":"b87e11cb.ab0c3","name":"Daylight Bright","output_key":"payload.brightness","input_key":"payload.lightData.curveInput","points":[{"x":0,"y":1}],"x":1040,"y":200,"wires":[["fb18f7b9.341cf8"]]},{"id":"8766223.b0360e","type":"spline-curve","z":"b87e11cb.ab0c3","name":"Sunset Bright","output_key":"payload.brightness","input_key":"payload.lightData.curveInput","points":[{"x":0.003,"y":1},{"x":0.29,"y":0.996},{"x":0.4,"y":0.96},{"x":0.556,"y":0.776},{"x":0.673,"y":0.753},{"x":0.986,"y":0.75}],"x":1040,"y":240,"wires":[["fb18f7b9.341cf8"]]},{"id":"cdfedd36.abee6","type":"spline-curve","z":"b87e11cb.ab0c3","name":"Evening Bright","output_key":"payload.brightness","input_key":"payload.lightData.curveInput","points":[{"x":0,"y":0.75},{"x":0.243,"y":0.753},{"x":0.5,"y":0.74},{"x":0.99,"y":0.5}],"x":1040,"y":280,"wires":[["fb18f7b9.341cf8"]]},{"id":"aa2fae39.e86e9","type":"api-current-state","z":"b87e11cb.ab0c3","name":"","server":"407b5317.e98f2c","version":1,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","override_topic":false,"entity_id":"sun.sun","state_type":"str","state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","blockInputOverrides":false,"x":180,"y":40,"wires":[["1c4413dd.a4885c"]]},{"id":"fb18f7b9.341cf8","type":"range","z":"b87e11cb.ab0c3","minin":"0.0","maxin":"1.0","minout":"2200","maxout":"6500","action":"scale","round":false,"property":"payload.kelvin","name":"Map to Kelvin","x":1260,"y":200,"wires":[["f48e1c0c.d8268"]]},{"id":"7e8d293f.212408","type":"link in","z":"b87e11cb.ab0c3","name":"","links":["e370369c.bd1d58"],"x":35,"y":40,"wires":[["aa2fae39.e86e9"]]},{"id":"c9bbde63.a6c23","type":"inject","z":"b87e11cb.ab0c3","name":"Manual Trigger","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":120,"y":120,"wires":[["aa2fae39.e86e9"]]},{"id":"f48e1c0c.d8268","type":"range","z":"b87e11cb.ab0c3","minin":"0","maxin":"1","minout":"1","maxout":"255","action":"clamp","round":true,"property":"payload.brightness","name":"Map to Brightness","x":1290,"y":240,"wires":[["84f9e1ac.7d58c","148ab10b.da900f"]]},{"id":"84f9e1ac.7d58c","type":"api-call-service","z":"b87e11cb.ab0c3","name":"set_value: circadian_kelvin","server":"407b5317.e98f2c","version":1,"debugenabled":false,"service_domain":"input_number","service":"set_value","entityId":"input_number.circadian_kelvin","data":"{\"value\": {{payload.kelvin}}}","dataType":"json","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":1340,"y":280,"wires":[[]]},{"id":"148ab10b.da900f","type":"api-call-service","z":"b87e11cb.ab0c3","name":"set_value: circadian_brightness","server":"407b5317.e98f2c","version":1,"debugenabled":false,"service_domain":"input_number","service":"set_value","entityId":"input_number.circadian_brightness","data":"{\"value\": {{payload.brightness}}}","dataType":"json","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":1350,"y":320,"wires":[[]]},{"id":"407b5317.e98f2c","type":"server","z":"","name":"Home Assistant","legacy":false,"hassio":false,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true}]

There’s a lot going on here so let’s break it down into its component parts.

normalizetimes
normalizetimes

The entry point to the flow is the output from the 30 second heartbeat. We first query the Home Assistant entity sun.sun to get the times at which the sun will rise and set. This data is added to msg.data.attributes.nextrising and msg.data.attributes.nextsetting respectively.

Generating the Light Settings

In order to build our light schedule, we will need to normalize this data to get the time of sunrise and sunset for the current date. We handle this in the Normalize Times node using the following javascript code.

const hour = 3600 * 1000;
const day = hour * 24;

const now = new Date();
const dawn = new Date(msg.data.attributes.next_rising);
const dusk = new Date(msg.data.attributes.next_setting);

var sunrise, sunset;
if(dawn.getDate() === now.getDate() ){
    sunrise = dawn.getTime();
}
else{
    sunrise = dawn.getTime() - day;
}

if(dusk.getDate() === now.getDate() ){
    sunset = dusk.getTime();
}
else{
    sunset = dusk.getTime() - day;
}

msg.payload = {
    ts: now.getTime(),
    times:{
        sunrise: {
            ts: sunrise
        },
        sunset: {
            ts: sunset
        }
    }
}

msg.payload

return msg;

curve-schedules

The next step is to generate and evaluate the curve schedule, or the times of day for which we should be using each curve. This is logically the heaviest part of the automation.

const hour      = 3600000;
const halfHour  = 1800000;
const day       = hour*24;

const now = msg.payload.ts;
const now_time = new Date(now);
const eveningEndTime = new Date(
    now_time.getFullYear(),
    now_time.getMonth(),
    now_time.getDate(),
    19, 30, 0, 0
);

const sunrise = msg.payload.times.sunrise.ts;
const sunset = msg.payload.times.sunset.ts;
const eveningEnd = eveningEndTime.getTime();

const schedule = [
    {colorCurve: 'nighttime', ts: (sunset + halfHour)-day},
    {colorCurve: 'sunrise', ts: sunrise - halfHour}, 
    {colorCurve: 'daylight', ts: sunrise + halfHour},
    {colorCurve: 'sunset', ts: sunset - halfHour}
];

if(sunset+halfHour < eveningEnd)
{
    schedule.push({colorCurve: 'evening', ts: sunset + halfHour}); 
    schedule.push({colorCurve: 'nighttime', ts: eveningEnd});    
}
else{
    schedule.push({colorCurve: 'nighttime', ts: sunset + halfHour});
}

schedule.push({colorCurve: 'sunrise', ts: (sunrise - halfHour) + day});
msg.payload.curveSchedule = schedule;
return msg;

A schedule is a simple array containing objects. Each object contains the timestamp at which it starts, and the name of the curve to use during that time.

We are also defining the time at which the evening ends in this function. If you recall, we only want to use the evening curve if the sun sets before we’re ready for our day to be over. I have it set to 7:30 pm (19:30 in a 24hour clock), but you can adjust to your preference.

You may have noticed that we are adding and subtracting Half Hours throughout the code. This is to account for the somewhat complex behavior of color temperature surrounding sunrise and sunset.

To evaluate the schedule, we iterate over the array until a timestamp is found that is greater than the current time. We then know that the desired color curve is defined in index – 1. Calculating the input x for the curve evaluation is a straightforward task. The code for this is found below.

const hour      = 3600000;
const half_hour = 1800000;

const now = msg.payload.ts;
const curveSchedule = msg.payload.curveSchedule;

var start, end, colorCurve;
for(var i=1; i < curveSchedule.length; i++){
    if(now < curveSchedule[i].ts){
        colorCurve = curveSchedule[i-1].colorCurve;
        start = curveSchedule[i-1].ts;
        end = curveSchedule[i].ts;
        break;
    }
}

const range = end - start;
const curveInput = (now - start)/range;

msg.payload.lightData = {
    colorCurve: colorCurve,
    now: now,
    start: start,
    end: end,
    curveInput: (curveInput < 0.0)? 
            0.0 
        : 
        (curveInput > 1.0)? 
            1.0
        : 
            curveInput
}

msg.payload.continue = "true";

return msg;

The next step is to determine the current color temperature and brightness based off the evaluated schedule.

schedule-evaluation
schedule-evaluation

We first notify Home Assistant of the current xincolorcurve as well as the selected colorcurve. This has no bearing on the automation itself, but it’s nice to have access to this information from Home Assistant directly.

We then set the Color Temperature and Brightness based on the values evaluated in the previous step.

spline-curve
spline-curve
bri-splinecurve
bri-splinecurve

As a last step, we just need to map the output values (0.0-1.0) to the corresponding range in Kelvin (2,200-6,500) and Brightness (1-255).

Finally, we update Home Assistant with the new values.

set-lights
set-lights

Applying the Light Settings

Home Assistant will now have the latest light settings stored in the input entities inputnumber.circadiankelvin and inputnumber.circadianbrightness. We can also track whether we want to apply the automations with the entity input_select.light_state.

In order to apply these settings, we just need to recall these values and set them to a light or light group so long as input_select.light_state is set to Circadian.

responsive-lights-1.png
responsive-lights-1.png
Lights-On-Trigger
Lights-On-Trigger

[{"id":"456e17cb.a7c5f8","type":"api-call-service","z":"7821773a.974a48","name":"Set Garage Entrance (circadian)","server":"407b5317.e98f2c","version":1,"debugenabled":false,"service_domain":"light","service":"turn_on","entityId":"light.garage_entrance","data":"{ \"kelvin\": {{kelvin}},\"brightness\":{{brightness}}}","dataType":"json","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":1050,"y":120,"wires":[[]]},{"id":"fc9e222d.1dd34","type":"api-current-state","z":"7821773a.974a48","name":"Get Kelvin (circadian)","server":"407b5317.e98f2c","version":1,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","override_topic":false,"entity_id":"input_number.circadian_kelvin","state_type":"str","state_location":"kelvin","override_payload":"msg","entity_location":"data","override_data":"msg","blockInputOverrides":false,"x":560,"y":120,"wires":[["fb283e3c.9319"]]},{"id":"fb283e3c.9319","type":"api-current-state","z":"7821773a.974a48","name":"Get Brightness (circadian) ","server":"407b5317.e98f2c","version":1,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","override_topic":false,"entity_id":"input_number.circadian_brightness","state_type":"str","state_location":"brightness","override_payload":"msg","entity_location":"data","override_data":"msg","blockInputOverrides":false,"x":790,"y":120,"wires":[["456e17cb.a7c5f8","f6d5e6e.7e00d18"]]},{"id":"9c4bfd38.5f798","type":"api-current-state","z":"7821773a.974a48","name":"Get Light State","server":"407b5317.e98f2c","version":1,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","override_topic":false,"entity_id":"input_select.light_state","state_type":"str","state_location":"light_state","override_payload":"msg","entity_location":"data","override_data":"msg","blockInputOverrides":false,"x":520,"y":60,"wires":[["9f6b74c4.7c8c38"]]},{"id":"79382a3d.3e7d14","type":"link in","z":"7821773a.974a48","name":"","links":["202a4dee.505f12","9470a07b.dfcc6","ae0e149e.3dea88"],"x":195,"y":60,"wires":[["43a2fb7a.b115a4"]]},{"id":"9f6b74c4.7c8c38","type":"switch","z":"7821773a.974a48","name":"","property":"light_state","propertyType":"msg","rules":[{"t":"neq","v":"Manual","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":670,"y":60,"wires":[["fc9e222d.1dd34"]]},{"id":"f6d5e6e.7e00d18","type":"api-call-service","z":"7821773a.974a48","name":"Set Hallway (circadian)","server":"407b5317.e98f2c","version":1,"debugenabled":false,"service_domain":"light","service":"turn_on","entityId":"light.hallway","data":"{ \"kelvin\": {{kelvin}},\"brightness\":{{brightness}}}","dataType":"json","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":1020,"y":160,"wires":[[]]},{"id":"43a2fb7a.b115a4","type":"api-current-state","z":"7821773a.974a48","name":"Hallway Light is On","server":"407b5317.e98f2c","version":1,"outputs":2,"halt_if":"on","halt_if_type":"str","halt_if_compare":"is","override_topic":false,"entity_id":"light.hallway","state_type":"str","state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","blockInputOverrides":false,"x":330,"y":60,"wires":[["9c4bfd38.5f798"],[]]},{"id":"dfa5539a.44654","type":"server-state-changed","z":"7821773a.974a48","name":"Hallway Light Turns On","server":"407b5317.e98f2c","version":1,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"light.hallway","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,"x":120,"y":100,"wires":[["43a2fb7a.b115a4"],[]]},{"id":"407b5317.e98f2c","type":"server","z":"","name":"Home Assistant","legacy":false,"hassio":false,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true}]

The automation will now apply to your lights, or at least the lights that you have configured. If you add the entity input_select.light_state to a tile in your Lovelace UI, you can activate and deactivate the automation directly from Home Assistant. There’s an easier (optional) way to turn on and off the automation however, and that’s to re-purpose your Hue Dimmer Switch (or any smart button) such that holding the on or off buttons will set your input_select.light_state. I have documented the procedure in this article on node-red. It should be a simple exercise to update the light_state names to apply to this use case.

Conclusion

The circadian light schedule has been the single greatest automation I have applied to my home thus far. I have seen a significant, and measurable thanks to my fitbit, improvement to my sleep since activating it. In my opinion, it is the one ‘killer-app’ of the smart home. I hope that this tutorial made it easy to implement this in your own home automations. Let me know how circadian cycle helped you in the comments.