Grundfos SCALA1 in Home Assistant via ESPHome (Bluetooth) – status + enable/disable
I’ve got my Grundfos SCALA1 booster pump working in Home Assistant over its built-in Bluetooth, using an ESPHome device (an M5Stack AtomS3 that’s also my Bluetooth proxy). No cloud, no Grundfos GO app needed once it’s paired.
What you get in Home Assistant
- Pump running – on while the motor is actually pumping
- Water demand – pressure low / water being drawn (stays on while a tap is open even if the pump is disabled – handy for spotting a tap left running or a leak)
- Pump enabled (switch) – on = automatic (runs when pressure drops), off = disabled (won’t run at all). Reflects the pump’s real state.
- Pump status bytes – diagnostic, the raw status values
The key discovery: the SCALA1 drops any Bluetooth connection that isn’t bonded and encrypted. Pairing only works while the pump’s blue wireless button is pressed, and Home Assistant’s own Bluetooth/proxy pairing path gets refused – but an ESP32 that requests encryption itself on connect works. ESPHome’s alpha3 component (for the ALPHA3 circulator) does exactly that, so this config uses it purely for its encryption step.
The pump talks Grundfos GENIbus over BLE (same service/characteristic as the ALPHA3: fe5d / 859cffd1-036e-432a-aa28-1a0085b87ba9).
Setup
-
Save the package below as
esphome/packages/scala1.yaml(a subfolder stops the ESPHome dashboard listing it as a device). -
Put your pump’s Bluetooth MAC address in
pump_mac. -
Add to your ESP32’s config (ESP-IDF framework):
packages: scala1: !include packages/scala1.yaml -
Install, then press the pump’s blue wireless button – the light should stay lit once bonded.
-
Only one Bluetooth connection is allowed: close the Grundfos GO app (or turn off the “Pump Bluetooth link” switch to use the app), and don’t run other Grundfos BLE integrations at the same time.
It works alongside bluetooth_proxy – the package raises esp32_ble: max_connections to 4 so the proxy keeps its 3 slots.
Package
# Grundfos SCALA1 package - add to an existing ESPHome Bluetooth proxy config:
#
# packages:
# scala1: !include packages/scala1.yaml
#
# Uses one Bluetooth connection slot of its own.
# FIRST-TIME PAIRING: after flashing, press the pump's blue wireless button.
# Disable the grundfos_bluetooth custom integration in Home Assistant - the pump
# only accepts one connection at a time.
substitutions:
pump_mac: "XX:XX:XX:XX:XX:XX" # your pump's Bluetooth MAC address
geni_service: "fe5d"
geni_char: "859cffd1-036e-432a-aa28-1a0085b87ba9"
# Quieten logs: the ALPHA3 component (kept only for encryption) warns about every
# SCALA1 reply it doesn't recognise; raw GENI replies only show at debug level.
# Change geni to DEBUG if you need to see the raw replies again.
logger:
logs:
alpha3: ERROR
geni: INFO
# The Bluetooth proxy uses 3 connection slots; the pump link needs a 4th.
# (If your main config already sets esp32_ble max_connections, make it at least 4.)
esp32_ble:
max_connections: 4
ble_client:
- mac_address: ${pump_mac}
id: scala
on_connect:
then:
- logger.log: "Connected to SCALA1"
on_disconnect:
then:
- logger.log: "Disconnected from SCALA1"
sensor:
# ESPHome's ALPHA3 component is used ONLY for its connect behaviour: it asks
# the pump for an encrypted (bonded) link as soon as it connects. Without this
# the SCALA1 hangs up after ~2 s. No sensors and no polling, so it sends nothing.
- platform: alpha3
ble_client_id: scala
update_interval: never
# Receives every GENIbus reply from the pump and decodes the status object.
- platform: ble_client
type: characteristic
ble_client_id: scala
name: "GENI raw"
internal: true
service_uuid: ${geni_service}
characteristic_uuid: ${geni_char}
notify: true
update_interval: never
lambda: |-
ESP_LOGD("geni", "RX (%u bytes) %s", (unsigned) x.size(), format_hex_pretty(x).c_str());
// Status reply to the 0x5B request: 24 LE F8 E7 0A .. .. .. A4 03 00 00 0B <S1> <S2> ...
// Observed:
// S1 bit 0x04 = disabled (STOP command)
// S1 bit 0x40 = demand (pressure low / water drawn) - stays set while disabled
// S2 = 0B running, 0A running down, 08 stopped -> bits 0x03 = motor running
if (x.size() >= 15 && x[0] == 0x24 && x[4] == 0x0A && x[8] == 0xA4) {
uint8_t s1 = x[13], s2 = x[14];
id(pump_running).publish_state((s2 & 0x03) != 0);
id(pump_demand).publish_state((s1 & 0x40) != 0);
id(pump_enabled).publish_state((s1 & 0x04) == 0);
char buf[8];
snprintf(buf, sizeof(buf), "%02X %02X", s1, s2);
if (id(pump_status_bytes).state != buf) {
ESP_LOGI("geni", "Status bytes changed: %s", buf);
id(pump_status_bytes).publish_state(buf);
}
}
return NAN;
# Poll the pump's status object every 10 s (request taken from the Grundfos app).
interval:
- interval: 10s
then:
- ble_client.ble_write:
id: scala
service_uuid: ${geni_service}
characteristic_uuid: ${geni_char}
value: [0x27, 0x07, 0xE7, 0xF8, 0x0A, 0x03, 0x5B, 0x00, 0xA4, 0x12, 0xA3]
binary_sensor:
- platform: template
id: pump_running
name: "Pump running"
device_class: running
- platform: template
id: pump_demand
name: "Water demand"
icon: mdi:water-alert
- platform: template
id: pump_enabled
name: "Pump enabled state"
internal: true
text_sensor:
# Raw status bytes, to confirm the decoding (diagnostic)
- platform: template
id: pump_status_bytes
name: "Pump status bytes"
entity_category: diagnostic
switch:
# Turn off to free the pump for the Grundfos GO app
- platform: ble_client
ble_client_id: scala
name: "Pump Bluetooth link"
# On = enabled: runs automatically when pressure is low (GENIbus START, id 6).
# Off = disabled: will not run even if pressure drops (GENIbus STOP, id 5).
- platform: template
name: "Pump enabled"
icon: mdi:water-pump
lambda: |-
if (!id(pump_enabled).has_state()) return {};
return id(pump_enabled).state;
turn_on_action:
- ble_client.ble_write:
id: scala
service_uuid: ${geni_service}
characteristic_uuid: ${geni_char}
value: [0x27, 0x05, 0xE7, 0xF8, 0x03, 0x81, 0x06, 0xE5, 0x87]
turn_off_action:
- ble_client.ble_write:
id: scala
service_uuid: ${geni_service}
characteristic_uuid: ${geni_char}
value: [0x27, 0x05, 0xE7, 0xF8, 0x03, 0x81, 0x05, 0xD5, 0xE4]
How the status is decoded
The package polls the status object the Grundfos app uses every 10 s. In the reply, two bytes matter:
| Byte | Meaning |
|---|---|
S1 bit 0x04 |
disabled (STOP command) |
S1 bit 0x40 |
demand – pressure low / water drawn |
S2 bits 0x03 |
motor running (0B running, 0A running down, 08 stopped) |
Seen in practice: 81 08 enabled + idle, C1 0B running, C5 08 → 45 08 disabled while demand persists.
Control uses plain GENIbus class-3 commands: START (6) to enable and STOP (5) to disable. The usual REMOTE (7) command is rejected by the SCALA1 as unknown.
Tested on one SCALA1 – feedback from other models/firmware welcome. Thanks to the ESPHome alpha3 component and the grundfos-bluetooth project’s protocol notes, which made this possible. Also to Claude!