Bewegungsgesteuertes Licht mit Home Assistant und Tradfri (Zigbee Tunable White)

Bewegungsgesteuertes Licht ist wahrscheinlich eine der ältesten „Smart Home“ Lösungen, die es gibt. Mit modernen smarten Leuchtmitteln und Steuerzentralen lässt sich allerdings noch viel mehr umsetzen, als nur an oder aus. Eine populäre Lösung ist hier Philips Hue, das allerdings (auch nach der Einführung der Hue Essentials) nach wie vor eher teuer ist – aber dankbarerweise kann man mit universellen Steuerzentralen wie Home Assistant Leuchtmittel verschiedenster Hersteller verwenden.

In meinem Fall habe ich bei Ikea während des Abverkaufs der „alten“ Zigbee-Produkte einige „Tradfri“ Tunable White Leuchtmittel erworben, die zusammen mit einem Zigbee-Bewegungsmelder als automatische Badezimmerbeleuchtung dienen sollte. Allerdings (schließlich soll das Licht ja so smart wie möglich sein) sollte die Automation mehr können als nur an oder aus:

  • Weiches Dimmen
  • Unterschiedliche Helligkeits- und Farbwerte basierend auf der Uhrzeit (Tag/Nachtmodus)
  • „Warndimmen“, sodass man nicht plötzlich auf der Toilette im Dunkeln sitzt

Tatsächlich bin ich selbstverständlich nicht der Erste mit dieser Idee, weshalb es bereits einige Automations-Blueprints mit diesem Funktionsumfang (und mehr!) gibt, z.B. dieses hier vom HA-Forum Nutzer „Blacky“. Theoretisch sollte damit also alles getan sein: Blueprint in HA importieren, Einstellungen anpassen und fertig? Leider nein, denn wie ich auch schon hier beschrieben habe, unterstützen die Tradfri Leuchtmittel leider keine gleichzeitige Anpassung der Helligkeit und Farbtemperatur, sodass solche Blueprints bzw. Automationen leider nicht wie gewünscht funktionieren. Des Weiteren hatte ich gelegentlich das Problem, dass eine der Leuchten einen der Befehle nicht empfangen oder erfolgreich umgesetzt hatte, was nicht schön aussieht und auch nicht förderlich für das Vertrauen der anderen Hausbewohner in die Automation ist. Also habe ich einen Automations-Blueprint erstellt, der diese Probleme löst – selbstverständlich funktioniert der auch nicht ausschließelich für Ikea Tradfri Leuchtmittel, sondern müsste für alle Tunable White Leuchten in Home Assistant funktionieren.

Der Blueprint setzt das wie folgt um:

  1. Senden des Einschaltbefehls mit der gewünschten Helligkeit und Dimm-Dauer
  2. Senden der gewünschten Farbtemperatur nach einstellbarer Verzögerung
  3. Prüfen, ob alle Leuchten den gewünschten Status haben
    • Falls nein: Befehle erneut an dieses Leuchtmittel senden
  4. Nach einstellbarer Dauer: Warndimmen
  5. Nach einstellbarer Dauer: Lichter ausschalten
  6. Prüfen, ob Lampen ausgeschaltet, falls nicht, erneut Ausschaltbefehl senden

Darüber hinaus bleibt der Status des Lichts konstant, solange es eingeschaltet ist, das heißt beim Erreichen der Umschaltzeit zwischen Tag oder Nacht passiert nichts, solange das Licht bereits eingeschaltet ist (damit man nicht plötzlich im Dunkeln steht).

Bei der Handhabung des Blueprints habe ich mich an dem oben erwähnten Skript von Blacky orientiert und ein ähnliches Einstellungsmenü eingebaut, welches alle für diesen Zweck relevanten Einstellungen enthält. Dabei habe ich mir von Codex helfen lassen, Teile des Skripts geschrieben hat. Das erschien mir deutlich zielführender, als eine Modifikation des bestehenden Skripts aus dem Forum, welches mehr als 12.000 Zeilen lang ist – meins kommt hingegen nur auf 824 Zeilen. Natürlich bietet es auch einen kleineren Funktionsumfang, aber es deckt alles ab was ich haben wollte und was nicht da ist, kann auch nicht zu unvorhergesehenen Edge-Cases und Problemen führen.

