Switch everything off except excluded entities

When searching for a good way to turn off all lights except x, y and z I didn’t find any solutions that are functional and easy configurable at the same time.

So this is a minimal example of my approach. Everything is configured in one config file (scripts.yaml) and easy to maintain for future changes. This method can be adapted for other device classes as well. If you want to turn off all media players or switches excluding some entities, just change the service call in line 4 and the exclude list below.

Prerequisite: This approach requires the service to accept entity_ids as comma separated lists. At the time of writing I successfully tested this with the light, switch and media_player turn_on & turn_off service calls. There maybe others as well, but be prepared, that it might not work for your use case.

all_out:
  sequence:
    - service: light.turn_off
      data_template:
        entity_id: >
          {% set exclude_light = [
            'light.house_number',
            'light.aquarium',
            'light.entrance'
          ] %}
          {%- for device in states.light|rejectattr('entity_id','in',exclude_light)|rejectattr('state','in','off') %}{%- if loop.first %}{%- else %}, {% endif %}{{device.entity_id }}{%- if loop.last %}{% endif %}{%- endfor  %}

Some explanation for the data_template:

set exclude_light is a list of entity_ids that shouldn’t be affected by the script, i.e. excluded. You won’t get errors, if you forget to delete the comma after the last entry, but of course it’s good programming style if you remember to do it. :slight_smile:

states.light|rejectattr('entity_id','in',exclude_light)|rejectattr('state','in','off') is a set of (jinja) filters for objects.

It starts with all light entity objects known to HA (states.light). The first filter excludes all light entities from the list with an entity_id contained in the exclude_light list defined before: “…|rejectattr('entity_id','in',exclude_light)…”

The second filter excludes all lights which are already off and don’t need to be switched off again: “...|rejectattr('state','in','off')...”. You may omit this filter, as it not crucial for the template to work, but just turning off devices that actually need to be turned off significantly reduces the noise on the HA event bus if you have a lot of devices in your config.

The filtered list is fetched to the for loop. The loop concatenates the entity_ids ({{device.entity_id }}) of the remaining entities to a comma separated list, omiting commas for the first ({%- if loop.first %}{%- else %}, {% endif %}) and the last ({%- if loop.last %}{% endif %}) loop pass to suppress commas before the first and after the last entity_id.

10 Likes

Not a bad workaround, thanks!
Let’s not forget tho - the feature request is still valid and should be fairly easy to implement.

A simple built-in exclude function that everyone can start using easily would still be ideal.

Thank you for sharing this, and specially thank you for explaining the templating. Understanding/learning jinja is helpful to all.

I have to add, that there is one flaw in this method. If everything is turned off already, the template above provides an empty result for the entity_id, leading to errors like this in the log:

2018-11-23 13:18:48 ERROR (MainThread) [homeassistant.core] Invalid service data for light.turn_off: Entity ID  is an invalid entity id for dictionary value @ data['entity_id']. Got ''

To avoid this error, you may drop the filter for devices already turned off, i.e. deleting the “...|rejectattr('state','in','off')...” part from the filter.

Another way to avoid an empty result would be to add one of the turned off entities back to the list, but I didn’t find an elegant way to do that without completely rewriting the template. If you have a nice solution for this, don’t hesitate to post it here. :slight_smile:

2 Likes

Hi all,
I wanted to share another way that I had to come up with. I use Alexa Media Player, WLED instances and AdGuard Home that create many switches. I could probably hide those since I do not use them, but I wanted to solve it programmatically.

This creates a list with exclusions by pattern ‘starts with’ and ‘ends with’

lights_out:
  alias: 'All lights and switches off script'
  sequence:
    - service: light.turn_off
      data_template:
        entity_id: >
          {% set exclude_light = [
            'light.fake'
          ] %}
          {%- for device in states.light|rejectattr('entity_id','in',exclude_light) %}{%- if loop.first %}{%- else %}, {% endif %}{{device.entity_id }}{%- if loop.last %}{% endif %}{%- endfor  %}
    - service: switch.turn_off
      data_template:
        entity_id: >
          {%- for device in states.switch if not ( null 
            or device.entity_id.startswith('switch.laundry_closet')
            or device.entity_id.startswith('switch.christmas_lights')
            or device.entity_id.startswith('switch.front_door_outside_plug')
            or device.entity_id.startswith('switch.adguard')
            or device.entity_id.startswith('switch.aleksey')
            or device.entity_id.startswith('switch.this_device')
            or device.entity_id.startswith('switch.living_room')
            or device.entity_id.startswith('switch.bedroom')
            or device.entity_id.startswith('switch.everywhere')
            or device.entity_id.endswith('sync_send')
            or device.entity_id.endswith('sync_receive')) -%}
          {%- if loop.first %}{%- else %}, {% endif %}{{device.entity_id }}{%- if loop.last %}{% endif %}{%- endfor  %}

Great solution @derandiunddasbo ! I use it as script this way:

lights_off_except:
  icon: mdi:home-lightbulb
  fields:
    exclude_lights:
      description: 'Excluded lights as list'
  sequence:
    - service: logbook.log
      data_template:
        entity_id: script.turn_off_lights
        name: Exclude log
        message: "Turning of all lights except: {{ exclude_lights }}"
    - service: light.turn_off
      data_template:
        entity_id: >
          {%- for device in states.light|rejectattr('entity_id','in', exclude_lights )|rejectattr('state','in','off') %}{%- if loop.first %}{%- else %}, {% endif %}{{ device.entity_id }}{%- if loop.last %}{% endif %}{%- endfor  %}

And then calling it:

service: script.lights_off_except
exclude_lights:
  - light.switch_hallway_light_light
  - light.wall_switch_hallway_light 

It looks like it works fine. This is solution for my long living problem! Can be used with other solution that query the excluded group by some condition.

6 Likes

Oh nice, calling it as a separate script makes the config looking much cleaner. Thanks for sharing @lhoracek.

Similar version… use the homeassistant option to turn off lights, switches and fans with the ability to exclude. Few extra bits in there to stop adguard being messed with and switches that happen to be connected to wifi fans. Sonos doesn’t seem to like turn_off so split them off to a separate script to call media_stop instead. Feels clunky coming from other languages, but at least conditions stop errors

all_off:
  alias: Turn Off Fans, Lights, Switches and Sonos
  mode: single
  sequence:
    - service: script.all_media_off
      data_template:
        exclude: >
          {%- for device in states|selectattr('entity_id','in',exclude) %}{%- if loop.first %}{%- else %}, {% endif %}{{ device.entity_id }}{%- endfor  %}
    - service: script.all_devices_off
      data_template:
        exclude: >
          {%- for device in states|selectattr('entity_id','in',exclude) %}{%- if loop.first %}{%- else %}, {% endif %}{{ device.entity_id }}{%- endfor  %}

all_media_off:
  alias: Turn Off Sonos
  mode: single
  sequence:
  - condition: template
    value_template: >
      {{ states.media_player|rejectattr('entity_id','in',exclude )|selectattr('state','in','playing')|list|count > 0 }}
  - service: media_player.media_stop
    data_template:
      entity_id: >
        {%- for device in states.media_player|rejectattr('entity_id','in',exclude)|selectattr('state','in','playing') %}{%- if loop.first %}{%- else %}, {% endif %}{{ device.entity_id }}{%- endfor  %}

all_devices_off:
  alias: Turn Off Fans, Lights and Switches
  mode: single
  sequence:
  - condition: template
    value_template: >
      {% set count = namespace(value=0) %}
      {%- for device in expand(states.fan,states.light,states.switch)|rejectattr('entity_id','in', exclude )|rejectattr('state','in','off')
      if not ( null or device.entity_id.startswith('switch.adguard') or (device.entity_id.startswith('switch') and device.entity_id.endswith('fan'))) -%}
      {% set count.value = count.value + 1 %}{%- endfor  %}{{ count.value > 0 }}
  - service: homeassistant.turn_off
    data_template:
      entity_id: >
        {%- for device in expand(states.fan,states.light,states.switch)|rejectattr('entity_id','in', exclude )|rejectattr('state','in','off') if not ( null 
          or device.entity_id.startswith('switch.adguard') or (device.entity_id.startswith('switch') and device.entity_id.endswith('fan'))) -%}
        {%- if loop.first %}{%- else %}, {% endif %}{{device.entity_id }}{%- endfor  %}

