Last active
April 17, 2026 22:04
-
-
Save jamesj2/65cf4760cbb3a7bd3af1b7906f8a3fe2 to your computer and use it in GitHub Desktop.
Patched IKEA BILRESA Matter scroll wheel blueprint with idle-event guard
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: IKEA BILRESA - Light Controller (Local Patched) | |
| description: > | |
| Local, patched copy of the SvKRO IKEA BILRESA Matter scroll wheel blueprint. | |
| It preserves the original multi-channel light controller behavior while | |
| filtering out non-user state changes from the BILRESA event entities. | |
| domain: automation | |
| source_url: https://gist.github.com/SvKRO/52f246362a19f0297fd18969b383181b | |
| input: | |
| remote_device: | |
| name: Remote Control | |
| description: The IKEA BILRESA scroll wheel (Matter) used as the trigger. | |
| selector: | |
| device: | |
| filter: | |
| - manufacturer: IKEA of Sweden | |
| model: BILRESA scroll wheel | |
| header_1: | |
| name: Channel 1 | |
| default: "" | |
| selector: | |
| constant: | |
| value: "" | |
| group_scroll_1: | |
| name: Scroll Wheel Settings | |
| icon: mdi:rotate-right | |
| collapsed: true | |
| input: | |
| input_light_1: | |
| name: Light / Light Group | |
| description: Select the light or group to be controlled. | |
| default: {} | |
| selector: | |
| target: | |
| entity: | |
| domain: light | |
| input_mode_1: | |
| name: Scroll Function | |
| description: Define which value is changed when turning the wheel. | |
| default: brightness | |
| selector: | |
| select: | |
| mode: dropdown | |
| options: | |
| - label: Brightness | |
| value: brightness | |
| - label: Color Temperature | |
| value: temp_std | |
| - label: Color Temperature (Philips Hue) | |
| value: temp_hue | |
| - label: Color (Hue) | |
| value: hue_std | |
| - label: Color (Philips Hue) | |
| value: hue_hue | |
| input_sens_1: | |
| name: Step Size | |
| description: Determine how much the value changes per scroll step. | |
| default: 5 | |
| selector: | |
| number: | |
| min: 1 | |
| max: 20 | |
| unit_of_measurement: "%" | |
| mode: slider | |
| input_transition_1: | |
| name: Scroll Transition | |
| description: Transition duration in seconds for scroll-based changes. | |
| default: 0 | |
| selector: | |
| number: | |
| min: 0 | |
| max: 120 | |
| step: 1 | |
| unit_of_measurement: s | |
| mode: slider | |
| group_single_click_1: | |
| name: Single Click | |
| icon: mdi:gesture-tap | |
| collapsed: true | |
| input: | |
| input_click_mode_1: | |
| name: Click Mode | |
| description: Choose between a single action or cycling through scenes. | |
| default: action | |
| selector: | |
| select: | |
| mode: dropdown | |
| options: | |
| - label: Perform single action | |
| value: action | |
| - label: Scene cycler | |
| value: scene_cycle | |
| input_scenes_1: | |
| name: Scene Selection | |
| description: Select the scenes to cycle through sequentially. | |
| default: [] | |
| selector: | |
| entity: | |
| domain: scene | |
| multiple: true | |
| input_click_1: | |
| name: Action on Click | |
| description: Action to perform when in Single Action mode. | |
| default: [] | |
| selector: | |
| action: {} | |
| group_multi_click_1: | |
| name: Additional Button Functions | |
| icon: mdi:plus-circle-outline | |
| collapsed: true | |
| input: | |
| input_double_1: | |
| name: Double Click | |
| description: Action to perform on double press. | |
| default: [] | |
| selector: | |
| action: {} | |
| input_triple_1: | |
| name: Triple Click | |
| description: Action to perform on triple press. | |
| default: [] | |
| selector: | |
| action: {} | |
| input_long_1: | |
| name: Hold | |
| description: Action to perform when holding the button. | |
| default: [] | |
| selector: | |
| action: {} | |
| input_release_1: | |
| name: Release | |
| description: Action to perform when releasing the button after a long press. | |
| default: [] | |
| selector: | |
| action: {} | |
| header_2: | |
| name: Channel 2 | |
| default: "" | |
| selector: | |
| constant: | |
| value: "" | |
| group_scroll_2: | |
| name: Scroll Wheel Settings | |
| icon: mdi:rotate-right | |
| collapsed: true | |
| input: | |
| input_light_2: | |
| name: Light / Light Group | |
| description: Select the light or group to be controlled. | |
| default: {} | |
| selector: | |
| target: | |
| entity: | |
| domain: light | |
| input_mode_2: | |
| name: Scroll Function | |
| description: Define which value is changed when turning the wheel. | |
| default: brightness | |
| selector: | |
| select: | |
| mode: dropdown | |
| options: | |
| - label: Brightness | |
| value: brightness | |
| - label: Color Temperature | |
| value: temp_std | |
| - label: Color Temperature (Philips Hue) | |
| value: temp_hue | |
| - label: Color (Hue) | |
| value: hue_std | |
| - label: Color (Philips Hue) | |
| value: hue_hue | |
| input_sens_2: | |
| name: Step Size | |
| description: Determine how much the value changes per scroll step. | |
| default: 5 | |
| selector: | |
| number: | |
| min: 1 | |
| max: 20 | |
| unit_of_measurement: "%" | |
| mode: slider | |
| input_transition_2: | |
| name: Scroll Transition | |
| description: Transition duration in seconds for scroll-based changes. | |
| default: 0 | |
| selector: | |
| number: | |
| min: 0 | |
| max: 120 | |
| step: 1 | |
| unit_of_measurement: s | |
| mode: slider | |
| group_single_click_2: | |
| name: Single Click | |
| icon: mdi:gesture-tap | |
| collapsed: true | |
| input: | |
| input_click_mode_2: | |
| name: Click Mode | |
| description: Choose between a single action or cycling through scenes. | |
| default: action | |
| selector: | |
| select: | |
| mode: dropdown | |
| options: | |
| - label: Perform single action | |
| value: action | |
| - label: Scene cycler | |
| value: scene_cycle | |
| input_scenes_2: | |
| name: Scene Selection | |
| description: Select the scenes to cycle through sequentially. | |
| default: [] | |
| selector: | |
| entity: | |
| domain: scene | |
| multiple: true | |
| input_click_2: | |
| name: Action on Click | |
| description: Action to perform when in Single Action mode. | |
| default: [] | |
| selector: | |
| action: {} | |
| group_multi_click_2: | |
| name: Additional Button Functions | |
| icon: mdi:plus-circle-outline | |
| collapsed: true | |
| input: | |
| input_double_2: | |
| name: Double Click | |
| description: Action to perform on double press. | |
| default: [] | |
| selector: | |
| action: {} | |
| input_triple_2: | |
| name: Triple Click | |
| description: Action to perform on triple press. | |
| default: [] | |
| selector: | |
| action: {} | |
| input_long_2: | |
| name: Hold | |
| description: Action to perform when holding the button. | |
| default: [] | |
| selector: | |
| action: {} | |
| input_release_2: | |
| name: Release | |
| description: Action to perform when releasing the button after a long press. | |
| default: [] | |
| selector: | |
| action: {} | |
| header_3: | |
| name: Channel 3 | |
| default: "" | |
| selector: | |
| constant: | |
| value: "" | |
| group_scroll_3: | |
| name: Scroll Wheel Settings | |
| icon: mdi:rotate-right | |
| collapsed: true | |
| input: | |
| input_light_3: | |
| name: Light / Light Group | |
| description: Select the light or group to be controlled. | |
| default: {} | |
| selector: | |
| target: | |
| entity: | |
| domain: light | |
| input_mode_3: | |
| name: Scroll Function | |
| description: Define which value is changed when turning the wheel. | |
| default: brightness | |
| selector: | |
| select: | |
| mode: dropdown | |
| options: | |
| - label: Brightness | |
| value: brightness | |
| - label: Color Temperature | |
| value: temp_std | |
| - label: Color Temperature (Philips Hue) | |
| value: temp_hue | |
| - label: Color (Hue) | |
| value: hue_std | |
| - label: Color (Philips Hue) | |
| value: hue_hue | |
| input_sens_3: | |
| name: Step Size | |
| description: Determine how much the value changes per scroll step. | |
| default: 5 | |
| selector: | |
| number: | |
| min: 1 | |
| max: 20 | |
| unit_of_measurement: "%" | |
| mode: slider | |
| input_transition_3: | |
| name: Scroll Transition | |
| description: Transition duration in seconds for scroll-based changes. | |
| default: 0 | |
| selector: | |
| number: | |
| min: 0 | |
| max: 120 | |
| step: 1 | |
| unit_of_measurement: s | |
| mode: slider | |
| group_single_click_3: | |
| name: Single Click | |
| icon: mdi:gesture-tap | |
| collapsed: true | |
| input: | |
| input_click_mode_3: | |
| name: Click Mode | |
| description: Choose between a single action or cycling through scenes. | |
| default: action | |
| selector: | |
| select: | |
| mode: dropdown | |
| options: | |
| - label: Perform single action | |
| value: action | |
| - label: Scene cycler | |
| value: scene_cycle | |
| input_scenes_3: | |
| name: Scene Selection | |
| description: Select the scenes to cycle through sequentially. | |
| default: [] | |
| selector: | |
| entity: | |
| domain: scene | |
| multiple: true | |
| input_click_3: | |
| name: Action on Click | |
| description: Action to perform when in Single Action mode. | |
| default: [] | |
| selector: | |
| action: {} | |
| group_multi_click_3: | |
| name: Additional Button Functions | |
| icon: mdi:plus-circle-outline | |
| collapsed: true | |
| input: | |
| input_double_3: | |
| name: Double Click | |
| description: Action to perform on double press. | |
| default: [] | |
| selector: | |
| action: {} | |
| input_triple_3: | |
| name: Triple Click | |
| description: Action to perform on triple press. | |
| default: [] | |
| selector: | |
| action: {} | |
| input_long_3: | |
| name: Hold | |
| description: Action to perform when holding the button. | |
| default: [] | |
| selector: | |
| action: {} | |
| input_release_3: | |
| name: Release | |
| description: Action to perform when releasing the button after a long press. | |
| default: [] | |
| selector: | |
| action: {} | |
| mode: single | |
| max_exceeded: silent | |
| trigger_variables: | |
| var_remote: !input remote_device | |
| var_events: > | |
| {{ device_entities(var_remote) | select('match', 'event\.') | sort | list }} | |
| temp_kanal1_rechts: "{{ var_events[0] if var_events | length > 0 else 'none' }}" | |
| temp_kanal1_links: "{{ var_events[1] if var_events | length > 1 else 'none' }}" | |
| temp_kanal1_klick: "{{ var_events[2] if var_events | length > 2 else 'none' }}" | |
| temp_kanal2_rechts: "{{ var_events[3] if var_events | length > 3 else 'none' }}" | |
| temp_kanal2_links: "{{ var_events[4] if var_events | length > 4 else 'none' }}" | |
| temp_kanal2_klick: "{{ var_events[5] if var_events | length > 5 else 'none' }}" | |
| temp_kanal3_rechts: "{{ var_events[6] if var_events | length > 6 else 'none' }}" | |
| temp_kanal3_links: "{{ var_events[7] if var_events | length > 7 else 'none' }}" | |
| temp_kanal3_klick: "{{ var_events[8] if var_events | length > 8 else 'none' }}" | |
| trigger: | |
| - platform: event | |
| event_type: state_changed | |
| event_data: | |
| entity_id: "{{ temp_kanal1_links }}" | |
| id: trigger_kanal1_links | |
| - platform: event | |
| event_type: state_changed | |
| event_data: | |
| entity_id: "{{ temp_kanal1_rechts }}" | |
| id: trigger_kanal1_rechts | |
| - platform: event | |
| event_type: state_changed | |
| event_data: | |
| entity_id: "{{ temp_kanal1_klick }}" | |
| id: trigger_kanal1_klick | |
| - platform: event | |
| event_type: state_changed | |
| event_data: | |
| entity_id: "{{ temp_kanal2_links }}" | |
| id: trigger_kanal2_links | |
| - platform: event | |
| event_type: state_changed | |
| event_data: | |
| entity_id: "{{ temp_kanal2_rechts }}" | |
| id: trigger_kanal2_rechts | |
| - platform: event | |
| event_type: state_changed | |
| event_data: | |
| entity_id: "{{ temp_kanal2_klick }}" | |
| id: trigger_kanal2_klick | |
| - platform: event | |
| event_type: state_changed | |
| event_data: | |
| entity_id: "{{ temp_kanal3_links }}" | |
| id: trigger_kanal3_links | |
| - platform: event | |
| event_type: state_changed | |
| event_data: | |
| entity_id: "{{ temp_kanal3_rechts }}" | |
| id: trigger_kanal3_rechts | |
| - platform: event | |
| event_type: state_changed | |
| event_data: | |
| entity_id: "{{ temp_kanal3_klick }}" | |
| id: trigger_kanal3_klick | |
| action: | |
| - variables: | |
| m_1: !input input_mode_1 | |
| m_2: !input input_mode_2 | |
| m_3: !input input_mode_3 | |
| s_1: !input input_sens_1 | |
| s_2: !input input_sens_2 | |
| s_3: !input input_sens_3 | |
| t_1: !input input_transition_1 | |
| t_2: !input input_transition_2 | |
| t_3: !input input_transition_3 | |
| l_1: !input input_light_1 | |
| l_2: !input input_light_2 | |
| l_3: !input input_light_3 | |
| action_kanal_id: "{{ trigger.id.split('_')[1] }}" | |
| old_state_obj: "{{ trigger.event.data.old_state if trigger.event.data.old_state is defined else none }}" | |
| new_state_obj: "{{ trigger.event.data.new_state if trigger.event.data.new_state is defined else none }}" | |
| old_state_val: "{{ old_state_obj.state if old_state_obj is not none else none }}" | |
| new_state_val: "{{ new_state_obj.state if new_state_obj is not none else none }}" | |
| old_attr: "{{ old_state_obj.attributes if old_state_obj is not none else dict() }}" | |
| new_attr: "{{ new_state_obj.attributes if new_state_obj is not none else dict() }}" | |
| event_attr: "{{ new_attr }}" | |
| ticks: "{{ new_attr.totalNumberOfPressesCounted | default(1) | int }}" | |
| action_event_type: "{{ new_attr.event_type | default('none') }}" | |
| curr_mode: > | |
| {% if action_kanal_id == 'kanal1' %} | |
| {{ m_1 }} | |
| {% elif action_kanal_id == 'kanal2' %} | |
| {{ m_2 }} | |
| {% else %} | |
| {{ m_3 }} | |
| {% endif %} | |
| base_sens: > | |
| {% if action_kanal_id == 'kanal1' %} | |
| {{ s_1 }} | |
| {% elif action_kanal_id == 'kanal2' %} | |
| {{ s_2 }} | |
| {% else %} | |
| {{ s_3 }} | |
| {% endif %} | |
| curr_transition_raw: > | |
| {% if action_kanal_id == 'kanal1' %} | |
| {{ t_1 }} | |
| {% elif action_kanal_id == 'kanal2' %} | |
| {{ t_2 }} | |
| {% else %} | |
| {{ t_3 }} | |
| {% endif %} | |
| curr_transition: "{{ curr_transition_raw | int(0) }}" | |
| step_val: > | |
| {% set val = base_sens * ticks %} | |
| {{ val if trigger.id.endswith('_rechts') else val * -1 }} | |
| curr_target_raw: > | |
| {% if action_kanal_id == 'kanal1' %} | |
| {{ l_1 }} | |
| {% elif action_kanal_id == 'kanal2' %} | |
| {{ l_2 }} | |
| {% else %} | |
| {{ l_3 }} | |
| {% endif %} | |
| curr_target: "{{ curr_target_raw.entity_id if curr_target_raw.entity_id is defined else curr_target_raw }}" | |
| state_payload_valid: > | |
| {{ | |
| old_state_obj is not none | |
| and new_state_obj is not none | |
| and old_state_val not in [none, 'unknown', 'unavailable'] | |
| and new_state_val not in [none, 'unknown', 'unavailable'] | |
| }} | |
| event_change_detected: > | |
| {{ | |
| old_state_val != new_state_val | |
| or (old_attr.totalNumberOfPressesCounted | default(-1) | int) | |
| != (new_attr.totalNumberOfPressesCounted | default(-1) | int) | |
| or (old_attr.event_type | default('')) | |
| != (new_attr.event_type | default('')) | |
| }} | |
| scroll_payload_present: > | |
| {{ | |
| new_attr.totalNumberOfPressesCounted is defined | |
| or (new_attr.event_type | default('')) not in ['', 'none', 'unknown', 'unavailable'] | |
| }} | |
| button_payload_present: > | |
| {{ | |
| action_event_type in [ | |
| 'multi_press_1', | |
| 'multi_press_2', | |
| 'multi_press_3', | |
| 'long_press', | |
| 'long_release' | |
| ] | |
| }} | |
| - choose: | |
| - conditions: > | |
| {{ | |
| (trigger.id.endswith('_rechts') or trigger.id.endswith('_links')) | |
| and state_payload_valid | |
| and event_change_detected | |
| and scroll_payload_present | |
| }} | |
| sequence: | |
| - choose: | |
| - conditions: "{{ curr_mode == 'brightness' }}" | |
| sequence: | |
| - service: light.turn_on | |
| target: | |
| entity_id: "{{ curr_target }}" | |
| data: | |
| brightness_step_pct: "{{ step_val }}" | |
| transition: "{{ curr_transition }}" | |
| - conditions: "{{ curr_mode == 'temp_std' }}" | |
| sequence: | |
| - service: light.turn_on | |
| target: | |
| entity_id: "{{ curr_target }}" | |
| data: | |
| color_temp_step_pct: "{{ step_val }}" | |
| transition: "{{ curr_transition }}" | |
| - conditions: "{{ curr_mode == 'temp_hue' }}" | |
| sequence: | |
| - service: light.turn_on | |
| target: | |
| entity_id: "{{ curr_target }}" | |
| data: | |
| transition: "{{ curr_transition }}" | |
| color_temp: > | |
| {% set curr = state_attr(curr_target, 'color_temp') | default(300) | int %} | |
| {% set min = state_attr(curr_target, 'min_mireds') | default(153) | int %} | |
| {% set max = state_attr(curr_target, 'max_mireds') | default(500) | int %} | |
| {% set next = curr - (step_val * 5) %} | |
| {{ ([min, next, max] | sort)[1] | int }} | |
| - conditions: "{{ curr_mode == 'hue_std' }}" | |
| sequence: | |
| - service: light.turn_on | |
| target: | |
| entity_id: "{{ curr_target }}" | |
| data: | |
| hue_step: "{{ step_val }}" | |
| transition: "{{ curr_transition }}" | |
| - conditions: "{{ curr_mode == 'hue_hue' }}" | |
| sequence: | |
| - service: light.turn_on | |
| target: | |
| entity_id: "{{ curr_target }}" | |
| data: | |
| transition: "{{ curr_transition }}" | |
| hs_color: | |
| - > | |
| {% set curr = state_attr(curr_target, 'hs_color')[0] | default(0) | float %} | |
| {{ (curr + step_val) % 360 }} | |
| - "{{ state_attr(curr_target, 'hs_color')[1] | default(100) }}" | |
| - conditions: > | |
| {{ | |
| trigger.id.endswith('_klick') | |
| and state_payload_valid | |
| and event_change_detected | |
| and button_payload_present | |
| }} | |
| sequence: | |
| - variables: | |
| cm_1: !input input_click_mode_1 | |
| cm_2: !input input_click_mode_2 | |
| cm_3: !input input_click_mode_3 | |
| cs_1: !input input_scenes_1 | |
| cs_2: !input input_scenes_2 | |
| cs_3: !input input_scenes_3 | |
| c_mode: > | |
| {% if action_kanal_id == 'kanal1' %} | |
| {{ cm_1 }} | |
| {% elif action_kanal_id == 'kanal2' %} | |
| {{ cm_2 }} | |
| {% else %} | |
| {{ cm_3 }} | |
| {% endif %} | |
| c_scenes: > | |
| {% if action_kanal_id == 'kanal1' %} | |
| {{ cs_1 }} | |
| {% elif action_kanal_id == 'kanal2' %} | |
| {{ cs_2 }} | |
| {% else %} | |
| {{ cs_3 }} | |
| {% endif %} | |
| - choose: | |
| - conditions: "{{ action_event_type == 'multi_press_1' }}" | |
| sequence: | |
| - choose: | |
| - conditions: "{{ c_mode == 'scene_cycle' and c_scenes | count > 0 }}" | |
| sequence: | |
| - service: scene.turn_on | |
| target: | |
| entity_id: > | |
| {% set count = c_scenes | count %} | |
| {% set idx = (as_timestamp(now()) | int // 2) % count %} | |
| {{ c_scenes[idx] }} | |
| - conditions: "{{ c_mode == 'action' }}" | |
| sequence: | |
| - choose: | |
| - conditions: "{{ action_kanal_id == 'kanal1' }}" | |
| sequence: !input input_click_1 | |
| - conditions: "{{ action_kanal_id == 'kanal2' }}" | |
| sequence: !input input_click_2 | |
| - conditions: "{{ action_kanal_id == 'kanal3' }}" | |
| sequence: !input input_click_3 | |
| - conditions: "{{ action_event_type == 'multi_press_2' }}" | |
| sequence: | |
| - choose: | |
| - conditions: "{{ action_kanal_id == 'kanal1' }}" | |
| sequence: !input input_double_1 | |
| - conditions: "{{ action_kanal_id == 'kanal2' }}" | |
| sequence: !input input_double_2 | |
| - conditions: "{{ action_kanal_id == 'kanal3' }}" | |
| sequence: !input input_double_3 | |
| - conditions: "{{ action_event_type == 'multi_press_3' }}" | |
| sequence: | |
| - choose: | |
| - conditions: "{{ action_kanal_id == 'kanal1' }}" | |
| sequence: !input input_triple_1 | |
| - conditions: "{{ action_kanal_id == 'kanal2' }}" | |
| sequence: !input input_triple_2 | |
| - conditions: "{{ action_kanal_id == 'kanal3' }}" | |
| sequence: !input input_triple_3 | |
| - conditions: "{{ action_event_type == 'long_press' }}" | |
| sequence: | |
| - choose: | |
| - conditions: "{{ action_kanal_id == 'kanal1' }}" | |
| sequence: !input input_long_1 | |
| - conditions: "{{ action_kanal_id == 'kanal2' }}" | |
| sequence: !input input_long_2 | |
| - conditions: "{{ action_kanal_id == 'kanal3' }}" | |
| sequence: !input input_long_3 | |
| - conditions: "{{ action_event_type == 'long_release' }}" | |
| sequence: | |
| - choose: | |
| - conditions: "{{ action_kanal_id == 'kanal1' }}" | |
| sequence: !input input_release_1 | |
| - conditions: "{{ action_kanal_id == 'kanal2' }}" | |
| sequence: !input input_release_2 | |
| - conditions: "{{ action_kanal_id == 'kanal3' }}" | |
| sequence: !input input_release_3 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment