Breaking down light groups

In a number of scripts I need to pass variables into them that represent groups of lights in different ways. So sometimes it might be a single string that is the name of a group (like light.room_name) and other times it may be a list of groups or individual lights (like [light.room1, light.room2, light.single_light]).

I’ve come up with this bunch of generic macros that seem to do that trick - in that they cope with being thrown either a single string that represents either a single bulb or a single group, or a list of strings that represent either groups or single bulbs.

You can throw in an ‘include’ set and an ‘exclude’ set and what you get back is an array of individual lights (well actually a comma separated string of individual bulbs given that everything is a string - but its trivial to split it out).

Posting for two reasons. 1) It may be just generically useful 2) It quite possibly can be optimised!
You should be able to copy the below into the developers template space and just change the light names in the last few lines to test in your setup. I have hue lights, tp link lights - and groups defined in the hue app and in HA… so the attribute checks cope with identifying the different types of groups and how to break them down.

{%- macro breakdown_group(group) -%}
  {%- if state_attr(group, 'is_hue_group') -%}
    {%- set entities = state_attr(group, 'lights') | map('lower') | map('regex_replace', '^(.*)', 'light.\\1') -%}
  {%- elif state_attr(group, 'entity_id') is not none -%}
    {%- set entities = state_attr(group, 'entity_id') | map('lower') | list -%}
  {%- else -%}
    {%- set entities = [group] | map('lower') -%}
  {%- endif -%}
  {{- entities | unique | join(',') -}}
{%- endmacro -%}

{%- macro lights(list) -%}
  {%- set t = namespace(lights = []) -%}
  {%- if list is string -%}
    {%- set t.lights = breakdown_group(list).split(',') -%}
  {%- else -%}
    {%- for l in list -%}
      {%- set t.lights = t.lights + breakdown_group(l).split(',') -%}
    {%- endfor -%}
  {%- endif -%}
  {{- t.lights | unique | join(',') -}}
{%- endmacro -%}

{%- macro get_lights(include, exclude) -%}
  {{- lights(include).split(',') | reject('in', lights(exclude).split(',')) | unique | join(',') -}}
{%- endmacro -%}

{# test with two groups included, one of them excluded and a specific light excluded as well #}
{% set exclude = ['light.garden_room','light.drbl'] %}
{% set include = ['light.garden_room', 'light.sunset_lights'] %}
{{ get_lights(include, exclude).split(',') }}
{# test to just get a group expanded without any excluded, should match previous without one light #}
{{ get_lights('light.sunset_lights', []).split(',') }}

The results of the above run should be two lists the same minus one specific light:

['light.mkp1', 'light.fhpr', 'light.fhpl', 'light.gpw1', 'light.gacw', 'light.garw', 'light.galw']

['light.mkp1', 'light.drbl', 'light.fhpr', 'light.fhpl', 'light.gpw1', 'light.gacw', 'light.garw', 'light.galw']

And an example use in a real script, this one happens to be a generic script that you set the variable ‘room’ to be the set of lights you want to randomise, and optionally the ‘colour_bias’ will bias the colours picked to a particular colour:

alias: Random Room Lights
sequence:
  - variables:
      settings: |-
        {%- macro pick_hue() -%}
          {%- if colour_bias is defined and colour_bias|float(-1) >= 0 -%}
            {%- set h = ((range(((colour_bias|float)*100)|int, (((colour_bias|float)+40)*100)|int) | random)/100) - 20 -%}
            {%- if h <= 0 -%}
              {%- set h = 360 + h -%}
            {%- elif h > 360 -%}
              {%- set h = h - 360 -%}
            {%- endif -%}
            {{ h }}
          {%- else -%}
            {{- (range(0,36000) | random) / 100 -}}
          {%- endif -%}
        {%- endmacro -%}

        {%- macro pick_saturation() -%}
          {%- if saturation is defined -%}
            {{- saturation -}}
          {%- else -%}
            {{- (range(3000,10000) | random) / 100 -}}
          {%- endif -%}
        {%- endmacro -%}

        {%- macro pick_brightness() -%}
          {%- if brightness is defined -%}
            {{- brightness -}}
          {%- else -%}
            {{- (range(20000,25500) | random) / 100 -}}
          {%- endif -%}
        {%- endmacro -%}

        {%- macro pick_hs(lights) -%}
        {%- for light in lights -%}
            "{{- light -}}": {"state":"on","hs_color": [ {{- pick_hue() -}}, {{- pick_saturation() -}} ],"brightness": {{- pick_brightness() -}}},
        {%- endfor -%}
        {%- endmacro -%}

        {%- macro breakdown_group(group) -%}
          {%- if state_attr(group, 'is_hue_group') -%}
            {%- set entities = state_attr(group, 'lights') | map('lower') | map('regex_replace', '^(.*)', 'light.\\1') -%}
          {%- elif state_attr(group, 'entity_id') is not none -%}
            {%- set entities = state_attr(group, 'entity_id') | map('lower') | list -%}
          {%- else -%}
            {%- set entities = [group] | map('lower') -%}
          {%- endif -%}
          {{- entities | unique | join(',') -}}
        {%- endmacro -%}

        {%- macro lights(list) -%}
          {%- set t = namespace(lights = []) -%}
          {%- if list is string -%}
            {%- set t.lights = breakdown_group(list).split(',') -%}
          {%- else -%}
            {%- for l in list -%}
              {%- set t.lights = t.lights + breakdown_group(l).split(',') -%}
            {%- endfor -%}
          {%- endif -%}
          {{- t.lights | unique | join(',') -}}
        {%- endmacro -%}

        {%- macro get_lights(include, exclude) -%}
          {{- lights(include).split(',') | reject('in', lights(exclude).split(',')) | unique | join(',') -}}
        {%- endmacro -%}

        {%- set lights = (get_lights(room,exclude)).split(',') -%}
        {{- ("{" + pick_hs(lights)[:-1] + "}") | from_json -}}
  - service: scene.apply
    data: >-
      { {% if transition is defined %}"transition": {{ transition }},{% endif
      %}"entities": {{ settings }} }
mode: parallel
icon: mdi:palette-outline
max: 10

Call the script like this (example excluding a specific bulb light.grcf1 in the room):

service: script.random_room_lights
data: 
  room: light.garden_room
  exclude: light.grcf1

or… to exclude a sub-group of specific lights in the room:

service: script.random_room_lights
data: 
  room: light.garden_room
  exclude:  light.garden_room_back

or… to exclude both a specific bulb and a sub-group:

service: script.random_room_lights
data: 
  room: light.garden_room
  exclude: 
    - light.garden_room_back
    - light.grcf1

or… going wild… including a whole bunch of rooms, and a couple of sub-groups:

service: script.random_room_lights
data: 
  room:
    - light.garden_room
    - light.lounge
    - light.kitchen
  exclude: 
    - light.garden_room_back
    - light.kitchen_left

And, just because I can, if you list out the individual lights, the scene is painted in the order you list the lights (as the order is preserved in the macros and by home assistant when it applies a scene), which makes for a rather pleasing light ‘wave’ across the room if you put the bulbs in the right order. This one deploys the scene in a spiral pattern (as it happens) across the ceiling lights in my lounge.

service: script.turn_on
data:
  variables:
    room:
      - light.mlc1
      - light.mlc3
      - light.mlr1
      - light.mlr2
      - light.mlc4
      - light.mlc2
      - light.mll2
      - light.mll1
    colour_bias: >-
      {% if colour_bias is defined %}{{ colour_bias }}{% endif %}
target:
  entity_id: script.random_room_lights