Thanks for this! Finally was able to solve my problem. Modified a bit so it only takes entities from a group. Works like a charm.

  - service: homeassistant.turn_off
    data_template:
      entity_id: >
        {% set exclude_light = [
            'switch.dining_table',
            'switch.living_room_lamp',
            'light.family_room_lamp'
           ] %}
        {% for device in expand('group.actual_lights')
              | rejectattr('entity_id','in',exclude_light) -%}
          {{device.entity_id }}
          {%- if not loop.last -%}
            ,{{' '}}
          {%- endif %}
        {%- endfor  %}

I have to add, that there is one flaw in this method. If everything is turned off already, the template above provides an empty result for the entity_id , leading to errors like this in the log

As this thread still seems to be popular, I want to share a new version of my script, that actually handles the case of an empty list remaining after excluding a list of lights instead of throwing unnecessary errors to the log.

This version takes advantage of two recently introduced concepts for HA scripts & automations: variables and the choose action which is essentially used as an “if-then” clause.

Thus, instead of calculating the list of lights to be turned off directly as a template for the service’s entity_id, the new version pre-calculates this list, saves it to the variable list_light and only executes the light.turn_off service in case this variable isn’t empty.

  all_out:
    sequence:
      - variables:
          list_light: >
            {% set exclude_light = [
              'light.house_number',
              'light.aquarium',
              'light.entrance'
            ] %}
            {%- for device in states.light|rejectattr('entity_id','in',exclude_light)|rejectattr('state','in','off') %}{%- if loop.first %}{%- else %}, {% endif %}{{device.entity_id }}{%- if loop.last %}{% endif %}{%- endfor  %}
      - choose:
        - conditions:
            - condition: template
              value_template: "{{ list_light|length > 0 }}"      
          sequence:
            - service: light.turn_off
              data:
                entity_id: "{{ list_light }}"
2 Likes

Very very useful !! I use it every day!
Thanks for sharing.
Any suggest regarding turn off ONLY lights in state “on” more than 2 hours?
I am not able to handle this…
Thanks

Yes, that would be possible with an additional rejectattr-Filter. The third rejectattr filter in the following template filters out all entities that changed their state less than 7200 secs (2 hours) ago. As this filter only receives “on” lights because of the previous filter rejectattr('state','in','off'), all remaining entities must have changed from “off” to “on”. Thus, their last_changed attibute represents the last change to “on”. The statement seems to be a bit clunky, but essentially

strptime(((now().timestamp() - 7200) | timestamp_utc) ~ '+00:00', '%Y-%m-%d %H:%M:%S%z')

just creates a datetime value from 2 hours ago, because rejectattr needs two values with the same datatype for comparsion:

  all_out:
    sequence:
      - variables:
          list_light: >
            {% set exclude_light = [
              'light.house_number',
              'light.aquarium',
              'light.entrance'
            ] %}
            {%- for device in states.light
                  |rejectattr('entity_id','in',exclude_light)
                  |rejectattr('state','in','off')
                  |rejectattr('last_changed', '>', strptime(((now().timestamp() - 7200) | timestamp_utc) ~ '+00:00', '%Y-%m-%d %H:%M:%S%z')) %}
              {%- if loop.first %}
              {%- else %}
                ,
              {%- endif -%}
                {{device.entity_id}}
              {%- if loop.last %}
              {% endif %}
            {%- endfor  %}
      - choose:
        - conditions:
            - condition: template
              value_template: "{{ list_light|length > 0 }}"      
          sequence:
            - service: light.turn_off
              data:
                entity_id: "{{ list_light }}"

Please be aware that this doesn’t work very well, i.e. not at all, after HA restarts, because the last_changed attributes of all HA entities update on every restart. If you don’t expect this, it may drive you crazy when trying to find out wth your template isn’t working as expected… :wink:

Oh, and yes, the same can of course be achieved with an appropriate selectattr filter instead of rejectattr. This may even be a bit more straight forward, but in the end it’s just a matter of personal preference.

Hi

When cutting and pasting this exact script into a new script in my system, it always comes up with

