This guide documents integrating the Shelly BLU Remote Control ZB (SBRC-005B) with Home Assistant via the BTHome protocol (Bluetooth Low Energy), without relying on any part of the Shelly ecosystem (app, cloud, gateway). Unlike Zigbee pairing for this device — which is documented on the Home Assistant forum and in Zigbee2MQTT — the Bluetooth/BTHome route currently has no published community write-up. This document fills that gap, based on a real deployment using ESPHome Bluetooth proxies with active GATT.
Hardware prerequisites covered in this guide:
- Shelly BLU Remote Control ZB (SBRC-005B), in BTHome mode (not Zigbee)
- At least one ESPHome Bluetooth proxy in range (a Shelly Gen2+ in passive mode also works, with limitations — see below)
- Home Assistant 2026.x with the BTHome integration
Table of contents
- Why BTHome instead of Zigbee
- Prerequisite: Bluetooth proxy and active GATT
- Discovering the device in Home Assistant
- The entities exposed by the device
- Understanding the channel mechanism
- Toggling a switch with the buttons
- Driving a roller shutter with the dimmer wheel
- Debug notification: see every event live
- The Magic Wand: rotation sensors (unexplored)
- Pitfalls encountered and their fixes
- Going further: multi-device extensibility per channel
Why BTHome instead of Zigbee
The BLU Remote Control ZB is a dual-protocol device: both Zigbee and Bluetooth (BTHome) are natively supported, and the choice is made at pairing time (press 4 times for BLE, 5 times for Zigbee).
| Zigbee (Z2M / ZHA) | Bluetooth (BTHome) | |
|---|---|---|
| Community support | Well documented (HA forum, Z2M issues) | Essentially nonexistent to date |
| Required infrastructure | Dedicated Zigbee coordinator | Bluetooth proxy/proxies (ESPHome or Shelly) |
| Advanced features (gestures, fine wheel control) | Limited (factory-fixed group binding 1-4) | Richer, direct access to the native payload |
| Independence from the Shelly ecosystem | Yes | Yes |
Choosing BTHome mainly makes sense if Bluetooth infrastructure is already in place (ESPHome or Shelly Gen2+ proxies available near the remote) — which is the case in the deployment documented here.
Prerequisite: Bluetooth proxy and active GATT
Two types of Bluetooth proxy can relay the remote's BTHome advertisements to Home Assistant:
- Shelly Gen2+: can act as a BLE proxy, but passive mode only. They do not support proxying active GATT connections.
- ESPHome (ESP32): supports active GATT in recent versions, which can potentially surface more information from the device (including some advanced features such as accelerometer-based gestures).
With ESPHome, active GATT is enabled by default in recent versions:
bluetooth_proxy:
# Active connections are now enabled by default
# To disable active connections (previous default behavior), use:
# active: false
No extra configuration is therefore needed if you're running an up-to-date ESPHome proxy — active GATT is already operational.
Discovering the device in Home Assistant
- Put the remote into BLE pairing mode: press a button 4 times (LEDs flash left to right)
- Home Assistant automatically detects the device via the BTHome integration (not the Shelly integration — this is a frequent point of confusion: Shelly BLU devices are not handled by the Shelly integration, but by BTHome)
- A discovery notification appears under Settings → Devices & services
- Confirm the addition — no encryption key is required for this device (unlike some other BTHome devices, which may require one)
The entities exposed by the device
Once added, the device exposes 7 entities:
| Entity | Domain | Role |
|---|---|---|
event.shelly_remote_button_1 |
event |
Button 1 |
event.shelly_remote_button_2 |
event |
Button 2 |
event.shelly_remote_dimmer |
event |
Scroll wheel (rotation) |
sensor.shelly_remote_channel |
sensor |
Active channel (0 to 3) |
sensor.shelly_remote_rotation |
sensor |
Magic Wand angle, X axis (degrees) — see dedicated chapter |
sensor.shelly_remote_rotation_2 |
sensor |
Magic Wand angle, Y axis (degrees) — see dedicated chapter |
sensor.shelly_remote_rotation_3 |
sensor |
Magic Wand angle, Z axis (degrees) — see dedicated chapter |
The event.* entities
The buttons and the dimmer wheel are not exposed as classic Home Assistant event-bus events (unlike Zigbee2MQTT or deCONZ, which use zha_event / deconz_event on the event bus). BTHome uses the Event entity pattern instead: an entity whose state is a timestamp (updated on every event received) and whose attributes carry the event details.
{{ states['event.shelly_remote_button_1'].state }}
# → '2026-06-20T21:00:37.814+00:00' (timestamp of the last event)
{{ states['event.shelly_remote_button_1'].attributes }}
# → {
# 'event_types': ['press', 'double_press', 'triple_press', 'long_press',
# 'long_double_press', 'long_triple_press', 'hold_press'],
# 'event_type': 'press',
# 'device_class': 'button',
# 'friendly_name': 'Shelly Remote Button 1'
# }
For the dimmer:
{{ states['event.shelly_remote_dimmer'].attributes }}
# → {
# 'event_types': ['rotate_left', 'rotate_right'],
# 'event_type': 'rotate_right',
# 'steps': 38,
# 'friendly_name': 'Shelly Remote Dimmer'
# }
Understanding the channel mechanism
This is the most important — and least intuitive — aspect of this remote in BTHome mode.
The channel is not multiplied per button
You might expect to see 8 button entities (2 buttons × 4 channels) and 4 dimmer entities. That is not the case. The device always exposes only 2 buttons + 1 wheel, regardless of which channel is selected. It's the separate sensor.shelly_remote_channel that tells you which channel was active at the moment of the press.
How the channel changes
A single press on the channel button (or a double press if the remote was inactive) changes the active channel (cycling 0 → 1 → 2 → 3 → 0...), signaled by a beep and an LED flash on the remote. However, this does not by itself update sensor.shelly_remote_channel. The channel sensor actually updates when an active button is pressed — button 1, button 2, or the wheel. There is no separate, silent "channel changed" event: the channel sensor updates at the same time the button/wheel action event fires.
Practical consequence for automations: always condition on the current state of sensor.shelly_remote_channel at trigger time, with no assumption of propagation delay — the value is already up to date by the time the first event fires.
conditions:
- condition: state
entity_id: sensor.shelly_remote_channel
state: "0" # The channel is a string, not a number, in this condition
Toggling a switch with the buttons
Base template: one button, one switch, one channel
alias: "Shelly Remote - Buttons - Channel 0"
description: >
Button 1 on channel 0 toggles switch.example_1;
Button 2 on channel 0 toggles switch.example_2.
mode: parallel
triggers:
- trigger: state
entity_id: event.shelly_remote_button_1
id: button_1
- trigger: state
entity_id: event.shelly_remote_button_2
id: button_2
conditions:
- condition: state
entity_id: sensor.shelly_remote_channel
state: "0"
actions:
- choose:
- conditions:
- condition: trigger
id: button_1
sequence:
- action: switch.toggle
target:
entity_id: switch.example_1
- conditions:
- condition: trigger
id: button_2
sequence:
- action: switch.toggle
target:
entity_id: switch.example_2
Key points:
mode: parallel: lets each button be processed independently, so a press on one doesn't block processing of the other if they're pressed in close succession- The channel condition applies globally to the automation (not per branch): if it's not the right channel, nothing fires, regardless of which button was pressed
- The trigger is a
statetrigger on theevent.*entity (noto:specified): this catches any timestamp change, i.e. any new event, regardless of itsevent_type
Extensible pattern: reserving a button for a future device
To anticipate adding a new device later without reworking the whole automation structure, leave one choose branch with an empty sequence:
actions:
- choose:
- conditions:
- condition: trigger
id: button_1
sequence:
- action: switch.toggle
target:
entity_id: switch.couch_light
- conditions:
- condition: trigger
id: button_2
sequence: [] # Reserved for a future device — add the action here when ready
Driving a roller shutter with the dimmer wheel
This is the trickiest part, because of a BTHome behavior that is documented nowhere: every wheel rotation sends two distinct, near-simultaneous events.
The dimmer's real-world behavior (captured live)
Watching the device's logbook in real conditions:
Short roll (one notch):
SBRC-005B E59F dimmer rotate_right: {'steps': 0} ← "ping" event, to be ignored
SBRC-005B E59F dimmer rotate_right: {'steps': 2} ← useful event, gesture magnitude
Long roll:
SBRC-005B E59F dimmer rotate_right: {'steps': 0} ← "ping" event, to be ignored
SBRC-005B E59F dimmer rotate_right: {'steps': 31} ← useful event, gesture magnitude
The first event consistently has steps: 0 — a protocol artifact with no usable value. The second carries the actual gesture magnitude (steps), proportional to how fast/long the wheel was rolled.
Timing of the two events: the first event (steps: 0) fires the moment you start moving the wheel. The second event (steps: N) fires the moment you stop moving the wheel — not at a fixed interval. If you keep scrolling for 4 seconds, the second event arrives roughly 4 seconds after the first one. This matters for automation design: there is no fixed delay to rely on, and a "wait" pattern based on a timeout would either fire too early on a long roll or waste time on a short one. The approach below sidesteps this entirely by reacting only to the second event, whenever it arrives.
Why a naive trigger on attribute: event_type doesn't work
A natural first approach is to trigger the automation on a change of the event_type attribute:
# DOES NOT WORK CORRECTLY
triggers:
- trigger: state
entity_id: event.shelly_remote_dimmer
attribute: event_type
to: rotate_right
The trap: this trigger only re-arms when event_type changes value. But the two consecutive events of a single gesture share the same event_type (rotate_right → rotate_right; only steps differs). The result: only the first event (the "ping" with steps: 0) actually fires the automation. The second event — the one carrying the useful information — never reaches the actions, even though it's clearly visible in the device's logbook.
The correct approach: trigger on any state change, filter in the conditions
alias: "Living Room Shutter - Shelly Remote Dimmer"
description: >
Shelly Remote dimmer on channel 0. Each rotation sends 2 BTHome events on
the same entity_id/event_type (no attribute change between the two): a
parasitic 'steps: 0' followed by 'steps: N' = gesture magnitude. Trigger on
ANY state change (timestamp) of the event entity; event_type and steps>0
filtering happens in the choose conditions. 1 step = 1% movement
(steps >= 100 = full open/close).
mode: queued
triggers:
- trigger: state
entity_id: event.shelly_remote_dimmer
conditions:
- condition: state
entity_id: sensor.shelly_remote_channel
state: "0"
actions:
- choose:
- conditions:
- condition: template
value_template: >
{{ trigger.to_state.attributes.event_type == 'rotate_left'
and trigger.to_state.attributes.steps | int(0) > 0 }}
sequence:
- action: cover.set_cover_position
data:
position: >
{{ min(100, (state_attr('cover.living_room_shutter', 'current_position') | int(0))
+ (trigger.to_state.attributes.steps | int(0))) }}
target:
entity_id: cover.living_room_shutter
- conditions:
- condition: template
value_template: >
{{ trigger.to_state.attributes.event_type == 'rotate_right'
and trigger.to_state.attributes.steps | int(0) > 0 }}
sequence:
- action: cover.set_cover_position
data:
position: >
{{ max(0, (state_attr('cover.living_room_shutter', 'current_position') | int(0))
- (trigger.to_state.attributes.steps | int(0))) }}
target:
entity_id: cover.living_room_shutter
Why this works:
- The
statetrigger withoutattribute:reacts to thestatevalue itself changing — which is a timestamp, so it changes on every event received, including both consecutive events of a single gesture - Sorting the "ping" event (
steps: 0, ignored) from the useful event (steps: N, processed) happens via atemplatecondition inside thechoose, reading directly fromtrigger.to_state.attributes cover.set_cover_positioncomputes a new position relative to the current position (current_position), treatingstepsas a percentage delta (1 step = 1%), capped between 0 and 100 viamin()/max()
This last point — the steps-to-percentage ratio — is the part you'll want to tweak to match your own blinds, so that a full, deliberate roll closes (or opens) the cover completely without needing a second gesture.
This template intentionally departs from the "prefer native conditions" rule: there is no native equivalent (numeric_state, etc.) for filtering on a trigger's own attribute from within a choose — Home Assistant's best-practices linter flags this case, but it's a legitimate false positive here.
Understanding the relative position calculation for the dimmer wheel
This is a quick breakdown of how the cover.set_cover_position template works in the dimmer automation, for anyone wondering what the min()/max() math is actually doing.
The general idea
The template doesn't say "set the cover to X%". It says "add or subtract a percentage from the cover's current position". That's why it reads current_position first, before computing the new value.
{{ min(100, (state_attr('cover.shelly_2_5_salon', 'current_position') | int(0)) + (trigger.to_state.attributes.steps | int(0))) }}
Broken down piece by piece:
| Piece | Role |
|---|---|
state_attr('cover.shelly_2_5_salon', 'current_position') |
Reads the cover's current position (e.g. 40%) |
| ` | int(0)` |
| `+ (trigger.to_state.attributes.steps | int(0))` |
min(100, ...) |
Prevents the result from going above 100% even if the raw math would |
Example 1 — Opening (rotate_left)
position: "{{ min(100, (state_attr('cover.shelly_2_5_salon', 'current_position') | int(0)) + (trigger.to_state.attributes.steps | int(0))) }}"
Scenario: the cover is at 40%, you roll the wheel upward, which fires steps: 15.
current_position = 40
steps = 15
new position = min(100, 40 + 15) = min(100, 55) = 55
→ The cover moves up to 55%.
If you'd been at 90% and rolled with steps: 25:
min(100, 90 + 25) = min(100, 115) = 100
→ min() stops the math from reaching 115%; the cover cleanly stops at 100% (fully open).
Example 2 — Closing (rotate_right)
position: "{{ max(0, (state_attr('cover.shelly_2_5_salon', 'current_position') | int(0)) - (trigger.to_state.attributes.steps | int(0))) }}"
Same logic, but subtracting instead of adding, and using max(0, ...) instead of min(100, ...) so the position never goes below 0%.
Scenario: the cover is at 55%, you roll downward with steps: 20.
current_position = 55
steps = 20
new position = max(0, 55 - 20) = max(0, 35) = 35
→ The cover moves down to 35%.
If you'd been at 10% with steps: 30:
max(0, 10 - 30) = max(0, -20) = 0
→ max() stops the math from going negative; the cover stops at 0% (fully closed).
Where the "ratio" actually lives
The only thing you might want to tune is the multiplier applied to steps before it's added/subtracted. As written, it's raw steps (so 1 step = 1%). If you want more travel per gesture (a short roll moves the cover further):
+ (trigger.to_state.attributes.steps | int(0) * 1.5) # 1 step = 1.5%, more generous movement
or for finer control:
+ (trigger.to_state.attributes.steps | int(0) * 0.5) # 1 step = 0.5%, more precise movement
The min()/max() calls never change — they just guard the 0-100 bounds regardless of whatever multiplier you apply to steps.
Works with non-Shelly covers (RF time-based)
This same template works as-is on an RF-controlled cover (a box emulating a physical remote, with no native position feedback), as long as the integration exposes current_position and the SET_POSITION feature — which is the case, for example, with the cover_rf_time_based integration, which internally calculates how long to activate the RF signal based on a configured travel time:
# Example RF cover entity configuration (excerpt)
travelling_time_up: 25
travelling_time_down: 24
Simply replace cover.living_room_shutter with the RF cover's entity_id in the template above — no other adaptation is needed.
Practical limitation of RF time-based covers: since there's no real physical position feedback (position is calculated, not measured), very closely spaced rolls may produce a slightly less smooth movement than with a cover offering native position feedback (such as a Shelly 2.5).
Debug notification: see every event live
Before building more complex automations, a simple debug automation lets you confirm all events are arriving correctly and quickly identify the active channel:
alias: "Notify - Shelly Remote button pressed"
description: >
Sends a notification to the phone with the name of the button/dimmer
used and the remote's active channel.
mode: single
triggers:
- trigger: state
entity_id: event.shelly_remote_button_1
id: button_1
- trigger: state
entity_id: event.shelly_remote_button_2
id: button_2
- trigger: state
entity_id: event.shelly_remote_dimmer
id: dimmer
actions:
- action: notify.send_message
target:
entity_id: notify.your_phone
data:
message: >
{{ trigger.to_state.attributes.friendly_name }}
({{ trigger.to_state.attributes.event_type }}) -
Channel {{ states('sensor.shelly_remote_channel') }}
Note on
notify.send_message: with the newer architecture of thenotifydomain in Home Assistant, per-device dedicated services (notify.your_phone) are no longer directly callable as a service — you need to use the genericnotify.send_messageservice with atarget.entity_idpointing at the notify entity.
The Magic Wand: rotation sensors (unexplored)
Beyond the buttons and the dimmer wheel, Shelly's own documentation describes a third interaction mode, referred to as the "Magic Wand":
"Long press [on channel button]: Starts angle measurement using the built-in accelerometer. Upon release, the rotation angle is transmitted in the X, Y, and Z axes — one angle per axis."
In practice, this surfaces as three additional sensor entities on the device, on top of the four already covered:
| Entity | Domain | Role |
|---|---|---|
sensor.shelly_remote_rotation |
sensor |
Rotation angle, X axis (degrees) |
sensor.shelly_remote_rotation_2 |
sensor |
Rotation angle, Y axis (degrees) |
sensor.shelly_remote_rotation_3 |
sensor |
Rotation angle, Z axis (degrees) |
Triggering a long press on the channel button and then physically tilting/rotating the remote produces values like these (captured live):
Channel: 2
Rotation: -1,0°
Rotation: -61,9°
Rotation: 58,0°
Each of the three sensors reports a signed angle, presumably relative to the remote's resting orientation at the moment the long press started — consistent with a per-axis accelerometer reading taken on release, as described in Shelly's documentation.
Status: not explored, not used
This guide does not cover how to build automations from these three sensors. They are documented here for completeness but no gesture-based automation has been built or tested against them as part of this write-up.
Open questions that would need investigating before relying on these sensors in an automation:
- Whether the three angles update together as a single atomic event, or independently (which would affect trigger design, similar to the dimmer's two-event quirk documented above)
- What axis convention is used, and whether it's stable across remotes/firmware versions
- Whether meaningful gesture patterns (e.g. "tilt left", "flip over") can be reliably derived from threshold values on one or more axes, or whether this requires more elaborate vector math
- Battery/responsiveness impact of relying on the accelerometer-driven Magic Wand feature versus the simpler button/dimmer events
If you experiment with this feature, the existing template patterns in this guide (trigger on any state change of the relevant entity, filter using trigger.to_state in the automation's conditions) are a reasonable starting point — but expect to do your own discovery work, as there is currently no reference implementation to build from.
Pitfalls encountered and their fixes
| Symptom | Cause | Fix |
|---|---|---|
| The channel seems stuck, with an apparent 15-20s update latency | Initial misreading — there's actually no latency: the channel only changes on an active button press, not in the background | Always trigger a press to advance the channel; reading sensor.shelly_remote_channel at event time is enough, no delay involved |
bthome_event shows nothing in Developer Tools → Events |
BTHome doesn't use the classic event bus (unlike ZHA/deCONZ) | Use the device's event.* entities, not an event bus |
| The shutter fully opens/closes instead of moving gradually | Initial misunderstanding of steps semantics (mistaken for a "held continuously" signal) |
steps represents a discrete gesture magnitude, not a continuous stream — use cover.set_cover_position in relative mode, not cover.open_cover/close_cover |
| The automation's action appears to be completely ignored even though the trigger fires (visible in traces) | A trigger using attribute: event_type that doesn't re-arm because the watched attribute doesn't change between the two consecutive events of a single gesture |
Trigger on any state change (no attribute restriction), move the filtering into the choose conditions |
Going further: multi-device extensibility per channel
The channel system (0 to 3) lets a single remote potentially control up to 4 different "contexts" — typically one room or function per channel. The recommended pattern for staying maintainable over time:
- One "Buttons" automation per channel, structured as a
choosewith one branch per button, with empty sequences reserved for buttons not yet assigned - One "Dimmer" automation per channel as soon as a cover needs to be driven on that channel, reusing the template from the previous section
- Keep a consistent naming scheme (
Shelly Remote - Buttons - Channel N,Shelly Remote - Dimmer - Channel N) to stay organized as the list of automations grows
This architecture lets you add a new device on an existing channel by touching only a single line (a sequence: of one choose branch), without having to rework the overall structure.
Document written from a real-world deployment using ESPHome Bluetooth proxies (active GATT), Home Assistant 2026.5.4, and a Shelly BLU Remote Control ZB (SBRC-005B) in BTHome mode.
And with the help of claude
If you see any mistakes please let me know ![]()




