The BLANCO CHOICE.All is a drinking water system under the sink: filtered still, medium and sparkling water, plus boiling water, from one tap. It talks to exactly one thing: the BLANCO UNIT App, over Bluetooth Low Energy. No cloud, no API, no binding, and nothing for openHAB to bind to.
So I put an ESP32 next to it that speaks BLE to the unit and MQTT to openHAB. Filter and CO2 levels become Items, and the sitemap gets nine buttons for the combinations of glass size and carbonation my family actually uses.
(My own installation runs in German, hence the labels in the screenshot. The examples below are in English.)
What you’re building
BLANCO CHOICE.All ──BLE──► ESP32 gateway ──MQTT──► broker ──► openHAB
(under the sink) (a few euro) Things / Items / Rules
The gateway is blanco-esp32. It keeps a BLE connection to the unit, authenticates with the unit’s pairing code, polls the status every 15 minutes and publishes it as JSON. Dispensing works the other way round: publish an amount and a carbonation level, and water comes out.
The heavy lifting, reverse engineering the BLE protocol, is not mine. That is @Nailik’s work in blanco_unit; my part is an ESP32 firmware that does not need a computer running next to the sink.
Before you start
- An ESP32 or ESP32-S3. Any board will do. I use an ESP32-S3-DevKitC-1; the classic ESP32 is tested too.
- Your unit’s five-digit pairing code. It is the same one the BLANCO UNIT App asks for, printed on the type plate behind the CO2 cartridge. Open the front flap of the under-sink unit, the plate sits next to the cylinder and the filter. It cannot be read from the display.
- Connectivity switched on at the unit. On the under-sink display, work through the menu until the connectivity mode appears and set it to
CON ON.CON OFFmeans the BLE module is off and nothing will find it. - An MQTT broker and the MQTT binding, which you almost certainly already have.
The unit accepts one BLE connection at a time. While the gateway holds it, the BLANCO UNIT App will not connect, and vice versa. Close the app before you start.
Step 1: Flash the gateway
There is a web installer: plug the board in, open the page in Chrome or Edge, pick your chip, done. No toolchain, no clone.
If you prefer to build it yourself:
git clone https://github.com/bruestel/blanco-esp32.git
cd blanco-esp32
pio run -e esp32s3 -t upload # or: -e esp32dev
On an ESP32-S3-DevKitC-1, use the port labelled UART, not the one labelled USB. The native USB port can leave the chip sitting in its ROM bootloader after a reset, which looks exactly like a dead board.
Step 2: Set it up in the browser
Nothing is compiled in. On first boot the gateway opens an access point called BlancoGW-XXXXXX, password blancogw. Join it and the setup page appears at http://192.168.4.1/.
Fill in WiFi and your broker, then scan for BLE devices. Your unit shows up as CHOICE.All-… and is sorted to the top. Finally enter the pairing code. Press Check connection before saving: it runs the whole BLE path and tells you which step fails, so a mistyped code shows up here rather than as a gateway that reboots every few seconds.
The topic prefix defaults to blanco and the device name to the unit’s MAC address without colons, which is what the examples below use. Both are configurable.
To get back in later: hold BOOT for five seconds, or publish to blanco/<name>/command/setup.
Step 3: The Thing
Two status channels and two command channels are enough for everything in the screenshot. The status topic carries one JSON document, so each channel picks its value out with JSONPATH:
// things/blanco.things
Thing mqtt:topic:central:blanco "Blanco" (mqtt:broker:central) @ "Kitchen" [
availabilityTopic="blanco/aabbccddeeff/available",
payloadAvailable="online",
payloadNotAvailable="offline"
] {
Channels:
// Status
Type number : filter_rest "Filter Rest" [ stateTopic="blanco/aabbccddeeff/status", transformationPattern="JSONPATH:$.status.filter_rest" ]
Type number : co2_rest "CO2 Rest" [ stateTopic="blanco/aabbccddeeff/status", transformationPattern="JSONPATH:$.status.co2_rest" ]
// Commands
Type string : dispense "Dispense" [ commandTopic="blanco/aabbccddeeff/command/dispense" ]
Type switch : refresh "Refresh" [ commandTopic="blanco/aabbccddeeff/command/refresh", on="true", off="true" ]
}
Replace aabbccddeeff with your unit’s MAC in lowercase without colons, the same value the setup page filled in for you.
The availability topic is worth wiring up: it is a retained last will, so openHAB marks the Thing offline the moment the gateway drops off, rather than showing stale values forever.
Step 4: Items
// items/blanco.items
Group gBlanco "Blanco"
Number:Dimensionless Blanco_FilterRest "Filter remaining [%d %%]" <battery> (gBlanco, gPersist) {channel="mqtt:topic:central:blanco:filter_rest", unit="%"}
Number:Dimensionless Blanco_CO2Rest "CO2 remaining [%d %%]" <carbondioxide> (gBlanco, gPersist) {channel="mqtt:topic:central:blanco:co2_rest", unit="%"}
String Blanco_Dispense "Dispense command" (gBlanco, gPersistNone) {channel="mqtt:topic:central:blanco:dispense"}
Switch Blanco_Refresh "Refresh" (gBlanco, gPersistNone) {channel="mqtt:topic:central:blanco:refresh"}
Step 5: The sitemap, without a single rule
This is the part I like. The dispense command is a small JSON document:
{ "amount": 200, "intensity": 1 }
amount is millilitres (50–2000), intensity is 1 still, 2 medium, 3 sparkling. Since the String Item passes its command straight through to the MQTT topic, the JSON can live in the sitemap mappings. No rule builds it, no transformation is involved:
// sitemaps/home.sitemap
Text item=Blanco_CO2Rest label="Blanco faucet []" staticIcon="faucet_kitchen" {
Default item=Blanco_FilterRest staticIcon="cleaning"
Default item=Blanco_CO2Rest
Switch item=Blanco_Dispense staticIcon="faucet_kitchen" label="Dispense (still) []" mappings=['{"amount":200,"intensity":1}'="Small glass", '{"amount":300,"intensity":1}'="Large glass", '{"amount":1000,"intensity":1}'="Bottle"]
Switch item=Blanco_Dispense staticIcon="faucet_kitchen" label="Dispense (medium) []" mappings=['{"amount":200,"intensity":2}'="Small glass", '{"amount":300,"intensity":2}'="Large glass", '{"amount":1000,"intensity":2}'="Bottle"]
Switch item=Blanco_Dispense staticIcon="faucet_kitchen" label="Dispense (sparkling) []" mappings=['{"amount":200,"intensity":3}'="Small glass", '{"amount":300,"intensity":3}'="Large glass", '{"amount":1000,"intensity":3}'="Bottle"]
}
Note the single quotes around each mapping value: the JSON contains double quotes, so the mapping has to be quoted the other way round. Three intensities times three sizes gives the nine buttons in the screenshot.
Step 6: Rules
Only one rule is actually required, and it exists for a UI reason: a Switch with mappings sends a command only when the value changes. Without resetting the Item, pressing “Small glass” twice in a row does nothing the second time.
// rules/blanco.rules
rule "Reset Blanco Dispense"
when
Item Blanco_Dispense received command
then
if (receivedCommand != NULL) {
Blanco_Dispense.postUpdate(NULL)
}
end
The other two are the reason I built this in the first place, because the unit shows its levels on a display nobody looks at:
rule "Blanco water filter empty"
when
Item Blanco_FilterRest changed
then
if (Blanco_FilterRest.state instanceof Number) {
val rest = (Blanco_FilterRest.state as Number).intValue
if (rest == 0) {
logInfo("Blanco", "Water filter at 0%, needs replacing")
val actions = getActions("pushover", "pushover:pushover-account:account")
actions.sendMessage("The water filter is empty (0%), please replace it", "💧 Blanco")
}
}
end
The same again for Blanco_CO2Rest, because a CO2 cylinder that runs out mid-party is the other thing worth a push notification. Both levels are percentages, so warning at 10 % instead of 0 % is a one-character change.
What else the gateway publishes
| Topic | Direction | Contents |
|---|---|---|
blanco/<name>/available |
→ openHAB | online / offline, retained, last will |
blanco/<name>/status |
→ openHAB | full JSON, retained, every 15 min |
blanco/<name>/command/result |
→ openHAB | success / failed after dispensing |
blanco/<name>/command/dispense |
→ gateway | {"amount":200,"intensity":1} |
blanco/<name>/command/refresh |
→ gateway | poll now, any payload |
blanco/<name>/command/reboot |
→ gateway | restart |
blanco/<name>/command/setup |
→ gateway | open the setup portal for three minutes |
The status document has more in it than the two levels I use: boiler and compressor temperatures, an error bit field, the serial number, firmware versions of all three controllers, and the gateway’s own version and hostname:
{
"device": { "device_name": "CHOICE.All-128", "serial_number": "…", "firmware": { "main": "…", "comm": "…", "elec": "…" } },
"gateway": { "version": "0.9.0", "hostname": "blanco-gw-a1b2c3" },
"status": { "filter_rest": 92, "co2_rest": 10, "temp_boiler_1": 7, "temp_boiler_2": 90, "temp_compressor": 35, "error_bits": 0 },
"last_update": "3600s uptime"
}
Adding another channel is one JSONPATH line.
Links
- Firmware, documentation and releases: github.com/bruestel/blanco-esp32
- Browser installer: bruestel.github.io/blanco-esp32
- Protocol work this builds on: Nailik/blanco_unit
- MQTT Binding docs
Happy to answer questions. And if someone gets it working with the drink.soda EVOL-S Pro, which uses the same app, I would like to hear about it.

