Skip to content

Instantly share code, notes, and snippets.

@mcinnes01
Last active January 18, 2026 23:27
Show Gist options
  • Select an option

  • Save mcinnes01/399132520034291b89a82659653b2893 to your computer and use it in GitHub Desktop.

Select an option

Save mcinnes01/399132520034291b89a82659653b2893 to your computer and use it in GitHub Desktop.
IKEA E2490 BILRESA Scroll Wheel
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
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
volume_target:
name: Volume target (media_player)
default: null
selector:
entity:
filter:
- domain: media_player
color_temp_target:
name: Color temperature target (light)
default: null
selector:
entity:
filter:
- domain: light
hue_target:
name: Hue target (light)
default: null
selector:
entity:
filter:
- domain: light
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: null
selector:
entity:
filter:
- domain: input_select
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: null
selector:
entity:
filter:
- domain: input_datetime
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
max: 3600
step: 1
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: {}
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
# Build list of available modes based on configured targets
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:
# Zigbee2MQTT device triggers for discrete actions
- 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
# Zigbee2MQTT main topic for payload_json with action_level (subscribe wide, filter in actions)
- platform: mqtt
id: z2m-payload
topic: zigbee2mqtt/#
# ZHA events
- 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:
# Check and reset mode if inactive for configured timeout
- if:
- condition: template
value_template: >
{{ scroll_mode_helper is not none
and last_activity_helper is not none
and reset_timeout > 0
and states(last_activity_helper) not in ['unknown', 'unavailable', '']
and (now() - (states(last_activity_helper) | as_datetime)).total_seconds() > reset_timeout }}
then:
- service: input_select.select_option
target:
entity_id: "{{ scroll_mode_helper }}"
data:
option: "{{ scroll_mode }}"
- choose:
# Handle Zigbee2MQTT payload with level
- conditions:
- condition: template
value_template: >
{{ trigger.id == 'z2m-payload'
and trigger.topic == z2m_topic
and trigger.payload_json is defined
and trigger.payload_json.action == 'brightness_move_to_level' }}
sequence:
- variables:
lvl: "{{ trigger.payload_json.action_level | default(none) }}"
trans: "{{ trigger.payload_json.action_transition_time | default(0) }}"
- choose:
# Brightness mode
- 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 }}"
# Update last activity timestamp
- 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() }}"
# Volume mode
- 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 %}
{{ volume_max }}
{% else %}
{{ ((lvl|float/241.0) * volume_max)|round(3) }}
{% endif %}
# Update last activity timestamp
- 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() }}"
# Color temperature mode
- 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 %}
{{ maxm }}
{% else %}
{% set ratio = ((lvl|float - 1.0)/240.0) %}
{{ (minm + ratio*(maxm-minm))|round(0)|int }}
{% endif %}
# Update last activity timestamp
- 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() }}"
# Hue mode
- 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 }} ]"
# Update last activity timestamp
- 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() }}"
# ZHA move_to_level (args contain level)
- 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 }}"
# Update last activity timestamp
- 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() }}"
# ON
- 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
# OFF
- 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
# ON DOUBLE
- conditions:
- condition: template
value_template: "{{ trigger.id == 'z2m-on-double' }}"
sequence:
- choose:
# If a mode helper is provided, cycle the mode on double ON
- conditions:
- condition: template
value_template: "{{ scroll_mode_helper is not none }}"
sequence:
# Cycle through only configured modes
- 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 }}"
# Update last activity timestamp
- 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
# Fallback: no helper → preserve original default behavior
- 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
# OFF DOUBLE
- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment