Skip to content

Instantly share code, notes, and snippets.

@jhol-byte
Last active January 13, 2026 16:38
Show Gist options
  • Select an option

  • Save jhol-byte/b2731a4d2476f530d76b9ff409f7f3a4 to your computer and use it in GitHub Desktop.

Select an option

Save jhol-byte/b2731a4d2476f530d76b9ff409f7f3a4 to your computer and use it in GitHub Desktop.
Home Assistent Blueprint "Ikea_bilresa_scroll_wheel"
blueprint:
name: Ikea_bilresa_scroll_wheel
description: |
Blueprint for Ikea Bilresa (button and scroll wheel for 3 different channels)
Version: 2026-01-10
domain: automation
# input selectors
input:
remote:
name: Remote
description: "Ikea Bilresa scroll wheel remote control"
selector:
device:
filter:
manufacturer: IKEA of Sweden
model: BILRESA scroll wheel
# input selectors: channel 1
channel1_section:
name: Channel 1
collapsed: true
input:
click_action_ch1:
name: "Channel 1: <click> action"
default: []
selector:
action: {}
double_click_action_ch1:
name: "Channel 1: <double-click> action"
default: []
selector:
action: {}
long_click_action_ch1:
name: "Channel 1: <long-click> action"
description: ""
default: []
selector:
action: {}
on_hold_action_ch1:
name: "Channel 1: <on-hold> action"
description: "repeat action until button is released, see also on_hold_delay"
default: []
selector:
action: {}
scroll_wheel_target_ch1:
name: "Channel 1: Scroll wheel target entity"
description: |
Light or media player to be controlled by scroll wheel for channel 1
- empty for <user defined> scroll wheel mode
selector:
entity:
multiple: true
filter:
domain:
- light
- media_player
default: []
scroll_wheel_mode_ch1:
selector:
select:
options:
- "lights: dim"
- "lights: color temp and color hue"
- "lights: color temp only"
- "lights: color hue only"
- "media: volume control"
- "user defined"
default: "lights: dim"
name: "Channel 1: Scroll wheel mode"
description: "Select attribute of target entity controlled by scroll wheel"
scroll_wheel_mode_ext_ch1:
selector:
select:
options:
- "relaxed"
- "instant"
default: "relaxed"
name: "Channel 1: Scroll wheel evaluation mode"
description: |
<relaxed>: one action call after scrolling
<instant>: many action calls while scrolling
Beware:
for <instant> mode the 9 hidden sensor entities of Bilresa device have to be enabled
scroll_wheel_user_action_ch1:
name: "Channel 1: User defined scroll wheel action"
description: |
For scroll wheel mode <user defined> only
Exposed variables:
- <scroll_clicks>: for <instant> mode always 1, for <relaxed> mode number between 1 to 8
- <scroll_direction>: 'left' or 'right'
default: []
selector:
action: {}
# input selectors: channel 1
channel2_section:
name: Channel 2
collapsed: true
input:
click_action_ch2:
name: "Channel 2: <click> action"
default: []
selector:
action: {}
double_click_action_ch2:
name: "Channel 2: <double-click> action"
default: []
selector:
action: {}
long_click_action_ch2:
name: "Channel 2: <long-click> action"
description: ""
default: []
selector:
action: {}
on_hold_action_ch2:
name: "Channel 2: <on-hold> action"
description: "repeat action until button is released, see also on_hold_delay"
default: []
selector:
action: {}
scroll_wheel_target_ch2:
name: "Channel 2: Scroll wheel target entity"
description: |
Light or media player to be controlled by scroll wheel for channel 2
- empty for <user defined> scroll wheel mode
selector:
entity:
multiple: true
filter:
domain:
- light
- media_player
default: []
scroll_wheel_mode_ch2:
selector:
select:
options:
- "lights: dim"
- "lights: color temp and color hue"
- "lights: color temp only"
- "lights: color hue only"
- "media: volume control"
- "user defined"
default: "lights: dim"
name: "Channel 2: Scroll wheel mode"
description: "Select attribute of target entity controlled by scroll wheel"
scroll_wheel_mode_ext_ch2:
selector:
select:
options:
- "relaxed"
- "instant"
default: "relaxed"
name: "Channel 2: Scroll wheel evaluation mode"
description: |
- <relaxed>: one action call after scrolling
- <instant>: many action calls while scrolling
Beware:
- for <instant> mode the 9 hidden sensor entities of Bilresa device have to be enabled
scroll_wheel_user_action_ch2:
name: "Channel 2: User defined scroll wheel action"
description: |
For scroll wheel mode <user defined> only
Exposed variables:
- <scroll_clicks>: for <instant> mode always 1, for <relaxed> mode number between 1 to 8
- <scroll_direction>: 'left' or 'right'
default: []
selector:
action: {}
# input selectors: channel 3
channel3_section:
name: Channel 3
collapsed: true
input:
click_action_ch3:
name: "Channel 3: <click> action"
default: []
selector:
action: {}
double_click_action_ch3:
name: "Channel 3: <double-click> action"
default: []
selector:
action: {}
long_click_action_ch3:
name: "Channel 3: <long-click> action"
description: ""
default: []
selector:
action: {}
on_hold_action_ch3:
name: "Channel 3: <on-hold> action"
description: "repeat action until button is released, see also on_hold_delay"
default: []
selector:
action: {}
scroll_wheel_target_ch3:
name: "Channel 3: Scroll wheel target entity"
description: |
Light or media player to be controlled by scroll wheel for channel 3
- empty for <user defined> scroll wheel mode
selector:
entity:
multiple: true
filter:
domain:
- light
- media_player
default: []
scroll_wheel_mode_ch3:
selector:
select:
options:
- "lights: dim"
- "lights: color temp and color hue"
- "lights: color temp only"
- "lights: color hue only"
- "media: volume control"
- "user defined"
default: "lights: dim"
name: "Channel 3: Scroll wheel mode"
description: "Select attribute of target entity controlled by scroll wheel"
scroll_wheel_mode_ext_ch3:
selector:
select:
options:
- "relaxed"
- "instant"
default: "relaxed"
name: "Channel 3: Scroll wheel evaluation mode"
description: |
- <relaxed>: one action call after scrolling
- <instant>: many action calls while scrolling
Beware:
- for <instant> mode the 9 hidden sensor entities of Bilresa device have to be enabled
scroll_wheel_user_action_ch3:
name: "Channel 3: User defined scroll wheel action"
description: |
For scroll wheel mode <user defined> only
Exposed variables:
- <scroll_clicks>: for <instant> mode always 1, for <relaxed> mode number between 1 to 8
- <scroll_direction>: 'left' or 'right'
default: []
selector:
action: {}
# input selectors: settings for all channels
global_setting:
name: Global settings
collapsed: true
input:
# light
dim_step_pct:
name: Dimmer step percentage
description: "change in brightness in %"
default: 5
selector:
number:
mode: box
dim_min_pct:
name: Minimum brightness
description: "minimum brightness in %"
default: 5
selector:
number:
mode: box
min_color_temp:
name: Minimum color temperature
description: "minimum color temperature in K (for color_mode = color_temp)"
default: 2200
selector:
number:
mode: box
max_color_temp:
name: Maximum color temperature
description: "maximum color temperature in K (for color_mode = color_temp)"
default: 4000
selector:
number:
mode: box
color_temp_step:
name: Color temperature step
description: "change in color temperature in K (for color_mode = color_temp)"
default: 100
selector:
number:
mode: box
color_hue_step:
name: Color hue step
description: "change in color hue in degree (0..360) (for color_mode = color_hue)"
default: 4
selector:
number:
mode: box
color_saturation:
name: Color saturation
description: "color saturation in % (for color_mode = color_hue)"
default: 100
selector:
number:
mode: box
# media_player
volume_step_pct:
name: Media player volume step percentage
description: "change in volume in %"
default: 3.5
selector:
number:
mode: box
volume_use_up_down_for_instant:
name: "Media player: use actions volume_up/volume_down for <instant> mode"
description: |
<instant> mode uses standard actions volume_up/volume_down instead of <volume step percentage> by default
if this is to coarse disable this option and adjust the <volume step percentage> value to the lowest value which still changes the volume while scrolling in both directions
default: true
selector:
boolean:
volume_max_pct:
name: Media player max volume percentage
description: "max volume in %"
default: 70
selector:
number:
mode: box
# misc
on_hold_delay:
name: On hold delay
description: delay between separate action calls while on hold button
default: 0.1
selector:
number:
mode: box
step: 0.1
# automation logic
alias: Ikea_bilresa_scroll_wheel
trigger:
- event_type: state_changed
event_data:
entity_id: "{{ var_button_ch1 }}"
trigger: event
id: button_ch1
- event_type: state_changed
event_data:
entity_id: "{{ var_scroll_left_ch1 }}"
trigger: event
id: scroll_left_ch1
- event_type: state_changed
event_data:
entity_id: "{{ var_scroll_right_ch1 }}"
trigger: event
id: scroll_right_ch1
- event_type: state_changed
event_data:
entity_id: "{{ var_scroll_left_ext_ch1 }}"
trigger: event
id: scroll_left_ext_ch1
- event_type: state_changed
event_data:
entity_id: "{{ var_scroll_right_ext_ch1 }}"
trigger: event
id: scroll_right_ext_ch1
- event_type: state_changed
event_data:
entity_id: "{{ var_button_ch2 }}"
trigger: event
id: button_ch2
- event_type: state_changed
event_data:
entity_id: "{{ var_scroll_left_ch2 }}"
trigger: event
id: scroll_left_ch2
- event_type: state_changed
event_data:
entity_id: "{{ var_scroll_right_ch2 }}"
trigger: event
id: scroll_right_ch2
- event_type: state_changed
event_data:
entity_id: "{{ var_scroll_left_ext_ch2 }}"
trigger: event
id: scroll_left_ext_ch2
- event_type: state_changed
event_data:
entity_id: "{{ var_scroll_right_ext_ch2 }}"
trigger: event
id: scroll_right_ext_ch2
- event_type: state_changed
event_data:
entity_id: "{{ var_button_ch3 }}"
trigger: event
id: button_ch3
- event_type: state_changed
event_data:
entity_id: "{{ var_scroll_left_ch3 }}"
trigger: event
id: scroll_left_ch3
- event_type: state_changed
event_data:
entity_id: "{{ var_scroll_right_ch3 }}"
trigger: event
id: scroll_right_ch3
- event_type: state_changed
event_data:
entity_id: "{{ var_scroll_left_ext_ch3 }}"
trigger: event
id: scroll_left_ext_ch3
- event_type: state_changed
event_data:
entity_id: "{{ var_scroll_right_ext_ch3 }}"
trigger: event
id: scroll_right_ext_ch3
condition:
- condition: template
value_template: >-
{{ trigger.event.data.old_state.state != 'unavailable'
and trigger.event.data.new_state.state != 'unavailable'
}}
action:
- variables:
channel: >-
{% if trigger.id in ['button_ch1', 'scroll_left_ch1', 'scroll_right_ch1', 'scroll_left_ext_ch1', 'scroll_right_ext_ch1'] %}
ch1
{% elif trigger.id in ['button_ch2', 'scroll_left_ch2', 'scroll_right_ch2', 'scroll_left_ext_ch2', 'scroll_right_ext_ch2'] %}
ch2
{% elif trigger.id in ['button_ch3', 'scroll_left_ch3', 'scroll_right_ch3', 'scroll_left_ext_ch3', 'scroll_right_ext_ch3'] %}
ch3
{% endif %}
- choose:
# button click
- conditions:
- condition: template
value_template: >
{{ trigger.id == ('button_' ~ channel) and trigger.event.data.new_state.attributes.event_type ==
'multi_press_1' }}
sequence:
- if: "{{ channel == 'ch1'}}"
then:
!input click_action_ch1
- if: "{{ channel == 'ch2'}}"
then:
!input click_action_ch2
- if: "{{ channel == 'ch3'}}"
then:
!input click_action_ch3
# button double-click
- conditions:
- condition: template
value_template: >
{{ trigger.id == ('button_' ~ channel) and trigger.event.data.new_state.attributes.event_type ==
'multi_press_2' }}
sequence:
- if: "{{ channel == 'ch1'}}"
then:
!input double_click_action_ch1
- if: "{{ channel == 'ch2'}}"
then:
!input double_click_action_ch2
- if: "{{ channel == 'ch3'}}"
then:
!input double_click_action_ch3
# button long-press
- conditions:
- condition: template
value_template: >
{{ trigger.id == ('button_' ~ channel) and trigger.event.data.new_state.attributes.event_type ==
'long_press' }}
# button on-hold action
sequence:
- if: "{{ channel == 'ch1'}}"
then:
sequence:
- sequence: !input long_click_action_ch1
- repeat:
while: "{{ repeat.index < 100 and state_attr(var_button_ch1, 'event_type') == 'long_press' }}"
sequence:
- sequence: !input on_hold_action_ch1
- delay: "{{ var_on_hold_delay }}"
- if: "{{ channel == 'ch2'}}"
then:
sequence:
- sequence: !input long_click_action_ch2
- repeat:
while: "{{ repeat.index < 100 and state_attr(var_button_ch2, 'event_type') == 'long_press' }}"
sequence:
- sequence: !input on_hold_action_ch2
- delay: "{{ var_on_hold_delay }}"
- if: "{{ channel == 'ch3'}}"
then:
sequence:
- sequence: !input long_click_action_ch3
- repeat:
while: "{{ repeat.index < 100 and state_attr(var_button_ch3, 'event_type') == 'long_press' }}"
sequence:
- sequence: !input on_hold_action_ch3
- delay: "{{ var_on_hold_delay }}"
# scroll wheel actions
- conditions:
- condition: template
value_template: >
{{ ( (vars['scroll_wheel_mode_ext_' ~ channel] == 'relaxed') and (trigger.id == ('scroll_left_' ~ channel) or trigger.id == ('scroll_right_' ~ channel))) or
( (vars['scroll_wheel_mode_ext_' ~ channel] == 'instant') and (trigger.id == ('scroll_left_ext_' ~ channel) or trigger.id == ('scroll_right_ext_' ~ channel)) and trigger.event.data.new_state.state == '1' )
}}
sequence:
- variables:
scroll_clicks: >
{% if (vars['scroll_wheel_mode_ext_' ~ channel] == 'relaxed') %}
{{ trigger.event.data.new_state.attributes.totalNumberOfPressesCounted }}
{% else %}
1
{% endif %}
scroll_direction: >
{% if (trigger.id == 'scroll_left_' ~ channel) or (trigger.id == 'scroll_left_ext_' ~ channel)%}
left
{% else %}
right
{% endif %}
- if:
# scroll wheel mode: <user defined>
- condition: template
value_template: "{{ vars['scroll_wheel_mode_' ~ channel] == 'user defined' }}"
then:
sequence:
- if: "{{ channel == 'ch1'}}"
then:
!input scroll_wheel_user_action_ch1
- if: "{{ channel == 'ch2'}}"
then:
!input scroll_wheel_user_action_ch2
- if: "{{ channel == 'ch3'}}"
then:
!input scroll_wheel_user_action_ch3
else:
- repeat:
for_each: "{{ vars['targets_entities_' ~ channel] }}"
sequence:
- choose:
- conditions:
- condition: template
value_template: "{{ vars['scroll_wheel_mode_' ~ channel] == 'lights: dim'}}"
sequence:
# scroll wheel mode: <lights: dim>
- if:
- condition: template
value_template: >-
{{ is_state(repeat.item, 'on')
}}
then:
- action: light.turn_on
target:
entity_id: "{{ repeat.item }}"
data:
brightness_step_pct: >-
{{ iif(scroll_direction == 'right', scroll_clicks * var_dim_step_pct,
-1*([scroll_clicks * var_dim_step_pct, state_attr(repeat.item, 'brightness')*100/254
- var_min_pct]|min))
}}
transition: 0.1
# scroll wheel mode: <lights: color temp>
- conditions:
- condition: template
value_template: "{{ vars['scroll_wheel_mode_' ~ channel] == 'lights: color temp only' or (vars['scroll_wheel_mode_' ~ channel] == 'lights: color temp and color hue' and state_attr(repeat.item, 'color_temp_kelvin') is not none ) }}"
sequence:
# scroll wheel mode: lights: color temp
- action: script.light_color_temp_helper
data:
mode: >-
{{ iif(scroll_direction == 'left', 'down', 'up')
}}
min_color_temp: "{{ var_min_color_temp }}"
max_color_temp: "{{ var_max_color_temp }}"
color_temp_step: "{{ var_color_temp_step * scroll_clicks }}"
target_light: "{{ repeat.item }}"
# scroll wheel mode: <lights: color hue>
- conditions:
- condition: template
value_template: "{{ vars['scroll_wheel_mode_' ~ channel] == 'lights: color hue only' or (vars['scroll_wheel_mode_' ~ channel] == 'lights: color temp and color hue' and state_attr(repeat.item, 'color_temp_kelvin') is none ) }}"
sequence:
- action: script.light_color_hs_helper
data:
target_light: "{{ repeat.item }}"
color_hue_step: "{{ var_color_hue_step * scroll_clicks }}"
color_saturation: "{{ var_color_saturation }}"
mode: >-
{{ iif(scroll_direction == 'left', 'down', 'up')
}}
# scroll wheel mode: <media: volume>
- conditions:
- condition: template
value_template: "{{ vars['scroll_wheel_mode_' ~ channel] == 'media: volume control' and is_state(repeat.item, 'playing')}}"
sequence:
- if:
- condition: template
value_template: "{{ vars['scroll_wheel_mode_ext_' ~ channel] == 'relaxed' or not var_volume_use_up_down_for_instant}}"
then:
- action: media_player.volume_set
target:
entity_id: "{{ repeat.item }}"
data:
volume_level: >-
{{ [[state_attr(repeat.item, 'volume_level')*100 +
iif(scroll_direction == 'right', 1, -1)* scroll_clicks * var_volume_step_pct, var_volume_max_pct] | min, 0] | max / 100
}}
else:
- if:
- condition: template
value_template: "{{ scroll_direction == 'left' }}"
then:
- action: media_player.volume_down
target:
entity_id: "{{ repeat.item }}"
else:
- if:
- condition: template
value_template: "{{ state_attr(repeat.item, 'volume_level')*100 < var_volume_max_pct }}"
then:
- action: media_player.volume_up
target:
entity_id: "{{ repeat.item }}"
mode: queued
max_exceeded: silent
variables:
vars: {'targets_entities_ch1': !input scroll_wheel_target_ch1, 'targets_entities_ch2': !input scroll_wheel_target_ch2, 'targets_entities_ch3': !input scroll_wheel_target_ch3,
'scroll_wheel_mode_ch1': !input scroll_wheel_mode_ch1, 'scroll_wheel_mode_ch2': !input scroll_wheel_mode_ch2, 'scroll_wheel_mode_ch3': !input scroll_wheel_mode_ch3,
'scroll_wheel_mode_ext_ch1': !input scroll_wheel_mode_ext_ch1, 'scroll_wheel_mode_ext_ch2': !input scroll_wheel_mode_ext_ch2, 'scroll_wheel_mode_ext_ch3': !input scroll_wheel_mode_ext_ch3
}
var_min_pct: !input dim_min_pct
var_dim_step_pct: !input dim_step_pct
var_color_hue_step: !input color_hue_step
var_color_saturation: !input color_saturation
var_min_color_temp: !input min_color_temp
var_max_color_temp: !input max_color_temp
var_color_temp_step: !input color_temp_step
var_on_hold_delay: !input on_hold_delay
var_volume_step_pct: !input volume_step_pct
var_volume_max_pct: !input volume_max_pct
var_volume_use_up_down_for_instant: !input volume_use_up_down_for_instant
trigger_variables:
var_remote: !input remote
# get all event names from device_id only
var_button_ch1: >-
{% for item in device_entities(var_remote) %}
{% set match = item | regex_findall(find='^event\..+[a-zA-Z]_3.*$') %}
{% if (match | length) > 0 %}
{{ match[0] }}
{% endif %}
{% endfor %}
var_scroll_left_ch1: >-
{% for item in device_entities(var_remote) %}
{% set match = item | regex_findall(find='^event\..+[a-zA-Z]_2.*$') %}
{% if (match | length) > 0 %}
{{ match[0] }}
{% endif %}
{% endfor %}
var_scroll_right_ch1: >-
{% for item in device_entities(var_remote) %}
{% set match = item | regex_findall(find='^event\..+[a-zA-Z]_1.*$') %}
{% if (match | length) > 0 %}
{{ match[0] }}
{% endif %}
{% endfor %}
var_scroll_left_ext_ch1: >-
{% for item in device_entities(var_remote) %}
{% set match = item | regex_findall(find='^sensor\..+[a-zA-Z]_2.*$') %}
{% if (match | length) > 0 %}
{{ match[0] }}
{% endif %}
{% endfor %}
var_scroll_right_ext_ch1: >-
{% for item in device_entities(var_remote) %}
{% set match = item | regex_findall(find='^sensor\..+[a-zA-Z]_1.*$') %}
{% if (match | length) > 0 %}
{{ match[0] }}
{% endif %}
{% endfor %}
var_button_ch2: >-
{% for item in device_entities(var_remote) %}
{% set match = item | regex_findall(find='^event\..+[a-zA-Z]_6.*$') %}
{% if (match | length) > 0 %}
{{ match[0] }}
{% endif %}
{% endfor %}
var_scroll_left_ch2: >-
{% for item in device_entities(var_remote) %}
{% set match = item | regex_findall(find='^event\..+[a-zA-Z]_5.*$') %}
{% if (match | length) > 0 %}
{{ match[0] }}
{% endif %}
{% endfor %}
var_scroll_right_ch2: >-
{% for item in device_entities(var_remote) %}
{% set match = item | regex_findall(find='^event\..+[a-zA-Z]_4.*$') %}
{% if (match | length) > 0 %}
{{ match[0] }}
{% endif %}
{% endfor %}
var_scroll_left_ext_ch2: >-
{% for item in device_entities(var_remote) %}
{% set match = item | regex_findall(find='^sensor\..+[a-zA-Z]_5.*$') %}
{% if (match | length) > 0 %}
{{ match[0] }}
{% endif %}
{% endfor %}
var_scroll_right_ext_ch2: >-
{% for item in device_entities(var_remote) %}
{% set match = item | regex_findall(find='^sensor\..+[a-zA-Z]_4.*$') %}
{% if (match | length) > 0 %}
{{ match[0] }}
{% endif %}
{% endfor %}
var_button_ch3: >-
{% for item in device_entities(var_remote) %}
{% set match = item | regex_findall(find='^event\..+[a-zA-Z]_9.*$') %}
{% if (match | length) > 0 %}
{{ match[0] }}
{% endif %}
{% endfor %}
var_scroll_left_ch3: >-
{% for item in device_entities(var_remote) %}
{% set match = item | regex_findall(find='^event\..+[a-zA-Z]_8.*$') %}
{% if (match | length) > 0 %}
{{ match[0] }}
{% endif %}
{% endfor %}
var_scroll_right_ch3: >-
{% for item in device_entities(var_remote) %}
{% set match = item | regex_findall(find='^event\..+[a-zA-Z]_7.*$') %}
{% if (match | length) > 0 %}
{{ match[0] }}
{% endif %}
{% endfor %}
var_scroll_left_ext_ch3: >-
{% for item in device_entities(var_remote) %}
{% set match = item | regex_findall(find='^sensor\..+[a-zA-Z]_8.*$') %}
{% if (match | length) > 0 %}
{{ match[0] }}
{% endif %}
{% endfor %}
var_scroll_right_ext_ch3: >-
{% for item in device_entities(var_remote) %}
{% set match = item | regex_findall(find='^sensor\..+[a-zA-Z]_7.*$') %}
{% if (match | length) > 0 %}
{{ match[0] }}
{% endif %}
{% endfor %}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment