Best Practices for complex lighting rules

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.

It’s only weird because you are unfamiliar with it. It’s a long-standing practice. If you search the forum you will find many examples of templated service calls.

FWIW, not too long ago there was a distinction between service_template and service but then templating capability was added to service and the other option name was deprecated.