blueprint: name: IKEA E2490 BILRESA Scroll Wheel (Zigbee2MQTT + ZHA) description: 'Unified controller blueprint for IKEA E2490 BILRESA scroll wheel working with Zigbee2MQTT and ZHA. - Buttons: on, off, on_double, off_double - Scroll: brightness_move_to_level with action_level from Zigbee2MQTT main topic, or ZHA move_to_level args Supports light brightness, media_player volume, light color_temp, light hue. Version: 2026-01-18 ' domain: automation input: controller_device: name: Controller Device (Zigbee2MQTT or ZHA) description: Select the E2490 device integrated via Zigbee2MQTT or ZHA. selector: device: filter: - integration: mqtt - integration: zha multiple: false scroll_target: name: Scroll wheel target entity (light) selector: entity: filter: - domain: - light multiple: false reorder: false scroll_mode: name: Default scroll mode description: Default mode when automation starts or after auto-reset timeout default: brightness selector: select: mode: dropdown options: - label: Brightness (light) value: brightness - label: Volume (media_player) value: volume - label: Color temperature (light) value: color_temp - label: Hue (light) value: hue sort: false custom_value: false multiple: false volume_target: name: Volume target (media_player) default: selector: entity: filter: - domain: - media_player multiple: false reorder: false color_temp_target: name: Color temperature target (light) default: selector: entity: filter: - domain: - light multiple: false reorder: false hue_target: name: Hue target (light) default: selector: entity: filter: - domain: - light multiple: false reorder: false scroll_mode_helper: name: (Optional) Scroll mode helper (input_select) description: 'Provide an input_select to allow cycling scroll modes at runtime with double ON. Must have options: brightness, volume, color_temp, hue (in that order)' default: selector: entity: filter: - domain: - input_select multiple: false reorder: false last_activity_helper: name: (Optional) Last activity helper (input_datetime) description: Provide an input_datetime to enable auto-reset after inactivity. Works with scroll mode helper and reset timeout. default: selector: entity: filter: - domain: - input_datetime multiple: false reorder: false reset_timeout: name: (Optional) Auto-reset timeout (seconds) description: Time in seconds of inactivity before resetting to default scroll mode. Set to 0 to disable auto-reset. Requires both scroll mode helper and last activity helper. default: 60 selector: number: min: 0.0 max: 3600.0 step: 1.0 unit_of_measurement: seconds mode: box volume_max: name: (Optional) Max volume description: Upper limit for volume mode (0.0 - 1.0). Values from the wheel will be clamped to this maximum. default: 1.0 selector: number: min: 0.0 max: 1.0 step: 0.01 mode: slider button_on_short: name: Button ON - short press (optional) default: [] selector: action: {} button_off_short: name: Button OFF - short press (optional) default: [] selector: action: {} button_on_double: name: Button ON - double press (optional) default: [] selector: action: {} button_off_double: name: Button OFF - double press (optional) default: [] selector: action: {} source_url: https://gist.github.com/mcinnes01/399132520034291b89a82659653b2893 alias: IKEA E2490 BILRESA Scroll Wheel (Zigbee2MQTT + ZHA) variables: controller_device: !input controller_device friendly_name: '{{ device_attr(controller_device, ''name'') | default('''') }}' z2m_topic: zigbee2mqtt/{{ friendly_name }} scroll_mode: !input scroll_mode scroll_mode_helper: !input scroll_mode_helper last_activity_helper: !input last_activity_helper reset_timeout: !input reset_timeout effective_mode: '{{ states(scroll_mode_helper) if scroll_mode_helper is not none and states(scroll_mode_helper) != '''' else scroll_mode }}' brightness_target: !input scroll_target volume_target: !input volume_target color_temp_target: !input color_temp_target hue_target: !input hue_target volume_max: !input volume_max available_modes: '{% set modes = [] %} {% if brightness_target is not none %}{% set modes = modes + [''brightness''] %}{% endif %} {% if volume_target is not none %}{% set modes = modes + [''volume''] %}{% endif %} {% if color_temp_target is not none %}{% set modes = modes + [''color_temp''] %}{% endif %} {% if hue_target is not none %}{% set modes = modes + [''hue''] %}{% endif %} {{ modes }} ' button_on_short_seq: !input button_on_short button_off_short_seq: !input button_off_short button_on_double_seq: !input button_on_double button_off_double_seq: !input button_off_double trigger: - platform: device id: z2m-on domain: mqtt device_id: !input controller_device type: action subtype: 'on' - platform: device id: z2m-off domain: mqtt device_id: !input controller_device type: action subtype: 'off' - platform: device id: z2m-on-double domain: mqtt device_id: !input controller_device type: action subtype: on_double - platform: device id: z2m-off-double domain: mqtt device_id: !input controller_device type: action subtype: off_double - platform: mqtt id: z2m-payload topic: zigbee2mqtt/# - platform: event id: zha-on event_type: zha_event event_data: device_id: !input controller_device command: 'on' - platform: event id: zha-off event_type: zha_event event_data: device_id: !input controller_device command: 'off' - platform: event id: zha-level event_type: zha_event event_data: device_id: !input controller_device command: move_to_level condition: [] action: - if: - condition: template value_template: "{{ scroll_mode_helper is not none \n and last_activity_helper is not none \n and reset_timeout > 0\n and states(last_activity_helper) not in ['unknown', 'unavailable', ''] \n and (now() - (states(last_activity_helper) | as_datetime)).total_seconds() > reset_timeout }}\n" then: - service: input_select.select_option target: entity_id: '{{ scroll_mode_helper }}' data: option: '{{ scroll_mode }}' - choose: - conditions: - condition: template value_template: "{{ trigger.id == 'z2m-payload' \n and trigger.topic == z2m_topic \n and trigger.payload_json is defined \n and trigger.payload_json.action == 'brightness_move_to_level' }}\n" sequence: - variables: lvl: '{{ trigger.payload_json.action_level | default(none) }}' trans: '{{ trigger.payload_json.action_transition_time | default(0) }}' - choose: - conditions: - condition: template value_template: '{{ effective_mode == ''brightness'' and brightness_target is not none }}' sequence: - service: light.turn_on target: entity_id: '{{ brightness_target }}' data: brightness: '{% if lvl is none %}255{% else %}{{ ((lvl|float/241.0)*255.0)|round(0)|int }}{% endif %} ' transition: '{{ trans }}' - if: - condition: template value_template: '{{ last_activity_helper is not none }}' then: - service: input_datetime.set_datetime target: entity_id: '{{ last_activity_helper }}' data: timestamp: '{{ now().timestamp() }}' - conditions: - condition: template value_template: '{{ effective_mode == ''volume'' and volume_target is not none }}' sequence: - service: media_player.volume_set target: entity_id: '{{ volume_target }}' data: volume_level: "{% if lvl is none %}\n {{ volume_max }}\n{% else %}\n \ {{ ((lvl|float/241.0) * volume_max)|round(3) }}\n{% endif %}\n" - if: - condition: template value_template: '{{ last_activity_helper is not none }}' then: - service: input_datetime.set_datetime target: entity_id: '{{ last_activity_helper }}' data: timestamp: '{{ now().timestamp() }}' - conditions: - condition: template value_template: '{{ effective_mode == ''color_temp'' and color_temp_target is not none }}' sequence: - variables: min_mireds: '{{ state_attr(color_temp_target, ''min_mireds'') | default(153) }}' max_mireds: '{{ state_attr(color_temp_target, ''max_mireds'') | default(500) }}' - service: light.turn_on target: entity_id: '{{ color_temp_target }}' data: color_temp: "{% set minm = min_mireds|int %} {% set maxm = max_mireds|int %} {% if lvl is none %}\n {{ maxm }}\n{% else %}\n {% set ratio = ((lvl|float - 1.0)/240.0) %}\n {{ (minm + ratio*(maxm-minm))|round(0)|int }}\n{% endif %}\n" - if: - condition: template value_template: '{{ last_activity_helper is not none }}' then: - service: input_datetime.set_datetime target: entity_id: '{{ last_activity_helper }}' data: timestamp: '{{ now().timestamp() }}' - conditions: - condition: template value_template: '{{ effective_mode == ''hue'' and hue_target is not none }}' sequence: - variables: current_hs: '{{ state_attr(hue_target, ''hs_color'') | default([0, 100]) }}' current_sat: '{{ current_hs[1] if current_hs is sequence and current_hs|length > 1 else 100 }}' hue_val: '{% if lvl is none %}360{% else %}{{ ((lvl|float/241.0)*360.0)|round(0)|int }}{% endif %} ' - service: light.turn_on target: entity_id: '{{ hue_target }}' data: hs_color: '[ {{ hue_val }}, {{ current_sat }} ]' - if: - condition: template value_template: '{{ last_activity_helper is not none }}' then: - service: input_datetime.set_datetime target: entity_id: '{{ last_activity_helper }}' data: timestamp: '{{ now().timestamp() }}' - conditions: - condition: template value_template: '{{ trigger.id == ''zha-level'' }}' sequence: - variables: lvl: '{% set a = trigger.event.data.args | default([]) %} {% if a is sequence and a|length > 0 %}{{ a[0] }}{% else %}{{ none }}{% endif %} ' trans: '{% set a = trigger.event.data.args | default([]) %} {% if a is sequence and a|length > 1 %}{{ a[1] }}{% else %}0{% endif %} ' - choose: - conditions: - condition: template value_template: '{{ effective_mode == ''brightness'' and brightness_target is not none }}' sequence: - service: light.turn_on target: entity_id: '{{ brightness_target }}' data: brightness: '{% if lvl is none %}255{% else %}{{ (lvl|int)|min(255)|max(1) }}{% endif %} ' transition: '{{ trans }}' - if: - condition: template value_template: '{{ last_activity_helper is not none }}' then: - service: input_datetime.set_datetime target: entity_id: '{{ last_activity_helper }}' data: timestamp: '{{ now().timestamp() }}' - conditions: - condition: template value_template: '{{ trigger.id in [''z2m-on'',''zha-on''] }}' sequence: - choose: - conditions: - condition: template value_template: '{{ button_on_short_seq | length > 0 }}' sequence: !input button_on_short default: - choose: - conditions: - condition: template value_template: '{{ effective_mode == ''volume'' and volume_target is not none }}' sequence: - service: media_player.turn_on target: entity_id: '{{ volume_target }}' - conditions: [] sequence: - service: light.turn_on target: entity_id: !input scroll_target - conditions: - condition: template value_template: '{{ trigger.id in [''z2m-off'',''zha-off''] }}' sequence: - choose: - conditions: - condition: template value_template: '{{ button_off_short_seq | length > 0 }}' sequence: !input button_off_short default: - choose: - conditions: - condition: template value_template: '{{ effective_mode == ''volume'' and volume_target is not none }}' sequence: - service: media_player.turn_off target: entity_id: '{{ volume_target }}' - conditions: [] sequence: - service: light.turn_off target: entity_id: !input scroll_target - conditions: - condition: template value_template: '{{ trigger.id == ''z2m-on-double'' }}' sequence: - choose: - conditions: - condition: template value_template: '{{ scroll_mode_helper is not none }}' sequence: - variables: current_mode: '{{ states(scroll_mode_helper) }}' modes_list: '{{ available_modes }}' current_index: '{{ modes_list.index(current_mode) if current_mode in modes_list else 0 }}' next_index: '{{ (current_index + 1) % (modes_list | length) }}' next_mode: '{{ modes_list[next_index] if modes_list | length > 0 else ''brightness'' }}' - service: input_select.select_option target: entity_id: '{{ scroll_mode_helper }}' data: option: '{{ next_mode }}' - if: - condition: template value_template: '{{ last_activity_helper is not none }}' then: - service: input_datetime.set_datetime target: entity_id: '{{ last_activity_helper }}' data: timestamp: '{{ now().timestamp() }}' - choose: - conditions: - condition: template value_template: '{{ button_on_double_seq | length > 0 }}' sequence: !input button_on_double - conditions: [] sequence: - choose: - conditions: - condition: template value_template: '{{ button_on_double_seq | length > 0 }}' sequence: !input button_on_double - conditions: [] sequence: - service: light.turn_on target: entity_id: !input scroll_target data: brightness: 255 - conditions: - condition: template value_template: '{{ trigger.id == ''z2m-off-double'' }}' sequence: - choose: - conditions: - condition: template value_template: '{{ button_off_double_seq | length > 0 }}' sequence: !input button_off_double default: - service: light.turn_off target: entity_id: !input scroll_target mode: restart max_exceeded: silent