Create combinatoric groups: Including and excluding entities from other groups

Tags: #<Tag:0x00007f73af593920>

Note: This first post contains the latest code as a summary of the entire discussion. Thanks to all the participants in this discussion who make this code better! @jhenkens, @jon102034050, @RonJ103

So, I was looking for a way to create groups by combining other groups. The use case is, e.g., to turn off all lights in my flat in case all lights with a physical switch have been turned off. Or, to turn off all lights except some dim lights in the kid’s room.

Implementing this in the actual code base would probably require some sophisticated way of determining the order in which groups are created. So, I started with a pure yaml approach and I think, I will stick to it.

The first part is a script to combine groups, which is defined as follows:

script:
  group_set:
    alias: Create groups with enhanced include/exclude statements
    sequence:
    - service: group.set
      data_template:
        object_id:  "{{ object_id }}"
        name:       "{{ name or object_id }}"
        icon:       "{{ icon or '' }}"
        all:        "{{ all or False }}"
        entities: >-
          {#- ENTITIES -#}
          {%- set ns = namespace(entities =
                          (entities.split(',')  if entities is string else entities or [])) -%}
          {#- TYPES -#}
          {%- for type in (types.split(',')     if types    is string else types    or []) -%}
          {%-   set ns.entities = ns.entities + (expand(states[type]) | map(attribute="entity_id") | list) -%}
          {%- endfor -%}
          {#- INCLUDE -#}
          {%- for group in (include.split(',')  if include  is string else include  or []) -%}
          {%-   if expand_groups == True -%}
          {%-     set ns.entities = ns.entities + (expand(group) | map(attribute="entity_id") | list)%}
          {%-   else -%}
          {%-     set ns.entities = ns.entities + states.group[group|regex_replace('^.*\.')].attributes.entity_id  | list -%}
          {%-   endif -%}
          {%- endfor -%}
          {#- EXCLUDE -#}
          {%- for group in (exclude.split(',')  if exclude  is string else exclude  or []) -%}
          {%-   if expand_groups == True -%}
          {%-     set ns.entities = ns.entities | reject('in',(expand(group) | map(attribute="entity_id") | list)) | list -%}
          {%-   else -%}
          {%-     set ns.entities = ns.entities | reject('in',  states.group[group|regex_replace('^.*\.')].attributes.entity_id) | list -%}
          {%-   endif -%}
          {%- endfor -%}
          {#- OUTPUT -#}
          {{ ns.entities|join(',') }}

It uses group.set to create the groups. You can supply most usual parameters to this service. Plus, in addition to the entities parameter, you can supply both a list of groups whose members to include in the new group as well as a list of groups whose members to exclude from the new group.

This script can then be called during the homeassistant.start event as follows:

automation:
- alias: Create test group
  trigger:
  - platform: homeassistant
    event: start
  action:
  - service: script.group_set
    data_template:
      name: Test group
      # Add specific named entity ids:
      entities: light.dummy,light.disco
      # Add all entities of a certain type (aka domain):
      types: light,switch
      # Add member entities of groups:
      include:
      - group.kids_room
      - group.bedroom
      # Remove member entities of other groups:
      exclude: group.night_lights,group.decorations
      # If True, recurse group members down to actual device entities:
      expand_groups: True

Note the two different ways to provide lists to the script.

Probably, an implementation in the actual group component would be better. But until then, this workaround should work around just fine… :wink: If I come up with more ideas on this, I’ll add them here.

Hope this helps someone…

See here for a some related threads:

7 Likes

This is awesome. Thank you. Looks like I can learn a bunch from this single script.

I think triggering your example automation off of group reloads would be a nice improvement. I tried this, but it didn’t work. Any ideas?

    trigger:
      - platform: event
        event_type: group.reload

You’re welcome! :slight_smile:

And I think your improvement really is a good idea. I have no clue, why it doesn’t work though. I don’t use the reload feature at all. Also, I think it wouldn’t even work for me, because it ignores packages. And I heavily rely on those…

If you make it work, let me know. I’ll try and edit the original post then. :+1:

It looks like this may work for triggering off of group reloads: Run automation / script on restart / core / groups / automation reload

I just need to say thank you for this - this is exactly what i’ve been looking for. You clearly have an entirely different view of how to use Homeassistant!

I used a modified version of this, post 0.104, to get all_lights back.

I added

        {# TYPES #}
        {% for type in (types.split(',') if types is string else types or []) %}
        {% set ns.entities = ns.entities + (expand(states[type]) | map(attribute="entity_id") | list) %}
        {% endfor %}

to the template section, and then just provide:

- id: create_all_lights_groups
  alias: Create All Light Groups
  trigger:
  - platform: homeassistant
    event: start
  action:
  - service: script.group_set
    data_template:
      object_id: all_lights
      name: All Lights
      types: "light"
1 Like

Nice extension! I added it to the first post, so people coming here get your feature right in the first spot.

Problem

Running this script twice in my startup only generates a group for the first instance. I believe the next call runs too early or something?

Note

If I call the script manually, later it does create the second group. Just doesn’t work on startup.

- id: create_all_lights_group
  alias: Create All Light Group
  trigger:
    - platform: homeassistant
      event: start
  action:
    - service: script.group_set
      data_template:
        object_id: all_lights
        name: All Lights
        types: "light"
        exclude: group.deconz_groups

- id: create_non_nightlight_group
  alias: "Create Non-Nightlight Group"
  trigger:
    - platform: homeassistant
      event: start
  action:
    - service: script.group_set
      data_template:
        object_id: non_nightlights
        name: "Non-Nightlights"
        types: "light"
        exclude: group.lights_night,group.deconz_groups

Expected:

group.all_lights and group.non_nightlights

Result:

only group.all_lights

Unfortunately, you can run scipts only once at a time. So, if you have two automations (as you do), it will try to run the script twice (at the same time, independently).

The solution would be to put both service calls in the same automation. Or to include an arbitray delay in one of the automations. Either way will call the scripts one after the other.

looks like this is broken with 0.107. https://github.com/home-assistant/core/pull/32021

I don’t update my HA too often, because of such issues… :roll_eyes:

As far as I can tell, it’s just the visible field, right? Can you tell what happens, if we still use it? Is it an error or just a warning? If you provide a corrected script, I’ll fix it in the original post…

:man_facepalming: yeah - that seems to have fixed it. Thanks!

You’re welcome. :slight_smile: What exactly did you change to make it work again? Could you post it here, so I can update the original post, please?

Sorry, should have been more clear. I followed your advice and removed visible

Thanks! I fixed that in the original post now.

This doesn’t seem to be working correctly for me anymore. I’m getting a lot of extra items in the group and sub-groups don’t seem to be excluded correctly.

Here is the group_set script:

  group_set:
    alias: Create groups with enhanced include/exclude statements
    sequence:
    - service: group.set
      data_template:
        object_id:  "{{ object_id }}"
        name:       "{{ name or object_id }}"
        icon:       "{{ icon or '' }}"
        all:        "{{ all or False }}"
        entities: >-
          {# ENTITIES #}
          {% set ns = namespace(entities =
                          (entities.split(',') if entities is string else entities or [])) %}
          {# TYPES #}
          {% for type in (types.split(',')     if types    is string else types or []) %}
          {% set ns.entities = ns.entities + (expand(states[type]) | map(attribute="entity_id") | list) %}
          {% endfor %}
          {# INCLUDE #}
          {% for group in (include.split(',')  if include  is string else include  or []) %}
          {% set ns.entities = ns.entities +               states.group[group|regex_replace('^.*\.')].attributes.entity_id  | list %}
          {% endfor %}
          {# EXCLUDE #}
          {% for group in (exclude.split(',')  if exclude  is string else exclude  or []) %}
          {% set ns.entities = ns.entities | reject('in',  states.group[group|regex_replace('^.*\.')].attributes.entity_id) | list %}
          {% endfor %}
          {# OUTPUT #}
          {{ ns.entities|join(',') }}

Here is my simplified automation (just a test case):

  - alias: Group Creation Test Case
    trigger:
      - platform: homeassistant
        event: start
    action:
      - service: script.group_set
        data_template:
          name: Group Exclude Test Case
          object_id: group_exclude_test_case
          types: light,switch
          include:
          - group.test_group_lights_switches
          exclude: group.computer_room 

Here are the definitions of the groups:

  test_group_lights_switches:
    name: Test Group Lights Switches
    entities:
      - group.kitchen
      - group.master_bedroom
      - group.computer_room

  kitchen:
    name: Kitchen
    entities:
      - switch.kitchen_light
      - switch.kitchen_recessed_lights
      - light.yeelight_kitchen_cabinets
      - light.yeelight_coffee_cabinets

  master_bedroom:
    name: "Master Bedroom"
    entities:
      - light.jeremys_lamp
      - light.nicholes_lamp
      - switch.master_light

  computer_room:
    name: "Computer Room"
    entities:
      - switch.computer_room_light

This is what the created group looks like:

There are a bunch of entities in the group that were not specified and group.computer room was supposed to be excluded. Note that switch.computer_room was excluded properly, but group.computer room (which contains switch.computer_room) was still included.

Also, can someone point me to the documentation for the group.set service call? I can’t find it. Thanks.

I think, you are using the script in a way it’s not supposed to be used:

    action:
      - service: script.group_set
        data_template:
          name: Group Exclude Test Case
          object_id: group_exclude_test_case
          types: light,switch # => THIS WILL ADD ALL LIGHTS AND SWITCHES TO YOUR GROUP
          include:
          - group.test_group_lights_switches # => THIS WILL ADD: group.kitchen,group.master_bedroom,group.computer_room
          exclude: group.computer_room # => THIS WILL EXCLUDE switch.computer_room_light

Seems like your output is exactly that (all switches and all lights plus the three groups minus one switch)…

group.set is documented in the group page, I believe.

1 Like

Thanks @akloeckner. I didn’t understand what the “types” argument was doing. I can get rid of that and I end up with this:

  - alias: Group Creation Test Case
    trigger:
      - platform: homeassistant
        event: start
    action:
      - service: script.group_set
        data_template:
          name: Group Exclude Test Case
          object_id: group_exclude_test_case
          include:
          - group.test_group_lights_switches
          exclude: group.computer_room 

I get this for the group:

It seems like my exclude doesn’t really work since the entity is excluded, but the parent group was still included. I thought this used to work for me, but maybe I just didn’t look closely enough.

1 Like