Um den Blueprint in Home Assistant zu importieren, müsst ihr unter Einstellungen -> Automationen & Szenen > Blueprints den Button „Blueprint importieren“ wählen. Leider könnt ihr hier nicht einfach Copy/Pasten, sondern müsst zwangsläufig eine URL angeben, unter welcher der Blueprint liegt. Hierzu habe ich den Blueprint auf meinen Server geladen, ihr könnt den Blueprint daher unter „http://silentnet.de/download/motion-light_tradfri_new.yaml“ abrufen. Alternativ findet ihr den Code auch hier drunter im Spoiler, sodass ihr ihn kopieren und anderswo hochladen könnt.

Blueprint Code
blueprint:
  name: Motion Light – Tradfri Tunable White Kompatibel
  description: >
    Bewegungslicht-Blueprint für Tunable-White-Leuchten.
    Helligkeit und Kelvin werden für IKEA Tradfri getrennt gesendet.
    Ein Smart-Reassert korrigiert anschließend nur tatsächlich abweichende
    Lampen. Der optionale Nacht-Helper hat Vorrang vor der Uhrzeit. Der beim
    Einschalten gewählte Tag-/Nachtmodus bleibt bis zum finalen Ausschalten
    gespeichert.
  domain: automation
  homeassistant:
    min_version: 2024.10.0
  input:
    entities:
      name: Geräte und Entitäten
      icon: mdi:motion-sensor
      collapsed: false
      input:
        motion_sensors:
          name: Bewegungs- oder Präsenzsensoren
          description: >
            Ein oder mehrere binary_sensor-Entities; aktiv ist der Zustand
            "on". Erst wenn alle Sensoren exakt "off" sind, wird ausgeschaltet.
          selector:
            entity:
              filter:
                - domain: binary_sensor
              multiple: true
        light_entities:
          name: Lampen
          description: >
            Eine oder mehrere light-Entities; einzelne Lampen und
            Home-Assistant-Light-Groups sind zulässig.
          selector:
            entity:
              filter:
                - domain: light
              multiple: true
        enable_helper:
          name: Enable-/Disable-Helper (optional)
          description: >
            Wenn ausgewählt, arbeitet die Automation nur bei "on".
            Leer bedeutet ausdrücklich: kein Helper.
          default: ""
          selector:
            entity:
              filter:
                - domain: input_boolean

    night_mode:
      name: Tag- und Nachtmodus
      icon: mdi:weather-night
      collapsed: false
      input:
        night_mode_enabled:
          name: Zeitgesteuerten Nachtmodus aktivieren
          description: >
            Gilt ohne Nacht-Helper. Ein ausgewählter Nacht-Helper hat Vorrang.
          default: true
          selector:
            boolean:
        night_start:
          name: Nachtmodus beginnt
          description: >
            24:00 beziehungsweise 24:00:00 wird intern als 00:00:00 behandelt.
          default: "22:00:00"
          selector:
            time:
        night_end:
          name: Nachtmodus endet
          description: >
            24:00 beziehungsweise 24:00:00 wird intern als 00:00:00 behandelt.
          default: "06:00:00"
          selector:
            time:
        night_mode_helper:
          name: Nachtmodus-Helper (optional)
          description: >
            Wenn ausgewählt, bedeutet "on" Nacht und "off" Tag; die Uhrzeit
            wird dann nicht ausgewertet.
          default: ""
          selector:
            entity:
              filter:
                - domain: input_boolean

    normal_light:
      name: Normales Licht
      icon: mdi:lightbulb-on
      collapsed: true
      input:
        normal_brightness_pct:
          name: Helligkeit
          default: 80
          selector:
            number:
              min: 1
              max: 100
              step: 1
              mode: slider
              unit_of_measurement: "%"
        normal_color_temp_kelvin:
          name: Farbtemperatur
          default: 3500
          selector:
            color_temp:
              unit: kelvin
              min: 2000
              max: 6500
        normal_transition:
          name: Einschalt-Transition
          default: 1
          selector:
            number:
              min: 0
              max: 10
              step: 0.1
              mode: box
              unit_of_measurement: s

    night_light:
      name: Nachtlicht
      icon: mdi:lightbulb-night
      collapsed: true
      input:
        night_brightness_pct:
          name: Helligkeit
          default: 15
          selector:
            number:
              min: 1
              max: 100
              step: 1
              mode: slider
              unit_of_measurement: "%"
        night_color_temp_kelvin:
          name: Farbtemperatur
          default: 2200
          selector:
            color_temp:
              unit: kelvin
              min: 2000
              max: 6500
        night_transition:
          name: Einschalt-Transition
          default: 1
          selector:
            number:
              min: 0
              max: 10
              step: 0.1
              mode: box
              unit_of_measurement: s

    turn_off:
      name: Ausschaltverhalten
      icon: mdi:lightbulb-off
      collapsed: false
      input:
        no_motion_delay:
          name: Zeit ohne Bewegung
          default:
            minutes: 5
          selector:
            duration:
        warning_enabled:
          name: Warn-Dimmen aktivieren
          default: true
          selector:
            boolean:
        warning_duration:
          name: Dauer des Warn-Dimmens
          default:
            seconds: 10
          selector:
            duration:
        warning_transition:
          name: Transition beim Warn-Dimmen
          default: 1
          selector:
            number:
              min: 0
              max: 10
              step: 0.1
              mode: box
              unit_of_measurement: s
        warning_level_pct:
          name: Warn-Helligkeit
          description: >
            Prozent der Zielhelligkeit des gespeicherten Tag-/Nachtmodus.
          default: 25
          selector:
            number:
              min: 1
              max: 100
              step: 1
              mode: slider
              unit_of_measurement: "% des Zielwerts"
        off_transition:
          name: Ausschalt-Transition
          default: 2
          selector:
            number:
              min: 0
              max: 30
              step: 0.1
              mode: box
              unit_of_measurement: s
        off_retry_delay:
          name: Verzögerung bis zur Ausschaltkontrolle
          description: >
            Danach werden standardmäßig nur Lampen erneut ausgeschaltet, die
            Home Assistant noch als eingeschaltet meldet.
          default:
            seconds: 3
          selector:
            duration:
        force_off_retry:
          name: Zweiten Ausschaltbefehl immer senden
          description: >
            Sendet den zweiten Off-Befehl auch an bereits als "off" gemeldete
            Lampen. Das kann Restglimmen beheben, bei manchen Tradfri-Lampen
            aber ein kurzes Aufblitzen verursachen.
          default: false
          selector:
            boolean:

    tradfri:
      name: Tradfri-Kompatibilität
      icon: mdi:zigbee
      collapsed: false
      input:
        color_command_delay:
          name: Verzögerung Helligkeit → Kelvin
          description: >
            Der Kelvin-Befehl enthält keine erneute Helligkeitsangabe.
          default:
            seconds: 1
          selector:
            duration:
        brightness_check_delay:
          name: Verzögerung bis zur Helligkeitskontrolle
          description: >
            Ab dem ersten Helligkeitsbefehl gemessen. Mindestens so lang wie
            Kelvin-Verzögerung und gewünschte Einschalt-Transition wählen.
          default:
            seconds: 3
          selector:
            duration:
        brightness_tolerance:
          name: Helligkeitstoleranz
          description: Home-Assistant-Skala von 0 bis 255.
          default: 10
          selector:
            number:
              min: 0
              max: 50
              step: 1
              mode: slider

trigger_variables:
  motion_trigger_entities: !input motion_sensors

# Ein Aggregat-Trigger feuert nur beim Übergang
# "kein Sensor on" -> "mindestens ein Sensor on".
triggers:
  - trigger: template
    value_template: >-
      {% set ns = namespace(any_on=false) %}
      {% for entity_id in motion_trigger_entities %}
        {% if is_state(entity_id, 'on') %}
          {% set ns.any_on = true %}
        {% endif %}
      {% endfor %}
      {{ ns.any_on }}

variables:
  motion_entities: !input motion_sensors
  lights: !input light_entities
  enable_helper_entity: !input enable_helper
  night_helper_entity: !input night_mode_helper
  use_timed_night_mode: !input night_mode_enabled
  night_start_time: !input night_start
  night_end_time: !input night_end
  normal_brightness: !input normal_brightness_pct
  normal_kelvin: !input normal_color_temp_kelvin
  normal_on_transition: !input normal_transition
  night_brightness: !input night_brightness_pct
  night_kelvin: !input night_color_temp_kelvin
  night_on_transition: !input night_transition
  no_motion_duration: !input no_motion_delay
  use_warning: !input warning_enabled
  warning_transition_seconds: !input warning_transition
  warning_factor: !input warning_level_pct
  brightness_tolerance_value: !input brightness_tolerance
  off_transition_seconds: !input off_transition
  force_off_retry_enabled: !input force_off_retry

