Technology Solutions for Everyday Folks
Screen snip of two settings in Home Assistant for Light state and a script trigger

Lighting the Dark Days of Winter with Home Assistant

This fall, right after the time change, I found myself often working intensely enough to suddenly notice "it was dark outside" in the late afternoon. Good for productivity, I suppose; not so much otherwise? ¯\_(ツ)_/¯

I'd automated a whole bunch of other light situations, mostly with some combination of brightness level (obtained from my Tempest weather station) and/or time-based. But all of those solutions were "binary" (on/off). My thought here was to slowly and incrementally bring up the overhead smart lights in my home office space.

Multiple Options Available

I did a little cursory The Googling on the process, and found some elegant and "complete" solutions for this sort of thing. Most involved installing a script or blueprint and configuring things to your liking. The more I looked, the more I found stuff that worked but seemed like overkill for what I wanted: just a stepped-up sequence over a time period (slow change). Besides, I'd like to understand what I'm doing, so just grabbing someone else's solution and making it work for me wasn't desirable, especially when working with the scripts node.

The First Attempt

I'd only created one other script in Home Assistant before, to handle the garage door opener business (converting my 23-year-old door opener to a "smart" opener). So I figured for a first or proof-of-concept attempt I'd use things I'm more familiar with, namely timers and scenes.

I created a 20-minute timer to handle the "interval" between steps up on the brightness. I then created a set of scenes for the brightness "steps" I wanted (10% to 50%, the first being the starting step and the last being the normal "on" brightness in the space).

Then comes the automation. Ultimately it's an automation with two triggers, a time-based condition (I only want this to run at a certain time period of the day), and a series of conditional actions based on the light state when the automation was triggered, like so (simplified for readability):

Triggers

trigger:
  - platform: state
    entity_id:
      - timer.stepped_lights_timer
    from: active
    to: idle
  - type: illuminance
    platform: device
    device_id: guid-of-illuminance_sensor
    entity_id: sensor.illuminance_sensor
    domain: sensor
    below: 6000

 This will call the automation whenever the timer ends or when the outside brightness drops below my setpoint (6,000 lux in the example).

Conditional

I only want this to run on weekdays (in the window of 3-6 p.m.) and when "I'm home" (my phone is on the network):

condition:
  - condition: and
    conditions:
      - condition: time
        weekday:
          - mon
          - tue
          - thu
          - wed
          - fri
        after: "15:00:00"
        before: "18:00:00"
      - condition: state
        entity_id: device_tracker.unifi_mac_address_of_phone
        state: home

Actions

There are a whole set of conditions in the actual automation (from "off" to the threshold for my lights when they're 40%), but for simplicity I've included three states: lights are off; lights at 20% (increment brightness to 30%); and lights at 40% (last increment):

action:
  - if:
      - condition: device
        type: is_off
        device_id: guid-of-office_fl1
        entity_id: light.office_fl1
        domain: light
    then:
      - service: timer.start
        data: {}
        target:
          entity_id: timer.stepped_lights_timer
      - service: scene.turn_on
        target:
          entity_id: scene.office_lights_10
        metadata: {}
  - if:
      - condition: numeric_state
        entity_id: light.office_fan_lights
        attribute: brightness
        above: 41
        below: 61
    then:
      - service: timer.start
        data: {}
        target:
          entity_id: timer.stepped_lights_timer
      - service: scene.turn_on
        target:
          entity_id: scene.office_lights_30
        metadata: {}
  - if:
      - condition: numeric_state
        entity_id: light.office_fan_lights
        attribute: brightness
        above: 92
        below: 112
    then:
      - service: scene.turn_on
        target:
          entity_id: scene.office_lights_50
        metadata: {}

The Outcome?

Hey, it worked great! I had to do some tinkering to get the above and below values for the actions (via the states developer tool in Home Assistant), but it worked great. Every twenty minutes (once invoked) the light would increase in brightness by 10% until it hit 50% -- my setpoint for the normal on/off switch. 

HOWEVER, I knew there was a better way to set this up without requiring all the different scenes and timer, along with all the moving parts (conditional actions) of the automation. After using this automation for a few days, I moved to...

The Second Attempt

Knowing my idea wasn't totally bonkers and actually provided the desired outcome, I wanted to button and clean it up. This meant using a script, but that was less gnarly because I knew the working parts in question by this point so it was more a matter of stitching things together. Bonus points since it only requires two things: the automation and a script (no timers or scenes).

The beautiful part of this is the automation itself is far simpler:

The Automation

trigger:
  - type: illuminance
    platform: device
    device_id: guid-of-illuminance_sensor
    entity_id: sensor.illuminance_sensor
    domain: sensor
    below: 6000
condition:
  - condition: and
    conditions:
      - condition: time
        after: "15:00:00"
        before: "18:00:00"
        weekday:
          - mon
          - tue
          - wed
          - thu
          - fri
      - condition: state
        entity_id: device_tracker.unifi_mac_address_of_phone
        state: home
action:
  - service: script.office_light_stepped_fade_up
    data: {}

This is way easier to read and understand than the first attempt (especially in UI mode), and ultimately it just calls the script where the real action takes place!

The Script

In ~30 lines, this script handles all the details we were doing before, but in one "thing:"

sequence:
  - variables:
      initial_brightness_pct: 10
      step_brightness: 10
      time_delay_mins: 18
      target_brightness_pct: 50
      iterations: "{{ (target_brightness_pct - initial_brightness_pct) / step_brightness }}"
  - service: light.turn_on
    data:
      brightness_pct: "{{ initial_brightness_pct }}"
    target:
      entity_id: light.office_fan_lights
    enabled: true
  - delay:
      hours: 0
      minutes: "{{ time_delay_mins }}"
      seconds: 0
      milliseconds: 0
    enabled: true
  - repeat:
      sequence:
        - service: light.turn_on
          data:
            brightness_step_pct: "{{ step_brightness }}"
          target:
            entity_id: light.office_fan_lights
        - delay:
            hours: 0
            minutes: "{{ time_delay_mins }}"
            seconds: 0
            milliseconds: 0
      count: "{{ iterations }}"
    enabled: true

A couple of things happen here when the script is invoked/called:

  1. I create a set of variables for the script such as initial step, step percentage, target brightness, and delay between steps (in minutes). Using basic math, I determine the number of iterations it would take to get from the initial step to the target when jumping by step_brightness): (target_brightness_pct - initial_brightness_pct) / step_brightness
  2. The light group is turned on with a brightness_pct value of initial_brightness_pct (in this example 10%)
  3. Delay/Pause for time_delay_mins
  4. Repeat the same business from steps 2 and 3 iterations times (except increment by step_brightness when repeating)

When this script completes, the lights will step up in brightness by 10% five times over the course of 72 minutes (three iterations (18 minutes) plus the initial 18-minute delay). Now, I could have combined the initial turn_on with the repeat sequence...but I chose to keep them separate for flexibility. I could start the sequence at 5% and have 10% steps, or some other combination where having the initial step be "different" than the increment could be useful. That's why the first step is outside of the repeat/loop.

The Outcome?

I really like this scripted option. I can keep things "together" (the variables, settings, etc.) and the automation and flow is far easier to understand than my first go. This option could also be made more extensible/modular for other lights, etc. by changing variables and such. And it's clean compared to what I was seeing in Google results!

Best of All...

When I tweaked the various settings such as the brightness invocation point (lux) and delay, the office light comes on and increments almost perfectly to offset the setting sun. It's very subtle; many days I don't even notice the steps (or when the light initially turns on) until well into the cycle.

There are several ways to solve this particular problem, but this solution was relatively simple and easy for me to implement -- both ways! It was a fun automation to build out and think through. Full disclosure, though, I've since deleted the original automation and timer because they were no longer necessary!

Good luck!