Best Practices for complex lighting rules

Hey all–

I’m still trying to get used to HA’s programming model and trying to figure out how to solve certain problems. Here’s one that I’m trying to solve now:

I have driveway lights that I currently turn on at 15 minutes before Sunset and turn off at 1am. This is controlled with a very simple automation.

I want to add a rule such that if it’s dark (let’s say for simplicity sunset to sunrise), if the garage door opens, the garage lights turn on for 10 minutes and then turn off.

Sounds simple right? Just add another rule. Then I start thinking about the edge cases. What if the garage door opens at 12:58am? With a stateless event-driven approach, the problem becomes very complicated quickly as you add more rules. I’d also like to add a third rule that says if the switch ever controlled manually, the “manual override” will take precedence for some amount of time (say an hour). You can start to see even more edge cases here.

My instinct is to keep everything as simple and separated as possible, but it results in a lot of overhead. Implement each of those three rules either as a automation-controlled input_boolean, or as a sensor template:

Rule 1: Sunset-15m to 1am: Is it preferable to implement this as an sensor template, so that I don’t need a separate automation and input_boolean? I’m probably missing something obvious, but after sunset, how do I check if the current time is after the last sunset (there doesn’t appear to be a Sun variable associated with this–only next sunset). Call this the driveway_nighttime_sensor.

Rule 2: This is probably an automation + input_boolean (pseudocode):
Trigger is “garage door state” from: “closed” to: “open”
condition: it’s dark (sunset to sunrise)
Action:

  • Set “garage door opened recently input_boolean” to “true”
  • Delay: 10
  • Set “garage door opened recently input_boolean” to “false”

Rule 3: Similar to rule 2, but with two input_booleans, one to capture whether we’re in “manual override mode” and one to capture what the overridden state is. Yuck.

Now, how to combine everything?
Is it better to combine everything as a template sensor, or again to use an automation and action to control the light? The latter would likely involve duplication between the trigger and the condition or action:

Template sensor approach (pseudocode):
value_template: if (manual override is in effect) -> manual override state
else -> driveway_nighttime_sensor OR garage door opened recently input_boolean
Paired with an automation that triggers on changes of the above template sensor to turn the light on and off.

OR

A single automation (pseudocode):
trigger: “change in manual override in effect” OR “change in manual override state” OR
“change in garage door opened recently input_boolean” OR
“change in driveway_nighttime_sensor”
(a few ways to approach the rest of this: condition/service template, action to call service light.turn_on with value_template, etc.)

Are there any efficiency concerns to be aware of with the above approach? For example, for Rule #1, using a state-evaluation approach likely causes that state to be re-evaluated whenever the time changes (every minute? every second?).

There’s a lot of information here–I’m basically looking for best practices how to solve challenges like these using the tools HA gives us. I’m also looking for recommendations on tried-and-true extensions that people might use to make it easier to solve these programs and easily maintain these configurations (few interdependencies, low duplication, etc.). Any optimizations to the approach above, or even a completely different way of thinking about the problem is absolutely welcome.

Thank you in advance for any time you’re willing to spend and for any information you’d willing to share.

I may not really help here, but i had also some no straight forward rules and decided to use Node-RED aus automation tool.

It is well integrated with Home Assistent and allows very flexible rules and flows to integrate with it.

Downside: another server component (i use docker therefor this is not a real downside for me)

Maybe it helps or brings up new ideas.

The quantity of automations (and associated entities) needed to create a solution is entirely at your discretion. Basically, do whatever is easier for you to create and maintain.

For example, I have a single automation that handles the control of two lights (interior/exterior) whenever the garage door is open/closed after dark. It monitors several entities to do its job:

  • binary_sensor for the garage door (contact sensor)
  • timer for determining when to turn off interior/exterior lights
  • binary_sensor for garage occupancy (motion sensor)

Here’s what that single automation does:

If it’s after sunset and the garage door opens:

  1. Turn on the garage light and the exterior light (but only if they are currently off).
  2. Start the timer with 10 minutes.

If it’s after sunset and the garage door closes:

  1. Start the timer with 2 minutes.

When the timer expires:

  1. Turn off the exterior light.
  2. If the garage is unoccupied, turn off the garage light.
  3. If the garage is occupied, wait for it to become unoccupied before turning off the garage light.

Nice. How are you implementing “garage occupancy” using a binary_sensor? I assume there would be some time delay after last motion sensed before declaring “unoccupied”. Same with garage door, although this one is probably easier since either the door is open, or it’s not. Insights appreciated!

I have a switch in the garage, to control the garage light, that also has built-in sensors reporting the following:

  • temperature
  • motion
  • occupancy

“Occupancy” is reported as:

  • on the moment it detects motion
  • off after at least 1 continuous minute of no motion detected

FWIW, if you have an existing motion sensor, you can use it in a Template Binary Sensor to report occupancy. The template would report off only if the time difference between now and the last time the motion sensor had changed to off is greater than 1 minute (or more; your choice).

The garage door’s state, open/closed, is detected by a (hard-wired) magnetic contact sensor

This is the exact tradeoff I’m trying to weigh, and I’m going to explore this some more. I was reading a bunch of threads last night and came across this one:

and was interested specifically in this quote:

Maybe I’m missing the context, but it seems like your template sensor would only be able to rely to now() to update (there isn’t anything else changing). Can you post your template sensor for reference so I have a better idea of what exactly is being registered and how it works?

Thanks.

As mentioned, I use the switch’s built-in occupancy sensor. What I described is how to emulate one using a Template Binary Sensor. At the moment, I don’t have the time to create and test the template for you.

Gotcha. Which device are you using?

I have completely modelled my lighting scenarios using a statemachine in NR.

Here are my automations for controlling my bathroom vanity light just as an example of what you can do. This particular light is controlled by motion, timers, occupancy mode, illuminance.

Bathroom Light Automation
- id: light_upstairs_bathroom_motion_lights
  alias: '[Light] Upstairs Bathroom Motion Lights'
  description: Turn on lights when motion detected in bathroom.
  initial_state: true
  mode: restart
  trigger:
    - platform: state
      entity_id: binary_sensor.upstairs_bathroom_sensor_motion
      to: 'on'
  condition:
    - condition: state
      entity_id: input_boolean.light_automation
      state: 'on'

    - condition: state
      entity_id: input_boolean.alarm_triggered
      state: 'off'

    - condition: or
      conditions:
        - condition: state
          entity_id: binary_sensor.auto_light_on
          state: 'on'

        - condition: numeric_state
          entity_id: sensor.upstairs_bathroom_sensor_illuminance
          below: 50
  action:
    - choose:
        - conditions:
            - condition: state
              entity_id: input_select.occupancy_mode
              state: Night

          sequence:
            - condition: state # in sequence so we don't trigger default if timer on
              entity_id: timer.upstairs_bathroom_vanity_light
              state: idle

            - service: light.turn_on
              entity_id: light.upstairs_bathroom_vanity_rgb_light
              data:
                profile: red_dim
      default:
        - choose:
            - conditions:
                - condition: state
                  entity_id: binary_sensor.quiet_hours
                  state: 'on'

              sequence:
                - condition: state # in sequence so we don't trigger default if timer is on
                  entity_id: timer.upstairs_bathroom_vanity_light
                  state: idle

                - service: light.turn_on
                  data:
                    entity_id: light.upstairs_bathroom_vanity_rgb_light
                    brightness: 40
                    #OPTION profile: sunrise_low
          default:
            - service: light.turn_on
              data:
                entity_id: light.upstairs_bathroom_vanity_rgb_light
                brightness: 125
                #OPTION profile: warm

            - service: light.turn_on
              data:
                entity_id: light.upstairs_bathroom_shower_light
                brightness: 70

#######################################################################################################################
## Light - Upstairs Bathroom Vanity Light Auto Off
#######################################################################################################################
- id: light_upstairs_bathroom_vanity_light_auto_off #OCC
  alias: '[Light] Upstairs Bathroom Vanity Light Auto Off'
  description: Turn off bathroom vanity light.
  initial_state: true
  trigger:
    - platform: state # specify from state, unknown at startup
      entity_id: binary_sensor.upstairs_bathroom_sensor_motion
      to: 'off'
      from: 'on'
      for:
        minutes: 5

    - platform: state
      entity_id: input_select.occupancy_mode
      to:
        - Away
        - Vacation
        - Night
      for:
        minutes: 2 # allow timer to cancel, occupant to leave

    - platform: state # req in case motion/timer expire during restart
      entity_id: input_boolean.startup_pending
      to: 'off'

    - platform: event
      event_type: timer.finished
      event_data:
        entity_id: timer.upstairs_bathroom_vanity_light
  condition:
    - condition: state
      entity_id: input_boolean.light_automation
      state: 'on'

    - condition: template
      value_template: "{{ is_state('input_boolean.presence_automation','on') if trigger.entity_id == 'input_select.occupancy_mode' else true }}"

    - condition: state
      entity_id: input_boolean.alarm_triggered
      state: 'off'

    - condition: state
      entity_id: timer.upstairs_bathroom_vanity_light
      state: idle

    - condition: state
      entity_id: binary_sensor.upstairs_bathroom_sensor_motion
      state: 'off'
  action:
    - choose:
        - conditions:
            - condition: state
              entity_id: input_select.occupancy_mode
              state: Night

          sequence:
            - service: light.turn_on # turn on the light back to night mode setting
              data:
                entity_id: light.upstairs_bathroom_vanity_rgb_light
                profile: red_min
                transition: 2
      default:
        - service: light.turn_off
          data:
            entity_id: light.upstairs_bathroom_vanity_rgb_light
            transition: 1

#######################################################################################################################
## Light - Upstairs Bathroom Vanity Light Timer Finished
#######################################################################################################################
- id: light_upstairs_bathroom_vanity_light_timer_finished
  alias: '[Light] Upstairs Bathroom Vanity Light Timer Finished'
  description: Turn light flux back on.
  initial_state: true
  trigger:
    - platform: event
      event_type: timer.finished
      event_data:
        entity_id: timer.upstairs_bathroom_vanity_light
  condition:
    - condition: state
      entity_id: input_boolean.light_automation
      state: 'on'

    - condition: state
      entity_id: script.start_shower
      state: 'off'
  action:
    - service: switch.turn_on
      entity_id: switch.light_flux_bathroom

ecobee Switch+

It integrates with Home Assistant via the Homekit Controller integration.

That’s pretty old information. Recent releases are now able to properly update template sensors even if they’re using now().

Thanks. BTW, I just downloaded Sun2 and I’m already super-impressed. I downloaded it because I want to try writing state-driven lighting rules instead of event-driven, and it’s the easiest way to get today’s sunset. Thanks for making this.

1 Like

Based on all of the above, as well as what I read in multiple other threads, this is what I came up with in order to:
a) minimize the number of extra entities
b) consolidate rules that affect the same entity
c) avoid duplication of code as much as possible

I debated using a template sensor to calculate the correct state of the driveway light, paired with an automation to turn the light on/off, so that I could rely on the automatic event registration to allow entities only to be referenced once. I elected to go with a single automation instead, just to avoid the extra sensor.

- id: '12345'
  alias: Driveway Light Automation
  description: ''
  trigger:
  - platform: time_pattern
    minutes: /1
  - platform: state
    entity_id: binary_sensor.driveway_dusk_dawn
  - platform: state
    entity_id: input_datetime.driveway_motion_triggered_time
  - platform: state
    entity_id: cover.garage_door_1
  - platform: state
    entity_id: cover.garage_door_2
  condition: []
  action:
  - service: light.turn_on
    data:
      brightness: >-
        {% set LIGHT_DELAY_SEC = 120 %}
        {% if
          as_timestamp(now()) >= as_timestamp(states('sensor.sunset')) or
          now() < now().replace(hour=02).replace(minute=00).replace(second=00).replace(microsecond=00) or
          ( (is_state('binary_sensor.driveway_dusk_dawn', 'on') or
             as_timestamp(now()) < as_timestamp(states('sensor.sunrise'))
            ) and
            (as_timestamp(now()) - as_timestamp(states('input_datetime.driveway_motion_triggered_time')) <= LIGHT_DELAY_SEC or
             is_state('cover.garage_door_1', 'open') or
             as_timestamp(now()) - as_timestamp(states.cover.garage_door_1.last_changed) <= LIGHT_DELAY_SEC or
             is_state('cover.garage_door_2', 'open') or
             as_timestamp(now()) - as_timestamp(states.cover.garage_door_2.last_changed) <= LIGHT_DELAY_SEC
            )
          )
        %}
          255
        {% else %}
          0
        {% endif %}"
    entity_id: light.driveway_light
  mode: single

Translation:

Re-evaluate the state of the light on:

  1. Every minute
  2. Change of dusk/dawn state
  3. Change of driveway motion trigger time
  4. Change of garage door 1 state
  5. Change of garage door 2 state

In order to consolidate on and off into a single automation, I used the trick to universally call the light.turn_on service and only change the brightness to represent on and off (255 and 0 respectively). The logic represents when the light should be on (otherwise it should it be off)

Set a constant of 120 seconds as delay before turning off the light

The light should be on unconditionally from sunset to 2am

  1. The time is after today’s sunset (and before midnight as implied by how this sunset sensor is updated) OR
  2. The time is before 2 AM
    • Is there a cleaner way to do a time comparison with a fixed time?

Other conditions can turn the light on only if it’s dark outside

  1. The time is before sunset (and after midnight as implied by how the sunrise sensor is updated) OR

  2. The dusk sensor indicates that its dark

  3. AND

  4. The driveway motion sensor has triggered within the last 120 seconds OR

  5. The garage door 1 is currently open OR

  6. The garage door 1 has changed state within the last 120 seconds OR

  7. The garage door 2 is currently open OR

  8. The garage door 2 has changed state within the last 120 seconds

I would really appreciate feedback on this approach and any ideas for optimizations. I tried to make relative_time work but I think I resolved that it only produces a string that is impractical for comparison.

Also, I don’t understand states('cover.garage_door_1') vs. states.cover.garage_door_1 other than that the documentation indicates to avoid the latter whenever possible. However I’m unable to access last_changed any other way, failing to get the states() and state_attr() functions to retrieve it.

Thanks everyone.

Update with my own optimizations.

There were a lot of DateTime comparisons with now() which I factored out into a macro called delta(). It takes a DateTime and converts it to a timestamp and subtracts now() as a timestamp to produce a delta. A negative result indicates that the given DateTime is in the past and a positive result indicates that it is in the future.
A variable NOW is set to zero and defined somewhat redundantly, but I think it makes the script a bit more readable:
NOW >= delta(states('sensor.sunset')) | int is much more understandable as “if it is after sunset” much easier than delta(states('sensor.sunset')) | int <= 0

The same macro can be used for the LIGHT_DELAY_SEC calculations by adding the offset to the delta and comparing to NOW.

I also added the whitespace eliminators to every {% %} pair. Is there a downside to doing this all the time (assuming you don’t actually care about whitespace)?

brightness: >
  {%- macro delta(dt) -%}{{ as_timestamp(dt) - as_timestamp(now()) }}{%- endmacro -%}
  {%- set NOW = 0 -%}
  {%- set LIGHT_DELAY_SEC = 120 -%}
  {%- if
    (NOW >= delta(states('sensor.sunset')) | int or
     NOW < delta(now().replace(hour=02).replace(minute=00).replace(second=00).replace(microsecond=00)) | int
    ) or
    ( (is_state('binary_sensor.driveway_dusk_dawn', 'on') or
       NOW < delta(states('sensor.sunrise')) | int
      ) and
      (NOW < delta(states('input_datetime.driveway_motion_triggered_time')) | int + LIGHT_DELAY_SEC or
       is_state('cover.garage_door_1', 'open') or
       NOW < delta(states.cover.garage_door_1.last_changed) | int + LIGHT_DELAY_SEC or
       is_state('cover.garage_door_2', 'open') or
       NOW < delta(states.cover.garage_door_2.last_changed) | int + LIGHT_DELAY_SEC
      )
    )
  -%}
    255
  {%- else -%}
    0
  {%- endif -%}

Also, any thoughts about how I could get inline comments within the if statement? Not sure if Jinja allows this.

Again, thoughts and feedback welcome.

Only thing I notice right away is the replace() which you can combine into one.

But pff, it would not be my hobby to do all this in jinja… Still rather do this in a yaml automation (or two…)

Thanks, I’ll look into that.

Jinja is definitely not my favorite, but not sure how to do this in multiple automations that are knowledgeable of each other such that they don’t stomp over the others’ actions.

Maybe organization is the key. I hate having a million automations, input_datetimes, other entities, etc. that are logically related and can’t be groups/defined together. Any thoughts on this to make the definition of additional automations more palatable?

You can add helpers to area’s as well. And I always prefix a helper, automation, script etc with the location.

And about the first I would just use a timer


Trigger

  • Sunset
  • Garage door 1 opens
  • Garage door 2 opens
  • Drive way motion sensor triggers

Conditions:

  • OR
    • After sunset
    • Before sunrise
    • Dusk sensor dark

Actions

  • Turn on light
  • (re)start timer of 120

Trigger

  • Garage 1 closed
  • Garage 2 closed

Condition

  • -

Actions

  • (re)start timer of 120

Trigger

  • Sunrise
  • Timer finished
  • 2:00
  • HA start

Condition

  • >2:00
  • Before sunset
  • OR
    • After sunrise
    • AND
      • Garage 1 closed
      • Garage 2 closed

Action

  • Turn off light

I think…

Why? Your template is simply determining whether to turn the light on or off. That can be done by templating the service call.

In its simplest form:

- service: "light.turn_{{ 'on' if <something is true> else 'off' }}"
  entity_id: light.whatever

Well, this a different way to do it, but it seems weird to break up the building of a string literal which references a service call with a very complex, multiline conditional, instead of a simple numeric value. I did debate between the two though.