conditions: []

# Ein einzelner Lauf hält den gewählten Modus bis zum vollständigen Ausschalten.
# Bewegung während Warte-/Warnphasen wird intern verarbeitet.
mode: single
max_exceeded: silent

actions:
  # Leere Helper-Auswahl ist kein versehentlich erfüllter State-Test.
  - condition: template
    value_template: >-
      {% set selected = enable_helper_entity is string
        and enable_helper_entity | length > 0 %}
      {{ not selected or is_state(enable_helper_entity, 'on') }}

  # Pflichtauswahlen zusätzlich absichern: niemals mit leeren Targets laufen.
  - condition: template
    value_template: >-
      {{ motion_entities | count > 0 and lights | count > 0 }}

  # Modus-Snapshot: Helper vor Uhrzeit; start=end bedeutet kein Zeitfenster.
  - variables:
      night_mode_active: >-
        {% set selected = night_helper_entity is string
          and night_helper_entity | length > 0 %}
        {% if selected %}
          {{ is_state(night_helper_entity, 'on') }}
        {% elif not (use_timed_night_mode | bool) %}
          {{ false }}
        {% else %}
          {# today_at() akzeptiert 24:00 nicht; Mitternacht normalisieren. #}
          {% set normalized_start = '00:00:00'
            if night_start_time in ['24:00', '24:00:00']
            else night_start_time %}
          {% set normalized_end = '00:00:00'
            if night_end_time in ['24:00', '24:00:00']
            else night_end_time %}
          {% set start = today_at(normalized_start) %}
          {% set end = today_at(normalized_end) %}
          {% set current = now() %}
          {% if start < end %}
            {{ start <= current < end }}
          {% elif start > end %}
            {{ current >= start or current < end }}
          {% else %}
            {{ false }}
          {% endif %}
        {% endif %}
      resolved_lights: >-
        {% set ns = namespace(ids=[]) %}
        {% for entity_id in lights %}
          {% set members = state_attr(entity_id, 'entity_id') %}
          {% if members is iterable and members is not string
                and members | count > 0 %}
            {% set ns.ids = ns.ids + (members | list) %}
          {% else %}
            {% set ns.ids = ns.ids + [entity_id] %}
          {% endif %}
        {% endfor %}
        {{ expand(ns.ids)
          | selectattr('domain', 'eq', 'light')
          | map(attribute='entity_id')
          | unique
          | list }}

  - variables:
      target_brightness_pct: >-
        {{ (night_brightness if night_mode_active | bool
          else normal_brightness) | int }}
      target_color_temp_kelvin: >-
        {{ (night_kelvin if night_mode_active | bool
          else normal_kelvin) | int }}
      target_on_transition: >-
        {{ (night_on_transition if night_mode_active | bool
          else normal_on_transition) | float }}
      warning_brightness_pct: >-
        {% set value =
          (target_brightness_pct | float * warning_factor | float / 100)
          | round(0) | int %}
        {{ [1, value] | max }}

  - variables:
      initial_brightness_targets: >-
        {% set target = target_brightness_pct | float * 255 / 100 %}
        {% set tolerance = brightness_tolerance_value | float %}
        {% set ns = namespace(ids=[]) %}
        {% for entity_id in resolved_lights %}
          {% set reported = state_attr(entity_id, 'brightness') %}
          {% if states(entity_id) != 'on'
                or reported is none
                or not is_number(reported)
                or (((reported | float) - target) | abs) > tolerance %}
            {% set ns.ids = ns.ids + [entity_id] %}
          {% endif %}
        {% endfor %}
        {{ ns.ids }}

  # Sofort: nur Helligkeit und Transition, niemals Kelvin.
  - if:
      - condition: template
        value_template: "{{ initial_brightness_targets | count > 0 }}"
    then:
      - action: light.turn_on
        target:
          entity_id: "{{ initial_brightness_targets }}"
        data:
          brightness_pct: "{{ target_brightness_pct }}"
          transition: "{{ target_on_transition }}"

  # Beide Delays beginnen am selben Punkt: Kelvin bei t=1 s, Check bei t=3 s.
  - parallel:
      - sequence:
          - delay: !input color_command_delay
          - condition: template
            value_template: >-
              {% set selected = enable_helper_entity is string
                and enable_helper_entity | length > 0 %}
              {{ (not selected or is_state(enable_helper_entity, 'on'))
                and resolved_lights | count > 0 }}
          # Tradfri: Kelvin und Transition, aber weiterhin keine Helligkeit.
          - action: light.turn_on
            target:
              entity_id: "{{ resolved_lights }}"
            data:
              color_temp_kelvin: "{{ target_color_temp_kelvin }}"
              transition: "{{ target_on_transition }}"

      - sequence:
          - delay: !input brightness_check_delay
          - condition: template
            value_template: >-
              {% set selected = enable_helper_entity is string
                and enable_helper_entity | length > 0 %}
              {{ not selected or is_state(enable_helper_entity, 'on') }}
          - variables:
              reassert_targets: >-
                {% set target = target_brightness_pct | float * 255 / 100 %}
                {% set tolerance = brightness_tolerance_value | float %}
                {% set ns = namespace(ids=[]) %}
                {% for entity_id in resolved_lights %}
                  {% set reported = state_attr(entity_id, 'brightness') %}
                  {% if states(entity_id) != 'on'
                        or reported is none
                        or not is_number(reported)
                        or (((reported | float) - target) | abs) > tolerance %}
                    {% set ns.ids = ns.ids + [entity_id] %}
                  {% endif %}
                {% endfor %}
                {{ ns.ids }}
          # Nur abweichende Lampen: wie beim ersten Einschalten zunächst
          # Helligkeit plus Transition, danach Kelvin als separater Befehl.
          - if:
              - condition: template
                value_template: "{{ reassert_targets | count > 0 }}"
            then:
              - action: light.turn_on
                target:
                  entity_id: "{{ reassert_targets }}"
                data:
                  brightness_pct: "{{ target_brightness_pct }}"
                  transition: "{{ target_on_transition }}"
              - delay: !input color_command_delay
              - if:
                  - condition: template
                    value_template: >-
                      {% set selected = enable_helper_entity is string
                        and enable_helper_entity | length > 0 %}
                      {{ (not selected
                        or is_state(enable_helper_entity, 'on'))
                        and reassert_targets | count > 0 }}
                then:
                  # Tradfri: Kelvin bleibt auch beim Reassert getrennt.
                  - action: light.turn_on
                    target:
                      entity_id: "{{ reassert_targets }}"
                    data:
                      color_temp_kelvin: "{{ target_color_temp_kelvin }}"
                      transition: "{{ target_on_transition }}"

  # Mode-Latch: Dieser Lauf umfasst den kompletten Beleuchtungszyklus.
  # Zeit- oder Helper-Wechsel ändern target_* deshalb nicht mehr. Neue Bewegung
  # wird innerhalb der Schleife behandelt und startet keinen neuen Lauf.
  - repeat:
      while:
        - condition: template
          value_template: >-
            {% set selected = enable_helper_entity is string
              and enable_helper_entity | length > 0 %}
            {{ not selected or is_state(enable_helper_entity, 'on') }}
      sequence:
        # Erst alle exakt "off"; unknown/unavailable blockiert das Ausschalten.
        - wait_template: >-
            {% set selected = enable_helper_entity is string
              and enable_helper_entity | length > 0 %}
            {% set disabled = selected
              and not is_state(enable_helper_entity, 'on') %}
            {% set ns = namespace(all_off=(motion_entities | count > 0)) %}
            {% for entity_id in motion_entities %}
              {% if not is_state(entity_id, 'off') %}
                {% set ns.all_off = false %}
              {% endif %}
            {% endfor %}
            {{ disabled or ns.all_off }}

        - if:
            - condition: template
              value_template: >-
                {% set selected = enable_helper_entity is string
                  and enable_helper_entity | length > 0 %}
                {{ not selected or is_state(enable_helper_entity, 'on') }}
          then:
            # Bereits verstrichene Inaktivität während der Tradfri-Prüfung
            # wird von der konfigurierten Nachlaufzeit abgezogen.
            - variables:
                no_motion_remaining_seconds: >-
                  {% set d = no_motion_duration %}
                  {% set total =
                    (d.get('days', 0) | float * 86400)
                    + (d.get('hours', 0) | float * 3600)
                    + (d.get('minutes', 0) | float * 60)
                    + (d.get('seconds', 0) | float)
                    + (d.get('milliseconds', 0) | float / 1000) %}
                  {% set ns = namespace(latest=0) %}
                  {% for sensor in expand(motion_entities) %}
                    {% set changed = as_timestamp(sensor.last_changed, 0) %}
                    {% if changed > ns.latest %}
                      {% set ns.latest = changed %}
                    {% endif %}
                  {% endfor %}
                  {% set elapsed = as_timestamp(now()) - ns.latest
                    if ns.latest > 0 else 0 %}
                  {% set remaining = total - elapsed %}
                  {{ 0 if remaining <= 0
                    else remaining | round(0, 'ceil') | int }}

            # Bewegung beendet den Nachlauf sofort; die Schleife beginnt erneut.
            - wait_template: >-
                {% set selected = enable_helper_entity is string
                  and enable_helper_entity | length > 0 %}
                {% set disabled = selected
                  and not is_state(enable_helper_entity, 'on') %}
                {% set ns = namespace(any_on=false) %}
                {% for entity_id in motion_entities %}
                  {% if is_state(entity_id, 'on') %}
                    {% set ns.any_on = true %}
                  {% endif %}
                {% endfor %}
                {{ disabled or ns.any_on }}
              timeout:
                seconds: "{{ no_motion_remaining_seconds }}"
              continue_on_timeout: true

            # wait.completed bleibt auch dann true, wenn ein sehr kurzer
            # Bewegungsimpuls beim nächsten Schritt schon wieder "off" ist.
            - variables:
                no_motion_interrupted: "{{ wait.completed }}"

            # Bei Bewegung im normalen Nachlauf werden nur Lampen korrigiert,
            # deren Rückmeldung wirklich vom gespeicherten Ziel abweicht.
            - variables:
                motion_restore_targets: >-
                  {% set target = target_brightness_pct | float * 255 / 100 %}
                  {% set tolerance = brightness_tolerance_value | float %}
                  {% set ns = namespace(ids=[]) %}
                  {% for entity_id in resolved_lights %}
                    {% set reported = state_attr(entity_id, 'brightness') %}
                    {% if states(entity_id) != 'on'
                          or reported is none
                          or not is_number(reported)
                          or (((reported | float) - target) | abs) > tolerance %}
                      {% set ns.ids = ns.ids + [entity_id] %}
                    {% endif %}
                  {% endfor %}
                  {{ ns.ids }}
            - if:
                - condition: template
                  value_template: >-
                    {% set selected = enable_helper_entity is string
                      and enable_helper_entity | length > 0 %}
                    {{ (not selected or is_state(enable_helper_entity, 'on'))
                      and (no_motion_interrupted | bool)
                      and motion_restore_targets | count > 0 }}
              then:
                - action: light.turn_on
                  target:
                    entity_id: "{{ motion_restore_targets }}"
                  data:
                    brightness_pct: "{{ target_brightness_pct }}"
                - delay: !input color_command_delay
                - if:
                    - condition: template
                      value_template: >-
                        {% set selected = enable_helper_entity is string
                          and enable_helper_entity | length > 0 %}
                        {{ (not selected
                          or is_state(enable_helper_entity, 'on'))
                          and motion_restore_targets | count > 0 }}
                  then:
                    - action: light.turn_on
                      target:
                        entity_id: "{{ motion_restore_targets }}"
                      data:
                        color_temp_kelvin: "{{ target_color_temp_kelvin }}"
                        transition: "{{ target_on_transition }}"

            # Nur ein echter Timeout bei weiterhin vollständig inaktiven Sensoren
            # darf Warn-Dimmen und den finalen Ausschaltpfad betreten.
            - if:
                - condition: template
                  value_template: >-
                    {% set selected = enable_helper_entity is string
                      and enable_helper_entity | length > 0 %}
                    {% set ns = namespace(all_off=(motion_entities | count > 0)) %}
                    {% for entity_id in motion_entities %}
                      {% if not is_state(entity_id, 'off') %}
                        {% set ns.all_off = false %}
                      {% endif %}
                    {% endfor %}
                    {{ (not selected or is_state(enable_helper_entity, 'on'))
                      and not (no_motion_interrupted | bool)
                      and ns.all_off }}
              then:
                # Ohne Warn-Dimmen bleibt dieses Flag false. Die Variable wird
                # vor dem verschachtelten If angelegt, damit dessen Update im
                # gesamten Script-Lauf sichtbar bleibt.
                - variables:
                    warning_interrupted: false

                # Warn-Dimmen basiert direkt auf dem gespeicherten Zielwert.
                - if:
                    - condition: template
                      value_template: "{{ use_warning | bool }}"
                  then:
                    - variables:
                        warning_targets: >-
                          {% set ns = namespace(ids=[]) %}
                          {% for entity_id in resolved_lights %}
                            {% if is_state(entity_id, 'on') %}
                              {% set ns.ids = ns.ids + [entity_id] %}
                            {% endif %}
                          {% endfor %}
                          {{ ns.ids }}
                    - if:
                        - condition: template
                          value_template: >-
                            {{ warning_targets | count > 0 }}
                      then:
                        - action: light.turn_on
                          target:
                            entity_id: "{{ warning_targets }}"
                          data:
                            brightness_pct: "{{ warning_brightness_pct }}"
                            transition: "{{ warning_transition_seconds }}"
                    - wait_template: >-
                        {% set selected = enable_helper_entity is string
                          and enable_helper_entity | length > 0 %}
                        {% set disabled = selected
                          and not is_state(enable_helper_entity, 'on') %}
                        {% set ns = namespace(any_on=false) %}
                        {% for entity_id in motion_entities %}
                          {% if is_state(entity_id, 'on') %}
                            {% set ns.any_on = true %}
                          {% endif %}
                        {% endfor %}
                        {{ disabled or ns.any_on }}
                      timeout: !input warning_duration
                      continue_on_timeout: true
                    - variables:
                        warning_interrupted: "{{ wait.completed }}"

                # Bei Bewegung im Warnfenster wird exakt der gespeicherte Modus
                # wiederhergestellt, nicht aus der Warnhelligkeit zurückgerechnet.
                - if:
                    - condition: template
                      value_template: >-
                        {% set selected = enable_helper_entity is string
                          and enable_helper_entity | length > 0 %}
                        {{ use_warning | bool
                          and (not selected
                            or is_state(enable_helper_entity, 'on'))
                          and (warning_interrupted | bool)
                          and resolved_lights | count > 0 }}
                  then:
                    - action: light.turn_on
                      target:
                        entity_id: "{{ resolved_lights }}"
                      data:
                        brightness_pct: "{{ target_brightness_pct }}"
                        transition: "{{ warning_transition_seconds }}"
                    - delay: !input color_command_delay
                    - if:
                        - condition: template
                          value_template: >-
                            {% set selected = enable_helper_entity is string
                              and enable_helper_entity | length > 0 %}
                            {{ (not selected
                              or is_state(enable_helper_entity, 'on'))
                              and resolved_lights | count > 0 }}
                      then:
                        # Tradfri: auch beim Restore Helligkeit und Kelvin trennen.
                        - action: light.turn_on
                          target:
                            entity_id: "{{ resolved_lights }}"
                          data:
                            color_temp_kelvin: >-
                              {{ target_color_temp_kelvin }}
                            transition: "{{ warning_transition_seconds }}"

                # Ohne neue Bewegung beginnt jetzt der einzige finale Off-Pfad.
                - if:
                    - condition: template
                      value_template: >-
                        {% set selected = enable_helper_entity is string
                          and enable_helper_entity | length > 0 %}
                        {% set ns = namespace(all_off=(motion_entities | count > 0)) %}
                        {% for entity_id in motion_entities %}
                          {% if not is_state(entity_id, 'off') %}
                            {% set ns.all_off = false %}
                          {% endif %}
                        {% endfor %}
                        {{ (not selected
                          or is_state(enable_helper_entity, 'on'))
                          and not (warning_interrupted | bool)
                          and ns.all_off and lights | count > 0 }}
                  then:
                    - action: light.turn_off
                      target:
                        entity_id: "{{ lights }}"
                      data:
                        transition: "{{ off_transition_seconds }}"

                    # Auch der Off-Retry ist bewegungsreaktiv. Bewegung erhält
                    # den alten Snapshot, solange der Off-Pfad nicht vollständig
                    # abgeschlossen wurde.
                    - wait_template: >-
                        {% set selected = enable_helper_entity is string
                          and enable_helper_entity | length > 0 %}
                        {% set disabled = selected
                          and not is_state(enable_helper_entity, 'on') %}
                        {% set ns = namespace(any_on=false) %}
                        {% for entity_id in motion_entities %}
                          {% if is_state(entity_id, 'on') %}
                            {% set ns.any_on = true %}
                          {% endif %}
                        {% endfor %}
                        {{ disabled or ns.any_on }}
                      timeout: !input off_retry_delay
                      continue_on_timeout: true
                    - variables:
                        off_retry_interrupted: "{{ wait.completed }}"

                    - if:
                        - condition: template
                          value_template: >-
                            {% set selected = enable_helper_entity is string
                              and enable_helper_entity | length > 0 %}
                            {{ (not selected
                              or is_state(enable_helper_entity, 'on'))
                              and (off_retry_interrupted | bool)
                              and resolved_lights | count > 0 }}
                      then:
                        - action: light.turn_on
                          target:
                            entity_id: "{{ resolved_lights }}"
                          data:
                            brightness_pct: "{{ target_brightness_pct }}"
                        - delay: !input color_command_delay
                        - if:
                            - condition: template
                              value_template: >-
                                {% set selected =
                                  enable_helper_entity is string
                                  and enable_helper_entity | length > 0 %}
                                {{ (not selected
                                  or is_state(enable_helper_entity, 'on'))
                                  and resolved_lights | count > 0 }}
                          then:
                            - action: light.turn_on
                              target:
                                entity_id: "{{ resolved_lights }}"
                              data:
                                color_temp_kelvin: >-
                                  {{ target_color_temp_kelvin }}
                                transition: "{{ target_on_transition }}"
                      else:
                        - if:
                            - condition: template
                              value_template: >-
                                {% set selected =
                                  enable_helper_entity is string
                                  and enable_helper_entity | length > 0 %}
                                {% set ns = namespace(
                                  all_off=(motion_entities | count > 0)) %}
                                {% for entity_id in motion_entities %}
                                  {% if not is_state(entity_id, 'off') %}
                                    {% set ns.all_off = false %}
                                  {% endif %}
                                {% endfor %}
                                {{ (not selected
                                  or is_state(enable_helper_entity, 'on'))
                                  and not (off_retry_interrupted | bool)
                                  and ns.all_off and lights | count > 0 }}
                          then:
                            - variables:
                                off_retry_targets: >-
                                  {% if force_off_retry_enabled | bool %}
                                    {{ lights }}
                                  {% else %}
                                    {% set ns = namespace(ids=[]) %}
                                    {% for entity_id in resolved_lights %}
                                      {% if is_state(entity_id, 'on') %}
                                        {% set ns.ids = ns.ids + [entity_id] %}
                                      {% endif %}
                                    {% endfor %}
                                    {{ ns.ids }}
                                  {% endif %}
                            - if:
                                - condition: template
                                  value_template: >-
                                    {{ off_retry_targets | count > 0 }}
                              then:
                                - action: light.turn_off
                                  target:
                                    entity_id: "{{ off_retry_targets }}"
                            # Auch ohne Retry-Target muss die Repeat-Schleife enden.
                            - stop: Beleuchtungszyklus vollständig beendet

Anschließend könnt ihr mit dem Blueprint eine Automation anlegen und die entsprechenden Einstellungen vornehmen, welche Lampen ihr wie steuern wollt. Das war’s schon! Falls ihr die Automation benutzt, lasst mich gerne wissen, wie es funktioniert.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre, wie deine Kommentardaten verarbeitet werden.