Use the IKEA BILRESA scroll wheel (Matter/Thread) as a smooth dimmer for any HA light. Slow rotation = fine adjustment. Fast spin = rapid sweep. Direction-locked, no bounce.
The BILRESA is a Matter Switch cluster device (Thread SED, 9 endpoints across 3 modes).
Mode 2 exposes EP4 (CW) and EP5 (CCW) as scroll endpoints with FeatureMap=22
(MS + MSR + MSM), MultiPressMax=18, ActionSwitch NOT set.
Each scroll detent fires a full Matter event sequence:
T+0ms InitialPress (newPosition=1)
T+30ms MultiPressOngoing (currentNumberOfPressesCounted=N)
T+34ms ShortRelease
T+500ms Next detent's InitialPress
Events arrive individually every ~500ms (the Matter MultiPress window timeout). Transport delay is 30-150ms. There is NO multi-second SED batching. DIRIGERA operates at the same 500ms cadence.
Home Assistant's Matter integration silently drops initial_press and
multi_press_ongoing events from scroll wheel devices when
kMomentarySwitchMultiPress is set. The root cause is an elif chain in
event.py that checks kMomentarySwitchMultiPress before kMomentarySwitch,
so initial_press, short_release, and multi_press_ongoing are never
registered for MSM devices.
Upstream status: PR #159045 is open and implements the same approach. Until it merges, apply this patch manually.
# Find event.py inside the HA container
docker exec homeassistant find /usr/src/homeassistant -path "*/matter/event.py" -type f
# Backup original
docker exec homeassistant cp <path>/event.py /config/event.py.original
# Copy patch into container and apply
docker cp event.py.patch homeassistant:/tmp/event.py.patch
docker exec homeassistant bash -c "cd / && patch -p1 < /tmp/event.py.patch"
# Full HA restart required
docker restart homeassistantNote: The patch lives inside the Docker container and is overwritten on HA updates. Re-apply after each update until an upstream PR is merged.
Copy helpers.yaml contents into your configuration.yaml and restart HA.
Copy automations.yaml into your automations file. Edit these entity IDs:
| Placeholder | Replace with |
|---|---|
event.bilresa_scroll_wheel_button_4 |
Your CW scroll button entity |
event.bilresa_scroll_wheel_button_5 |
Your CCW scroll button entity |
light.kajplats_e14_ws_globe_806lm |
Your target light entity |
Required after patching and adding helpers.
Aligns with PR #159045:
replaces the elif chain with independent if checks per feature flag.
Registers initial_press, short_release, multi_press_ongoing, and
multi_press_complete as flat event types (no more multi_press_1..N enumeration).
Adds MULTI_PRESS_COUNT_TO_NAME so multi_press_complete events carry an
event_type_extra field ("single", "double", "triple") for easy button automations.
Removes the artificial min(..., 8) cap on MultiPressMax.
- Triggers:
initial_press(first detent) andmulti_press_ongoing(subsequent detents) for both CW and CCW endpoints, usingattribute: event_typefilters so only relevant events fire the automation. - Step calculation: Quadratic acceleration via
currentNumberOfPressesCounted:step = clamp(count² ÷ 6, 2, 50). Slow click → step=2. Fast spin (count=18) → step=50. - Direction lock: Prevents bounce when CW/CCW events fire within 400ms of each other (common during direction transitions). Unlocks after 400ms idle.
- Own-state brightness: Reads
input_number.bilresa_brightnessinstead of the light entity, avoiding stale Matter state during rapid scrolling. - Transition: 300ms smooth dimming on every step.
- Mode:
restart— each new event cancels the previous run.
Syncs input_number.bilresa_brightness when the light changes from other sources.
Blocked for 2s after any scroll event to prevent corrupting own-state brightness.
| Helper | Purpose |
|---|---|
input_number.bilresa_direction |
Current scroll direction (1=CW, -1=CCW, 0=unlocked) |
input_number.bilresa_brightness |
Own-state brightness tracker (1-254) |
input_datetime.bilresa_last_scroll |
Timestamp of last processed scroll event |
| Parameter | Location | Default | Description |
|---|---|---|---|
| Max step | step: — 50] | min |
50 | Max brightness change per event (fast spin) |
| Min step | step: — 2] | max |
2 | Min brightness change (slow click) |
| Acceleration | step: — divisor 6 |
6 | Lower = steeper curve (try 4 or 8) |
| Direction lock | idle_time | float > 0.4 |
0.4s | Time before allowing direction reversal |
| Sync grace | > 2.0 in sync condition |
2.0s | Idle time before external sync fires |
| Transition | transition: 0.3 |
0.3s | Dimming transition (keep < 0.5s) |
Use debug_logging.py to instrument the matter-server and measure event timing:
# Apply debug patch (wiped on addon restart — safe to leave)
docker cp debug_logging.py addon_core_matter_server:/tmp/debug_logging.py
docker exec addon_core_matter_server python3 /tmp/debug_logging.py
docker exec addon_core_matter_server s6-svc -r /run/service/matter-server
# Watch live events
docker logs -f addon_core_matter_server 2>&1 | grep SWITCH_EVENTOutput format:
SWITCH_EVENT received_at=2026-04-16T09:42:24.775 ep=5 event_id=1 event_number=68800 dev_ts=33423740 ts_type=... data=Switch.Events.InitialPress(newPosition=1)
SWITCH_EVENT received_at=2026-04-16T09:42:24.927 ep=5 event_id=5 event_number=68801 dev_ts=33423781 data=Switch.Events.MultiPressOngoing(newPosition=1, currentNumberOfPressesCounted=3)
Tested with:
- IKEA BILRESA scroll wheel (Matter/Thread SED, Mode 2: EP4=CW, EP5=CCW)
- IKEA KAJPLATS E14 WS Globe 806lm
- Home Assistant OS, SkyConnect USB dongle (Thread/Matter)
- Patch is local: Re-apply after HA updates.
- 500ms cadence is firmware: The MultiPress window is hardcoded in BILRESA firmware.
input_numbermax=254: HA capsinput_numberat 254. The automation uses 254 as the brightness ceiling. Setting 255 causes a silent automation crash.- Direction bounce: CW/CCW endpoints can fire within 1ms during direction transitions. The 400ms direction lock handles this.
v1: Basic
initial_presstrigger, interval-based velocity (time between events for step size).
v2 (2026-04-16): Replaced interval-based velocity with count-based acceleration using
currentNumberOfPressesCounted. Empirical testing confirmed events arrive every ~500ms (Matter MultiPress window), not multi-second SED batches. Added 300ms transition. Fixed direction lock for unlocked state. Sync automation has 2s idle guard.
v3 (2026-04-16): Replaced linear step
clamp(count, 3, 20)with quadraticclamp(count²÷6, 2, 50). Slow scroll=step 2 (precise), fast spin=step 50 (~2.5s full range). Fixed:new_brightnessupper bound corrected to 254 to matchinput_numbermax (255 caused silent automation crash).
v4 (2026-04-16): Updated
event.pypatch to align with upstream PR #159045. Replacedmulti_press_1..Nenumeration with flatmulti_press_ongoing+multi_press_completeevent types. AddedMULTI_PRESS_COUNT_TO_NAMEdict andevent_type_extrafield onmulti_press_completeevents ("single","double","triple"). Simplified_on_matter_node_eventhandler. No automation changes needed — scroll wheel automation already usedmulti_press_ongoing.