Shelly BLU Remote Control ZB over BTHome : a complete guide

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

  1. Why BTHome instead of Zigbee
  2. Prerequisite: Bluetooth proxy and active GATT
  3. Discovering the device in Home Assistant
  4. The entities exposed by the device
  5. Understanding the channel mechanism
  6. Toggling a switch with the buttons
  7. Driving a roller shutter with the dimmer wheel
  8. Debug notification: see every event live
  9. The Magic Wand: rotation sensors (unexplored)
  10. Pitfalls encountered and their fixes
  11. 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

  1. Put the remote into BLE pairing mode: press a button 4 times (LEDs flash left to right)
  2. 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)
  3. A discovery notification appears under Settings → Devices & services
  4. 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 state trigger on the event.* entity (no to: specified): this catches any timestamp change, i.e. any new event, regardless of its event_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_rightrotate_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 state trigger without attribute: reacts to the state value 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 a template condition inside the choose, reading directly from trigger.to_state.attributes
  • cover.set_cover_position computes a new position relative to the current position (current_position), treating steps as a percentage delta (1 step = 1%), capped between 0 and 100 via min()/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.

:warning: 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 the notify domain in Home Assistant, per-device dedicated services (notify.your_phone) are no longer directly callable as a service — you need to use the generic notify.send_message service with a target.entity_id pointing 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 choose with 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 :slight_smile: