Script to turn off all lights that have been on for more than X hours. Need help with creating a list inside a loop

I have created a script that turns off all the lights in the house that have not been updated for last 10 hours. This script will be triggered by a “sunrise” automation. (Would love to change that constant 10 hours to be last sunset, but that’s for the future). Here is my script:

turn_off_forgotten_lights:
  alias: "Turn Off Forgotten Lights"
  sequence:
    - service: light.turn_off
      data_template:
        entity_id: >
            {%- for entity_id in states.light|selectattr('state','eq','on')|map(attribute='entity_id') -%}
              {%- set parts = entity_id.split('.') -%}
              {%- if (as_timestamp(now()) - as_timestamp(states[parts[0]][parts[1]].last_updated)) > 36000 -%}
                {{ entity_id }},
              {%- endif -%}
            {%- endfor -%}light.switchlinc_dimmer_44_XX_de

As you can see my problem is that comma. I can’t figure out a way to create a proper list. I tried to put comma before the entity only when it is not the first entity. To do so I created a variable to keep track of first entity_id. But creating this variable outside the for loop gets out of scope, and I can’t create it inside the loop as it would keep resetting. I also cannot use loop.first or loop.last because there is an if inside which means first entity doesn’t necessarily mean first loop.

For now, I end up putting a light entity (light.switchlinc_dimmer_44_XX_de) at the end so that I can end the list without a comma. This light is in my crawlspace and is pretty much always off, and I am hoping that no one will be in the crawlspace during sunrise. But I need a proper solution.

Can someone help me with creating a proper comma separated list without the hard-coded light in it? Any way to create arrays and I can then join the array with .join(',')?

turn_off_forgotten_lights:
  alias: "Turn Off Forgotten Lights"
  sequence:
    - service: light.turn_off
      data_template:
        entity_id: >
          {% set from = (utcnow()|as_timestamp - 10*60*60)|timestamp_utc %}
          {% set from = strptime(from ~ '+0000', '%Y-%m-%d %H:%M:%S%z') %}
          {{ states.light|selectattr('state','eq','on')
                         |selectattr('last_changed','<',from)
                         |map(attribute='entity_id')|join(', ') }}

This first gets a string representation of the UTC time 10 hours ago. Then it converts that back into a Python time zone aware datetime object. Then it selects all lights that are on and have a last_changed field that is later than 10 hours ago, grabs their corresponding entity_id's, and joins them into a comma separated list.

Note that you need to guard against calling this script if none of the lights meet that criteria, because then it will call the service with an empty entity_id option. Or you could add a condition to the script so the service is only called if at least one light meets the criteria have it return none in this case:

turn_off_forgotten_lights:
  alias: "Turn Off Forgotten Lights"
  sequence:
    - service: light.turn_off
      data_template:
        entity_id: >
          {% set from = (utcnow()|as_timestamp - 10*60*60)|timestamp_utc %}
          {% set from = strptime(from ~ '+0000', '%Y-%m-%d %H:%M:%S%z') %}
          {% set lights = states.light|selectattr('state','eq','on')
                         |selectattr('last_changed','<',from)
                         |map(attribute='entity_id')|join(', ') %}
          {{ lights if lights | length > 0 else 'none' }}

EDIT: Updated per suggestion & correction noted in below posts.

3 Likes

Thank you very much! And thank you for improving my solution with the condition.

1 Like

I recently fulfilled the same requirement by supplying none when there are no entities that meet the criteria. It eliminates the need to duplicate the action template in a condition.

turn_off_forgotten_lights:
  alias: "Turn Off Forgotten Lights"
  sequence:
    - service: light.turn_off
      data_template:
        entity_id: >
          {% set from = (utcnow() | as_timestamp - 10*60*60) | timestamp_utc %}
          {% set from = strptime(from ~ '+0000', '%Y-%m-%d %H:%M:%S%z') %}
          {% set lights = states.light
                 | selectattr('state', 'eq', 'on')
                 | selectattr('last_changed', '<', from)
                 | map(attribute='entity_id') | join(', ') %}
          {{ lights if lights | length > 0 else 'none' }}

So when there are no entities to turn off, the action quietly does nothing.


EDIT
Included change suggested by Mariusthvdb. Replaced > with <.

NOTE
Solution post now incorporates all suggested changes.

Since I was looking for something like this (find left entities in an on state for over x hours) was glad to find this.
Still, this doesn’t really do what OP asked does it? This turns off all lights that are on, and are turned on within the last 10 hours.

It doesnt only turn off lights that have been on for the last 10 hours (or more)…
or am I misreading (and testing ) this?

if a light would have been left alone for 10 hours, shouldn’t it be

| selectattr('last_changed', '<', from)
?

Ah, yes, much better! I keep forgetting none was added back in the 0.106 release.

Yes, I believe you are correct. Seems I got it backwards. In fact when I tested the template, it returned only lights that had gone on more recently than 10 hours ago.

I’ll update my solution above based on your and @123’s correction/suggestion.

I think I need more sleep!!! :tired_face:

@dev-home, please see updated solution.

Since all other(core or custom) integrations only show the next sunset (or am I missing the obvious here Phil) wouldn’t be the easiest way to do this to create an input_boolean.sun and set that automatically on sun_rise/sun_set. So at all times, you would know the last time it changed, and use that in the script at hand?

Well, my sun2 custom integration does have today, tomorrow & yesterday attributes for the sunset sensor, so, yeah, if the OP used that it could be:

turn_off_forgotten_lights:
  alias: "Turn Off Forgotten Lights"
  sequence:
    - service: light.turn_off
      data_template:
        entity_id: >
          {% set lights = states.light|selectattr('state','eq','on')
               |selectattr('last_changed','<',state_attr('sensor.sunset', 'yesterday'))
               |map(attribute='entity_id')|join(', ') %}
          {{ lights if lights | length > 0 else 'none' }}
1 Like

Somehow I knew you’d figured that out already :wink:
thanks!

and here I was fiddling with the likes of:

  - alias: Sunrise sunset
    trigger:
      - platform: sun
        event: sunrise
      - platform: sun
        event: sunset
    action:
      service_template: >
        input_boolean.turn_{{'on' if trigger.to_state.event == 'sunrise' else 'off'}}
      entity_id: input_boolean.sun_state

to create and set a boolean to use in the script… duh

Well that certainly is one way to capture the most recent sunrise or sunset time. BTW, it would need to be trigger.event == 'sunrise'

:blush:
thanks

This is an awesome community! Thanks a lot @123 and @pnbruckner.

I also missed the > vs <. Good catch @Mariusthvdb