Create combinatoric groups: Including and excluding entities from other groups

Think of the script as a set operation:

group.group_exclude_test_case = group.test_group_lights_switches \ group.computer_room

If you want to exclude the entity group.computer_room, you will have to wrap it in another group. There is no way (currently) to directly exclude single entities.

It seems like there is a “recursion depth” limitation. I think the script only goes down one level and finds whatever is there. This can be physical items such as lights, switches, locks, etc or non-physical items like more groups which could then contain even more groups or physical items.

If we made a list of physical items for the created group (only entities like lights, switches, etc without any groups) then when we excluded a group (or entity) it could create a list of physical items to remove from the “include list”.

the script only goes down one level

That’s true and by design. I thought about recursing the groups, but I ditched it for explicity.

Thanks for the replies. I’m starting to understand what is going on now. I’d like to work on recursing the groups, but I am having trouble testing the code in the template editor. Maybe due to how the variables are passed in? Is there a trick to putting the code in the template editor to play with it? Are you able to execute the code in the template editor?

You will need to

{% set include = '...' %}

for all variables you’d like to use. Then you can paste the script after that set’ing…

If you find a good way to recurse, maybe you make it optional via a recurse option and share the improved script here?

1 Like

Thanks @akloeckner. I was leaving off the “set” part when using the template editor. That helped a lot. I have modified your code to allow for recursion of the groups:

  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(',') }}

Example automation:

  - 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
          expand_groups: True

This is the group that was created (works as desired):

@RonJ103, cool! Just to make sure: the exclude part has no expand in it. Is that intentional? I’ll add your changes to the first post then…

No, that wasn’t intentional, it was an oversight. I have fixed the code above.

Great, thank you! I included in the first post, so everyone can find it right away!

1 Like

image

How could i use this function and exclude the attribute is_deconz_group ? so it doesn’t pick up light groups from deconz?

You could simply use data_template when calling the built-in group.set script. Then you could do some templating, more or less like the following:

entities: {{ states.light | selectattr(... deconz stuff ...) | join(',') }}

You can test this in the template editor within Lovelace before putting it to work in your config.

Thanks for the pointer… gonna try experimenting a bit to try to understand where I’d put that line

With the new version 2021.4.6, I got warnings for undefined input variables, so here goes the update with checks against undefined variables. In this course, I also removed again the types input in favor of a slightly more greedy usage of expand(). So, if you want to use the new version, just include your types, e.g. include: light,switch.

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

(updated also in top post)

2 Likes

Can you use masks with the exclude in this? eg to exclude any lights prefixed “browser_” or such?

I don’t think that will work: include and exclude are basically limited to what expand() can do. And I don’t think it can do wildcards (*) or the likes.

However, it would be possible to

  1. create a specific group containing all your browser_* entities. You’ll have to name and list them explicitly for that, though.
  2. exclude that specific group.

If that is not what you are looking for, you could craft your own template and pass it to the built-in group.set…

FWIW, selecting entities based on a common characteristic becomes very easy if that characteristic is stored as a custom attribute as opposed to a sub-string within the entity’s name. It’s also much neater because it avoids long-winded entity names.

You are free to add as many custom attributes as you wish to an entity. For example if I create a custom attribute called battery_type, I can select all sensor entities with a specific type of battery using:

{{ states.sensor | selectattr('attributes.battery_type', 'eq', 'CR123A') | map(attribute='entity_id') | list }}

If adding custom attributes is of no interest to you, you may want to consider using the recently introduced match filter which easily lets you select entities using a regular expression to match a sub-string within their name.

Thanks for the tip… my main issue is that browser_mod adds a “light” as a browser control, fortunately when I checked, it does toss a type tag in there so I should be able to filter that out.

Tried adding a mod to the script that would remove anything that had the attribute type browser_mod but a few hours later it is still just failing completely… sigh… bedtime.

Not sure what you were using but rejectattr() would be my choice.

Still having issues here… tried this again and it seems like it somehow is colliding with the browser_mod plugin unfortunately.

Not sure if anyone can make sense of this, but this is what the log spits out.

2021-08-24 19:46:26 ERROR (MainThread) [homeassistant.helpers.template] Template variable error: 'browser_mod' is undefined when rendering '{# ENTITIES #} {% set ns = namespace(entities =

                ([] if entities == null else entities.split( ',' ) if entities is string else entities or [])) %}

{# TYPES #} {% for type in  ([] if types    == null else 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 ([] if include  == null else include.split ( ',' ) if include  is string else include  or [])  %} {%-   if expand_groups == True -%} {%-     set ns.entities = ns.entities +              (expand(states[group])| map(attribute="entity_id")  | list)%} {%-   else -%} {%-     set ns.entities = ns.entities +              states[group].attributes.entity_id                  | list -%} {%-   endif -%} {% endfor %} {# EXCLUDE #} {% for group in ([] if exclude  == null else exclude.split ( ',' ) if exclude  is string else exclude  or [])  %} {%-   if expand_groups == True -%} {%-     set ns.entities = ns.entities | reject('in', (expand(states[group])| map(attribute="entity_id")) | list)%} {%-   else -%} {%-     set ns.entities = ns.entities | reject('in', states[group].attributes.entity_id                ) | list -%} {%-   endif -%} {% endfor %} {% set ns.entities = ns.entities | reject('in', browser_mod.split(',')) | list %} {# OUTPUT #} {{ ns.entities | join (',') }}'

2021-08-24 19:46:26 ERROR (MainThread) [homeassistant.components.script.group_set] Create groups with enhanced include/exclude statements: Error executing script. Error for call_service at pos 1: Error rendering data template: UndefinedError: 'browser_mod' is undefined


1-08-24 19:46:26 ERROR (MainThread) [homeassistant.components.automation.create_all_lights_group] Create All Lights Group: Error executing script. Error for call_service at pos 1: Error rendering data template: UndefinedError: 'browser_mod' is undefined

2021-08-24 19:46:26 ERROR (MainThread) [homeassistant.components.automation.create_all_lights_group] Error while executing automation automation.create_all_lights_group: Error rendering data template: UndefinedError: 'browser_mod' is undefined

Config

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


group excluded ->

deconz_groups:
  name: "deCONZ Groups"
  entities:
    - light.kok
    - light.kontor
    - light.fonster
    - light.matplats
    - light.lekplats
    - light.soffa
    - light.soffa_tak
    - light.sovrum
    - light.vardagsrum
    - light.vardagsrum_tak
    - light.ytterdorr
    - light.hall
    - light.garderob
    - light.adrians_rum
    - light.brezza
    - light.entre
    - light.rwl021_11