Dynamic, Hue-like Color Scenes with Randomly Distributed Colors

This blueprint lets you apply a (optionally dynamic) light scene to a selection of lights. It’s the ‘blueprintification’ of this script. Next to the dynamic scenes, it was updated to allow light selection by label.

Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.

The full blueprint:

blueprint:
  name: Hue-like Scenes
  domain: script
  description: >-
    This script replicates Hue scenes. Each scene has five different 
    colors
    represented by XY values. These colors are distributed randomly
    on the participating lights (must support XY or RGB color modes) -- each 
    light gets a different color.

    The script can run in two modes, selectable *at runtime*. In mode *once* 
    the script sets the colors once and then stops. In mode *loop*, the script
    repeats continuously, with a delay between iterations. If the script is run
    in loop mode, it can either be terminated by called `script.turn_off` 
    service or by turning off one of the participating lights. 

    The script has various configuration options, described below. The intended 
    setup is to instantiate the blueprint for each area in which it will be run.

    ## Changelog 

    ### 2.0

    - Made into a blueprint. Intended use case: Instantiate blueprint for each area (then they can run independently in parallel).

    - Supports loop mode, a.k.a. dynamic scenes

    - Can exclude lights by manufacturer

    - Select lights by labels

    ## Scene images

    The images used in the Hue app to represent a scene use creative commons 
    licenses. The following is a list of (some) scenes and image sources:

    - Savanna Sunset: https://www.flickr.com/photos/ladydragonflyherworld/6293722846

    - Majestic Morning: https://unsplash.com/photos/aerial-photo-of-foggy-trees-and-mountains-dbu1zrkZZuo

    - Horizon: https://www.pexels.com/photo/rocks-on-water-2261500/

    - Tropical Twilight: https://www.flickr.com/photos/130079987@N02/16342014082

    - Crocus: https://unsplash.com/photos/close-up-photo-of-purple-petaled-flower-hB_xgEXucQs
  author: Nils Reiter
  homeassistant:
    min_version: "2024.04.00"
  input:
    target:
      name: Target
      description: >-
        Select one or more areas, labels or light entities. The 
        script picks compatible lights on its own.
      selector:
        target:
          entity:
            domain: light
    skipgroups:
      name: Skip groups
      description: >-
        If enabled, group light entities will be skipped. Note that 
        this only applies to the groups defined in Home Assistant. Groups of 
        Zigbee lights defined in the settings of the Zigbee device are not 
        recognized as group. See option `filter_manufacturer"` below.
      default: true
      selector:
        boolean: null
    hue_min:
      name: Hue minimum value
      description: >-
        Only relevant when scene is "Random". Specifies the minimum hue value to
        draw from.
      selector:
        number:
          min: 0
          max: 359
      default: 0
    hue_max:
      name: Hue maximum value
      description: >-
        Only relevant when scene is "Random". Specifies the maximum hue value to
        draw from.
      selector:
        number:
          min: 1
          max: 360
      default: 360
    sat_min:
      name: Saturation minimum value
      description: >-
        Only relevant when scene is "Random". Specifies the minimum saturation
        value to draw from.
      selector:
        number:
          min: 0
          max: 100
      default: 99
    sat_max:
      name: Saturation maximum value
      description: >-
        Only relevant when scene is "Random". Specifies the maximum saturation
        value to draw from.
      selector:
        number:
          min: 1
          max: 101
      default: 101
    filter_manufacturer:
      name: Filter lighty by manufacturer
      description: >-
        If set, lights by that manufacturer will be skipped. This can be used to
        skip groups defined in the Zigbee controller. E.g., if set to "Silicon 
        Labs" groups created on SkyConnect will be removed.
      selector:
        text:
      default: ""
    inner_delay:
      name: Inner delay
      description: >-
        Adds a very short delay between sending commands to lights 
        to prevent flooding the network.
      selector:
        number:
          min: 0
          max: 2
          step: 0.1
          unit_of_measurement: "s"
      default: 0.5