"Message malformed: extra keys not allowed @ data['all_out']" 

when I try and save.
Has anyone else had this issue and can suggest what the problem is? Changing the excluded entities to ones in my system doesn’t help.

Thanks in advance

Peter

UPDATE:

I got it work with this code instead

alias: New Script
variables:
  list_light: |
    {% set exclude_light = [
      'light.outside_rear_security_light_dimmer',
       ] %} {%- for device in
       states.light|rejectattr('entity_id','in',exclude_light)|rejectattr('state','in','off')
       %}{%- if loop.first %}{%- else %}, {% endif %}{{device.entity_id }}{%-
       if loop.last %}{% endif %}{%- endfor  %}
sequence:
  - condition: template
    value_template: '{{ list_light|length > 0 }}'
  - service: light.turn_off
    target:
      entity_id: '{{ list_light }}'
mode: single

UPDATE 2:

Have discovered that Scenes effectively do all the above automatically. I.e. if you have a Home Assistant Scene for 10 lights (e.g. all lights are off), then when you turn the Scene on it will only send zwave messages for the lights which are not already off.

You still have to manually update your scene for new lights/exception etc. But for smaller groups of entities, then it’s easier than doing all the above coding.

I use this to turn off specific lights that are on (and not the containing groups):

alias: Turn Off Lights That Are On
sequence:
  - variables:
      on_list: >-
        {%- set exclude = exclude if exclude is defined else [] -%} {{
        states.light 
          | rejectattr('entity_id','in',exclude )
          | selectattr('attributes.is_hue_group','undefined')
          | selectattr('attributes.entity_id', 'undefined')
          | selectattr('state','eq','on')
          | map(attribute='entity_id')
          | join(',')
        }}
  - choose:
      - conditions:
          - condition: template
            value_template: "{{ on_list | length > 0 }}"
        sequence:
          - service: homeassistant.turn_off
            data_template:
              entity_id: "{{ on_list }}"
mode: single
icon: mdi:lightbulb-off

Then you can call it with another script or automation to control the exclusions (this one specifically excludes my 8 external lights as an example):

alias: Turn Off Internal Lights
sequence:
  - service: script.turn_off_lights_that_are_on
    data:
      exclude:
        - light.gacw
        - light.galw
        - light.garw
        - light.mkp1
        - light.fhpl
        - light.fhpr
        - light.gpw1
        - light.drbl
mode: single
icon: mdi:lightbulb-group-off

And for those with things to switch off that don’t appear as ‘lights’, the on_list variable template can be changed to look something like this to handle other domains and potential ‘on’ states:

{%- set exclude = exclude if exclude is defined else [] -%}
{%- set domains = ['light','switch'] -%}
{%- set on_states = ['on','active'] -%}
{{ states 
  | selectattr('domain','in', domains) 
  | rejectattr('entity_id','in', exclude )
  | selectattr('attributes.is_hue_group','undefined') 
  | selectattr('attributes.entity_id', 'undefined') 
  | selectattr('state','in',on_states)  
  | map(attribute='entity_id') 
  | join(',')
}}

In my case I didn’t have many uses for this kind (exclude one/more device and turn off rest), so instead of making a script I inlined it…

alias: Turn off basement office at 1AM
description: ""
trigger:
  - platform: time
    at: "01:00:00"
condition:
  - type: is_not_present
    condition: device
    device_id: 998920b147d40f1be45e08400575fde5
    entity_id: 030503c16f738e5c78929ca06da37988
    domain: binary_sensor
  - condition: time
    weekday:
      - tue
      - wed
      - thu
      - fri
    after: "00:59:00"
action:
  - data_template:
      entity_id: >
        {% set area_id = 'basement_office' %} 
        {% set exclude_device_names = ['sonoff1_basement_office'] %} 
        {% set exclude_device_ids = exclude_device_names | map('device_id') | list %} 
        {% set devices = area_devices(area_id) %} 
        {% set filtered_devices = devices | reject('in', exclude_device_ids) %} 
        {% set device_entities = filtered_devices | map('device_entities') | list|
        sum(start=[]) %}

        {{ device_entities | join(', ') }}
    action: homeassistant.turn_off
mode: single

Works well…