Last active
January 3, 2026 20:01
-
-
Save basnijholt/39205a1082f2740a930b437b12db290b to your computer and use it in GitHub Desktop.
Home Assistant Blueprint: IKEA STYRBAR with color temperature cycling
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| blueprint: | |
| name: ZHA – IKEA STYRBAR all controls & 6-temp palette (modern syntax) | |
| description: > | |
| Smooth, responsive control for the IKEA STYRBAR (square) remote on ZHA. | |
| • UP/DOWN (short): ON / OFF | |
| • UP/DOWN (hold): fast and smooth brightness stepping that stops immediately on release | |
| (implemented with `mode: restart` for zero run-on) | |
| • LEFT/RIGHT (short): cycle through 6 preset color temperatures (wrap-around; uses an input_number helper to track index) | |
| • LEFT/RIGHT (hold): smooth color temperature stepping that stops immediately on release | |
| Setup notes: | |
| 1) If you want LEFT/RIGHT short-press cycling, create an *input_number* helper with min=1, max=6, step=1. | |
| Example entity id: input_number.styrbar_color_index (you can choose another; select it below). | |
| 2) Choose your 6 color temperature presets (in Kelvin) for short-press cycling. | |
| 3) Adjust hold speed with "Tick interval (ms)", brightness step, and color temp step. | |
| domain: automation | |
| input: | |
| remote: | |
| name: Remote (ZHA device) | |
| selector: | |
| device: | |
| integration: zha | |
| manufacturer: IKEA of Sweden | |
| model: Remote Control N2 | |
| multiple: false | |
| light: | |
| name: Light(s) | |
| selector: | |
| target: | |
| entity: | |
| domain: light | |
| # Stores the current palette index (1..6) for LEFT/RIGHT cycling | |
| color_index_helper: | |
| name: Color index helper (1–6) | |
| description: Create/select an input_number with min=1, max=6, step=1 to remember the active palette color. | |
| default: input_number.styrbar_color_index | |
| selector: | |
| entity: | |
| domain: input_number | |
| # 6-color temperature palette (Kelvin) | |
| palette_temp1: { name: Temp 1 (Kelvin), default: 2700, selector: { color_temp: { unit: kelvin, min: 2000, max: 6500 } } } | |
| palette_temp2: { name: Temp 2 (Kelvin), default: 3000, selector: { color_temp: { unit: kelvin, min: 2000, max: 6500 } } } | |
| palette_temp3: { name: Temp 3 (Kelvin), default: 4000, selector: { color_temp: { unit: kelvin, min: 2000, max: 6500 } } } | |
| palette_temp4: { name: Temp 4 (Kelvin), default: 5000, selector: { color_temp: { unit: kelvin, min: 2000, max: 6500 } } } | |
| palette_temp5: { name: Temp 5 (Kelvin), default: 5500, selector: { color_temp: { unit: kelvin, min: 2000, max: 6500 } } } | |
| palette_temp6: { name: Temp 6 (Kelvin), default: 6500, selector: { color_temp: { unit: kelvin, min: 2000, max: 6500 } } } | |
| # Hold behavior (tuning) | |
| tick_ms: | |
| name: Tick interval (ms) | |
| description: Time between brightness steps while holding. Smaller = faster/smoother. | |
| default: 10 | |
| selector: | |
| number: | |
| min: 5 | |
| max: 200 | |
| step: 5 | |
| unit_of_measurement: ms | |
| mode: slider | |
| step_pct: | |
| name: Brightness step (% per tick) | |
| description: Amount of brightness changed per tick while holding UP/DOWN. | |
| default: 5 | |
| selector: | |
| number: | |
| min: 1 | |
| max: 20 | |
| step: 1 | |
| unit_of_measurement: '%' | |
| mode: slider | |
| # Color temperature hold behavior | |
| temp_step_kelvin: | |
| name: Color temp step (K per tick) | |
| description: Amount of color temperature changed per tick while holding LEFT/RIGHT. | |
| default: 50 | |
| selector: | |
| number: | |
| min: 10 | |
| max: 200 | |
| step: 10 | |
| unit_of_measurement: K | |
| mode: slider | |
| temp_min_kelvin: | |
| name: Min color temp (K) | |
| description: Minimum color temperature limit. | |
| default: 2000 | |
| selector: | |
| number: | |
| min: 1500 | |
| max: 4000 | |
| step: 100 | |
| unit_of_measurement: K | |
| mode: slider | |
| temp_max_kelvin: | |
| name: Max color temp (K) | |
| description: Maximum color temperature limit. | |
| default: 6500 | |
| selector: | |
| number: | |
| min: 4000 | |
| max: 10000 | |
| step: 100 | |
| unit_of_measurement: K | |
| mode: slider | |
| # Restart mode ensures release events cancel the running hold loop immediately (no run-on) | |
| mode: restart | |
| max_exceeded: silent | |
| trigger: | |
| - platform: event | |
| event_type: zha_event | |
| event_data: | |
| device_id: !input remote | |
| action: | |
| # -------- Modern-syntax variables (scoped to this run) -------- | |
| - variables: | |
| color_index_entity: !input color_index_helper | |
| tick_ms_input: !input tick_ms | |
| step_pct_input: !input step_pct | |
| temp_step_input: !input temp_step_kelvin | |
| temp_min_input: !input temp_min_kelvin | |
| temp_max_input: !input temp_max_kelvin | |
| light_target: !input light | |
| tick_s: "{{ (tick_ms_input | float(10)) / 1000 }}" | |
| step_up: "{{ step_pct_input | int(5) }}" | |
| step_down: "{{ (step_pct_input | int(5)) * -1 }}" | |
| temp_step: "{{ temp_step_input | int(50) }}" | |
| temp_min: "{{ temp_min_input | int(2000) }}" | |
| temp_max: "{{ temp_max_input | int(6500) }}" | |
| cap_ticks: 1200 # ~12s watchdog at 10ms ticks (failsafe) | |
| cmd: "{{ trigger.event.data.command }}" | |
| cluster: "{{ trigger.event.data.cluster_id }}" | |
| args: "{{ trigger.event.data.args | default([]) }}" | |
| - choose: | |
| # --- UP short → ON --- | |
| - conditions: "{{ cluster == 6 and cmd == 'on' }}" | |
| sequence: | |
| - service: light.turn_on | |
| target: !input light | |
| data: { transition: 0.1 } | |
| # --- DOWN short → OFF --- | |
| - conditions: "{{ cluster == 6 and cmd == 'off' }}" | |
| sequence: | |
| - service: light.turn_off | |
| target: !input light | |
| # --- HOLD UP (brightness +) — step on timeout, stop on release/opposite --- | |
| - conditions: "{{ cluster == 8 and cmd == 'move_with_on_off' }}" | |
| sequence: | |
| - variables: { _i: 0 } | |
| - repeat: | |
| sequence: | |
| # Wait first; step only if still holding | |
| - wait_for_trigger: | |
| # Release / stop | |
| - platform: event | |
| event_type: zha_event | |
| event_data: { device_id: !input remote, cluster_id: 8, command: "stop" } | |
| - platform: event | |
| event_type: zha_event | |
| event_data: { device_id: !input remote, cluster_id: 8, command: "stop_with_on_off" } | |
| # Opposite hold also cancels | |
| - platform: event | |
| event_type: zha_event | |
| event_data: { device_id: !input remote, cluster_id: 8, command: "move" } | |
| timeout: "{{ tick_s }}" | |
| continue_on_timeout: true | |
| - choose: | |
| - conditions: "{{ not wait.completed }}" | |
| sequence: | |
| - service: light.turn_on | |
| target: !input light | |
| data: { brightness_step_pct: "{{ step_up }}", transition: 0 } | |
| - variables: { _i: "{{ _i + 1 }}" } | |
| until: | |
| - condition: template | |
| value_template: "{{ wait.completed or (_i | int) >= (cap_ticks | int) }}" | |
| # --- HOLD DOWN (brightness −) — step on timeout, stop on release/opposite --- | |
| - conditions: "{{ cluster == 8 and cmd == 'move' }}" | |
| sequence: | |
| - variables: { _i: 0 } | |
| - repeat: | |
| sequence: | |
| - wait_for_trigger: | |
| # Release / stop | |
| - platform: event | |
| event_type: zha_event | |
| event_data: { device_id: !input remote, cluster_id: 8, command: "stop" } | |
| - platform: event | |
| event_type: zha_event | |
| event_data: { device_id: !input remote, cluster_id: 8, command: "stop_with_on_off" } | |
| # Opposite hold also cancels | |
| - platform: event | |
| event_type: zha_event | |
| event_data: { device_id: !input remote, cluster_id: 8, command: "move_with_on_off" } | |
| timeout: "{{ tick_s }}" | |
| continue_on_timeout: true | |
| - choose: | |
| - conditions: "{{ not wait.completed }}" | |
| sequence: | |
| - service: light.turn_on | |
| target: !input light | |
| data: { brightness_step_pct: "{{ step_down }}", transition: 0 } | |
| - variables: { _i: "{{ _i + 1 }}" } | |
| until: | |
| - condition: template | |
| value_template: "{{ wait.completed or (_i | int) >= (cap_ticks | int) }}" | |
| # --- LEFT/RIGHT short → color temp cycle (wrap) --- | |
| - conditions: "{{ cluster == 5 and cmd == 'press' and args in [[256,13,0],[257,13,0]] }}" | |
| sequence: | |
| - variables: | |
| curr_idx: "{{ states(color_index_entity) | int(1) }}" | |
| next_idx: > | |
| {% if args == [256,13,0] %} | |
| {{ 1 if curr_idx >= 6 else curr_idx + 1 }} | |
| {% else %} | |
| {{ 6 if curr_idx <= 1 else curr_idx - 1 }} | |
| {% endif %} | |
| - service: input_number.set_value | |
| target: { entity_id: !input color_index_helper } | |
| data: { value: "{{ next_idx }}" } | |
| - choose: | |
| - conditions: "{{ next_idx == 1 }}" | |
| sequence: | |
| - service: light.turn_on | |
| target: !input light | |
| data: { color_temp_kelvin: !input palette_temp1, transition: 0 } | |
| - conditions: "{{ next_idx == 2 }}" | |
| sequence: | |
| - service: light.turn_on | |
| target: !input light | |
| data: { color_temp_kelvin: !input palette_temp2, transition: 0 } | |
| - conditions: "{{ next_idx == 3 }}" | |
| sequence: | |
| - service: light.turn_on | |
| target: !input light | |
| data: { color_temp_kelvin: !input palette_temp3, transition: 0 } | |
| - conditions: "{{ next_idx == 4 }}" | |
| sequence: | |
| - service: light.turn_on | |
| target: !input light | |
| data: { color_temp_kelvin: !input palette_temp4, transition: 0 } | |
| - conditions: "{{ next_idx == 5 }}" | |
| sequence: | |
| - service: light.turn_on | |
| target: !input light | |
| data: { color_temp_kelvin: !input palette_temp5, transition: 0 } | |
| - conditions: "{{ next_idx == 6 }}" | |
| sequence: | |
| - service: light.turn_on | |
| target: !input light | |
| data: { color_temp_kelvin: !input palette_temp6, transition: 0 } | |
| # --- HOLD LEFT (color temp warmer / lower K) — step on timeout, stop on release --- | |
| - conditions: "{{ cluster == 5 and cmd == 'hold' and args == [3329, 0] }}" | |
| sequence: | |
| - variables: | |
| _i: 0 | |
| # Get first light entity from target to read current color temp | |
| first_light: > | |
| {% set entities = light_target.entity_id if light_target.entity_id is defined else [] %} | |
| {% if entities is string %}{{ entities }}{% elif entities | length > 0 %}{{ entities[0] }}{% else %}{% endif %} | |
| - repeat: | |
| sequence: | |
| - wait_for_trigger: | |
| - platform: event | |
| event_type: zha_event | |
| event_data: { device_id: !input remote, cluster_id: 5, command: "release" } | |
| # Opposite hold also cancels | |
| - platform: event | |
| event_type: zha_event | |
| event_data: { device_id: !input remote, cluster_id: 5, command: "hold", args: [3328, 0] } | |
| timeout: "{{ tick_s }}" | |
| continue_on_timeout: true | |
| - choose: | |
| - conditions: "{{ not wait.completed }}" | |
| sequence: | |
| - variables: | |
| current_temp: "{{ state_attr(first_light, 'color_temp_kelvin') | int(4000) }}" | |
| new_temp: "{{ [temp_min | int, current_temp - (temp_step | int)] | max }}" | |
| - service: light.turn_on | |
| target: !input light | |
| data: | |
| color_temp_kelvin: "{{ new_temp }}" | |
| transition: 0 | |
| - variables: { _i: "{{ _i + 1 }}" } | |
| until: | |
| - condition: template | |
| value_template: "{{ wait.completed or (_i | int) >= (cap_ticks | int) }}" | |
| # --- HOLD RIGHT (color temp cooler / higher K) — step on timeout, stop on release --- | |
| - conditions: "{{ cluster == 5 and cmd == 'hold' and args == [3328, 0] }}" | |
| sequence: | |
| - variables: | |
| _i: 0 | |
| first_light: > | |
| {% set entities = light_target.entity_id if light_target.entity_id is defined else [] %} | |
| {% if entities is string %}{{ entities }}{% elif entities | length > 0 %}{{ entities[0] }}{% else %}{% endif %} | |
| - repeat: | |
| sequence: | |
| - wait_for_trigger: | |
| - platform: event | |
| event_type: zha_event | |
| event_data: { device_id: !input remote, cluster_id: 5, command: "release" } | |
| # Opposite hold also cancels | |
| - platform: event | |
| event_type: zha_event | |
| event_data: { device_id: !input remote, cluster_id: 5, command: "hold", args: [3329, 0] } | |
| timeout: "{{ tick_s }}" | |
| continue_on_timeout: true | |
| - choose: | |
| - conditions: "{{ not wait.completed }}" | |
| sequence: | |
| - variables: | |
| current_temp: "{{ state_attr(first_light, 'color_temp_kelvin') | int(4000) }}" | |
| new_temp: "{{ [temp_max | int, current_temp + (temp_step | int)] | min }}" | |
| - service: light.turn_on | |
| target: !input light | |
| data: | |
| color_temp_kelvin: "{{ new_temp }}" | |
| transition: 0 | |
| - variables: { _i: "{{ _i + 1 }}" } | |
| until: | |
| - condition: template | |
| value_template: "{{ wait.completed or (_i | int) >= (cap_ticks | int) }}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment