Light Group Cycler Blueprint

Here’s a simple light group cycling script that turns on/off each light group in the configured list.

Features:

  • Can target individual lights or light groups
  • Lights can be shared between groups without flickering
  • If the current target group is off it will be turned on, rather than cycling to the next group
  • Brightness is syncronized between groups during cycling (so brightness of lights shared between groups don’t get out of sync)

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

blueprint:
  name: Light Cycle
  description: >-
    Cycle through an ordered list of light targets (individual lights or
    light-group helpers). Each run turns the next target on and turns off
    only the current target's lights that are not shared with the next
    target, so shared lights never flicker. The active target is stored as
    the sole member of a dynamically managed group (group.<object_id>) so
    other automations can adjust it and this script can find its position on
    the next run. If the active target is off, running the script turns it
    back on instead of advancing. Wraps from last target to first. Note: the
    helper group does not survive a Home Assistant restart; the first run
    after a restart starts again at the first target.
  domain: script
  input:
    targets:
      name: Light Targets
      description: >-
        Ordered list of light entities or light-group helper entities to
        cycle through. The order selected here is the cycle order, wrapping
        from the last target back to the first.
      selector:
        entity:
          filter:
            - domain: light
          multiple: true
    helper_object_id:
      name: Helper Group Object ID
      description: >-
        Object ID of the dynamically managed group that tracks the active
        target, e.g. "office_light_cycle" creates group.office_light_cycle.
        Created automatically on first run — do not pre-create a helper.
        Must be unique for each script made from this blueprint.
      selector:
        text:

mode: queued
max: 10

sequence:
  # 1. Bind inputs to variables (!input cannot be used inside Jinja templates)
  - variables:
      targets: !input targets
      helper_object_id: !input helper_object_id
      helper_entity: "group.{{ helper_object_id }}"

  # 2. Nothing to do without targets
  - if:
      - condition: template
        value_template: "{{ targets | count == 0 }}"
    then:
      - stop: "No targets configured"

  # 3. Determine current position from the helper group and pick the next
  #    target. If the current target is off, re-select it (turn it back on
  #    instead of advancing). Missing helper (first run) or stale member
  #    (targets changed) both fall back to the first target.
  - variables:
      current_target: >-
        {{ (state_attr(helper_entity, 'entity_id') or [])
           | select('in', targets) | list | first | default('') }}
      next_target: >-
        {{ current_target
           if current_target and is_state(current_target, 'off')
           else (targets[(targets.index(current_target) + 1) % (targets | count)]
                 if current_target in targets else targets[0]) }}
      # Member lights of the current target that are not part of the next
      # target — only these are turned off, so shared lights never blink
      lights_to_turn_off: >-
        {{ expand([current_target] if current_target else [])
           | map(attribute='entity_id')
           | reject('in', expand(next_target) | map(attribute='entity_id') | list)
           | list }}
      # Carry the current target's brightness over when advancing so targets
      # stay in sync; none when turning the same target back on or when the
      # current target reports no brightness (off, non-dimmable, first run)
      sync_brightness: >-
        {{ state_attr(current_target, 'brightness')
           if current_target and next_target != current_target else none }}

  # 4. Switch targets: turn the next target on (at the previous target's
  #    brightness when advancing) and turn off the non-shared lights of the
  #    previous target, in parallel. Shared lights are in neither the off
  #    list nor at a different brightness, so they never blink.
  - parallel:
      - if:
          - condition: template
            value_template: "{{ sync_brightness is not none }}"
        then:
          - action: light.turn_on
            target:
              entity_id: "{{ next_target }}"
            data:
              transition: 0.3
              brightness: "{{ sync_brightness }}"
        else:
          - action: light.turn_on
            target:
              entity_id: "{{ next_target }}"
            data:
              transition: 0.3
      - if:
          - condition: template
            value_template: "{{ lights_to_turn_off | count > 0 }}"
        then:
          - action: light.turn_off
            target:
              entity_id: "{{ lights_to_turn_off }}"
            data:
              transition: 0.3

  # 5. Record the new active target so other automations (rotary knob
  #    brightness, etc.) and the next run of this script can find it
  - action: group.set
    data:
      object_id: "{{ helper_object_id }}"
      entities:
        - "{{ next_target }}"
1 Like