mode: restart
fields:
  scene:
    name: Scene
    description: The scene to apply.
    required: true
    default: Savanna Sunset
    selector:
      select:
        options:
          - Savanna Sunset
          - Golden Pond
          - Horizon
          - Frosty Dawn
          - Crocus
          - Tropical Twilight
          - Majestic Morning
          - Random
  repeat_delay:
    selector:
      duration: {}
    name: Repeat delay
    default: "00:00:00"
    description: >-
      If set to a time > 0, the script runs in loop mode. 
      After the delay, the script repeats and assigns the
      colors anew. Please note that the script stops when one of the lights
      is turned off or it is termined with a service call.
    required: true
  onlyonlights:
    name: Only lights currently on?
    description: >-
      If enabled, the scene is only applied to the lights currently on. This is
      useful if you want to have on-the-fly control over the participating 
      lights.
    required: true
    default: false
    selector:
      boolean: null
  brightness:
    name: Brightness
    description: >-
      If set, all lights will be set to the given brightness percentage. In
      loop mode, brightness is only applied on the first iteration, allowing
      brightness changes afterwards.
    example: 50
    required: false
    default: 100
    selector:
      number:
        min: 1
        max: 100
        unit_of_measurement: "%"
  transition:
    name: Transition time
    description: How long to move from one color to the next
    required: false
    example: 2
    default: 5
    selector:
      number:
        min: 0
        max: 30
        unit_of_measurement: s
variables:
  target: !input target
  sat_min: !input sat_min
  sat_max: !input sat_max
  hue_min: !input hue_min
  hue_max: !input hue_max
  filter_manufacturer: !input filter_manufacturer
  skipgroups: !input skipgroups
  scenes: |-
    {% set scenes = {
      "Savanna Sunset": {
        "colors": [[0.644, 0.3348],[0.5246,
          0.3864],[0.4801, 0.4309],[0.5862, 0.3575],[0.4162, 0.4341]]
      },
      "Golden Pond": {
        "colors": [[0.5695, 0.3999],[0.482,
          0.4489],[0.496, 0.4424],[0.5584, 0.4083],[0.5063, 0.4474]]
      },
      "Horizon": {
        "colors": [[0.2779, 0.2188],[0.1811,
          0.1979],[0.5247, 0.3877],[0.592, 0.385],[0.1731, 0.1978]]
      },
      "Frosty Dawn": {
        "colors": [[0.4221, 0.386],[0.387,
          0.4328],[0.4013, 0.4172],[0.439, 0.3782],[0.4675, 0.3769]]
      },
      "Crocus": {
        "colors": [[0.2877, 0.2519],[0.2194, 0.1332],[0.4212, 0.38],[0.3818, 0.485],[0.4195, 0.4216]]
      },
      "Tropical Twilight": {
        "colors": [[0.5813, 0.3636],[0.2412, 0.1171],[0.3044, 0.1803],[0.4731, 0.3723],[0.363, 0.2716]]
      },
      "Color Palette Nr. 4557": {
        "colors": [[0.0264,0.0368],[0.1369,0.2082],[0.6616,0.8396],[0.4481,0.2822],[0.1447,0.0799]]
      },
      "Majestic Morning": {
        "colors": [[0.3979, 0.413],[0.4741, 0.43],[0.4659, 0.3561],[0.1879, 0.0644],[0.5471, 0.3588]]
      },
      "Random": {
        "colors": []
      }
    }
    %}
    {{ scenes }}
  lights: |-
    {% set ns = namespace(areas=[], l=[]) %}

    ## collect areas
    {% if target.area_id is defined %}
      {% if target.area_id is iterable and not target.area_id is string %}
        {% set ns.areas = ns.areas + target.area_id %}
      {% else %}
        {% set ns.areas = ns.areas + [target.area_id] %}
      {% endif %}
    {% endif %}

    {% if target.floor_id is defined %}
      {% if target.floor_id is iterable and not target.floor_id is string %}
        {% for f in target.floor_id %}
          {% set ns.areas = ns.areas + floor_areas(f) %}
        {% endfor %}        
      {% else %}
        {% set ns.areas = ns.areas + floor_areas(target.floor_id) %}
      {% endif %}
    {% endif %}

    ## process areas
    {% for a in ns.areas %}
      {% set ns.l = ns.l + area_entities(a)|select('match', 'light.')|list %}
    {% endfor %}

    {% if target.entity_id is defined %}
      {% if target.entity_id is iterable and not target.entity_id is string %}
        {% set ns.l = ns.l + (target.entity_id|list) %}
      {% else %}
        {% set ns.l = ns.l + [target.entity_id] %}
      {% endif %}
    {% endif %}

    ## process labels
    {% if target.label_id is defined %}
      {% if target.label_id is iterable and not target.label_id is string %}
        {% for a in target.label_id %}
          {% set ns.l = ns.l + label_entities(a)|select('match', 'light.')|list %}              
        {% endfor %}
      {% else %}
        {% set ns.l = ns.l + label_entities(target.label_id)|select('match', 'light.')|list %}
      {% endif %}
    {% endif %}

    {% if onlyonlights|default(true) %}
    {% set ns.l = ns.l| select('is_state', 'on')%}
    {% endif %}

    {% if skipgroups|default(true) %}
    {% set ns.l|from_json %}
    [{%- for ll in ns.l -%}
      {%- if not state_attr(ll, "entity_id")-%}
          "{{ ll }}"
          {%- if not loop.last-%},{%-endif-%}
      {%-endif-%}
    {%- endfor -%}]
    {% endset %}
    {% endif %}

    {% if filter_manufacturer is defined %}
    {% set ns.l|from_json %}
    [{%- for ll in ns.l -%}
        {%- if device_attr(device_id(ll), "manufacturer") != filter_manufacturer -%}
          "{{ ll }}"
          {%- if not loop.last-%},{%-endif-%}
        {% endif %}
    {%- endfor -%}]
    {% endset %}
    {% endif %}

    [{%- for ll in ns.l %}
      {%- set colormodes = state_attr(ll, "supported_color_modes") -%}
      {%- if "xy" in colormodes or "rgb" in colormodes -%}
      "{{ ll }}"
      {%- if not loop.last-%},{%-endif-%}
      {%- endif -%}
    {%- endfor -%}]
sequence:
  - repeat:
      while:
        - condition: template
          value_template: >-
            {{ repeat.index == 1 or (
              (repeat_delay.hours > 0 or repeat_delay.minutes > 0 or repeat_delay.seconds > 0) and 
              (( lights | list | count ) == (lights | select('is_state', 'on') | list | count))
            ) }}
      sequence:
        - variables:
            colors: |-
              # shuffle color order
              {% set ns = namespace(x = scenes[scene].colors) %}
              {% for i in range(ns.x | length - 1, 0, -1) %}
                {% set j = range(0, i + 1) | random %}
                {% if j != i %}
                  {% set ns.x = ns.x[:j]+[ns.x[i]]+ns.x[j+1:i]+[ns.x[j]]+ns.x[i+1:] %}
                {% endif %}
              {% endfor %}
              {{ ns.x }}
            first: "{{ repeat.index == 1}}"
        - repeat:
            for_each: "{{ lights }}"
            sequence:
              - variables:
                  datadict: |-
                    {% if scene == "Random" %}
                      {% set dd = { "hs_color": [
                        range(hue_min|default(0),hue_max|default(360))|random, 
                        range(sat_min|default(99),sat_max|default(101))|random ] } %}
                    {% else %}
                      {% set xy = colors[repeat.index % colors|length] %}
                      {% set dd = {"xy_color": [xy|first, xy|last] } %}
                    {% endif %}
                    {% if brightness is defined and first %}
                      {% set bd = {"brightness_pct": brightness } %}
                      {% set dd = dict(dd, **bd) %}
                    {% endif %}
                    {% if transition %}
                      {% set bd = {"transition": transition } %}
                      {% set dd = dict(dd, **bd) %}
                    {% endif %}
                    {{ dd }}
              - service: light.turn_on
                data: "{{ datadict }}"
                target:
                  entity_id: "{{ repeat.item }}"
              - delay: !input inner_delay
        - wait_for_trigger:
            - platform: template
              value_template: "{{ (( lights | list | count ) != (lights | select('is_state', 'on') | list | count)) }}"
          timeout: "{{ repeat_delay }}"

There is currently no easy way to add scenes, I’m thinking about that.

The GitHub repository has a bit more on how this is integrated in my setup.