Bosch eBike Live Data Interface bridge as an ESPHome external component (BLE)

Hi all,

I have been working on an ESPHome external component that turns a plain ESP32 into a Bluetooth bridge for the Bosch smart system Live Data Interface that Bosch quietly published in May 2026. It surfaces all 13 live data fields (battery SoC, speed, cadence, rider power, odometer, light, lock state, charger connected, and the rest) as ESPHome sensors and binary_sensors. Repo and bilingual docs:

ha-bosch-ebike/esphome at main · Xunil99/ha-bosch-ebike · GitHub

Why this is interesting

Bosch's LDI spec (v1.0, May 2026) is public, Apache-2.0 licensed, with no registration and no keys: Live Data Interface: eBike-Echtzeitdaten via API nutzen. The catch is that smartphone support is explicitly out of scope, so we need a small stationary BLE accessory. An ESP32 in the garage or shed turned out to be the natural fit. Smart system control unit release v19 or newer is required on the bike side.

Easy install (no ESPHome dashboard needed)

For non-technical users I built a browser-based installer using esp-web-tools plus an automated GitHub Actions build:

Bosch eBike LDI Bridge - Web Installer

Plug an ESP32 via USB, open the page in Chrome or Edge, click Install. WiFi setup happens in the same browser session via Improv-Serial. After that, the device shows up in HA's auto-discovery and adopts into the ESPHome dashboard if available. The whole flashing flow takes about a minute, including WiFi.

DIY-style YAML for those who prefer to compile themselves is also in the repo under esphome/example-bridge.yaml. It pulls the component directly from the repo via external_components: source: github://..., so updates flow in automatically on the next compile.

A few technical highlights that may interest the ESPHome crowd

  • Inverse GAP / GATT roles: the eBike is GAP Central + GATT Server, the accessory must be GAP Peripheral + GATT Client. The ESP32 advertises with a Service Solicitation (AD type 0x15) for the LDI service UUID and then acts as GATT client on the resulting connection. This is unusual and not directly representable in NimBLE's ble_hs_adv_fields, so the AD payload is built by hand and passed to ble_gap_adv_set_data().
  • Mandatory bonding with LE Secure Connections, Just Works, stored persistently in NVS. NimBLE's ble_store_config_init() handles persistence cleanly across reboots and OTA.
  • Bosch firmware quirks: the spec acknowledges three known issues in v19. Two of them (DLE and MTU not being initiated by the eBike) are worked around by the bridge actively triggering both itself right after encryption setup.
  • Protobuf payload: the live data characteristic is a com.bosch.ebike.LiveData proto3 message. Hand-rolled decoder, around 120 lines, no nanopb build setup required. Forward-compatible by design, since unknown tags are skipped per proto3 rules.
  • Bond-resume race fix: NimBLE sometimes delivers BLE_GAP_EVENT_ENC_CHANGE to user space before BLE_GAP_EVENT_CONNECT on bonded reconnects. The handler reads conn_handle straight from the event struct rather than from cached state to avoid an ENOTCONN from ble_gattc_exchange_mtu.

Pairs with the cloud integration

This bridge complements an existing HACS integration in the same repo that pulls tour history from the Bosch Data Act cloud API. From v1.10.0, the cloud integration can optionally use the live BLE values as the source of truth for tour distance and battery consumption, replacing the snapshot-based cloud estimates.

Status

Tested with a Performance Line CX on smart system v19.54.0. Should work with any v19+ drive unit since the LDI is unit-agnostic, but confirmation from other variants (SX, Cargo, Performance Line) would be very welcome. ESP32-S3, C3 and C6 also untested so far, plain ESP32 dev boards work.

Issues, ideas, drive unit confirmations and PRs all appreciated.

Cheers,
Xunil99

Thanks for your effort.
Sounds very interesting.
Does the bike only send live data when it is turned on?
I understood that the ESP is stationary placed inside the garage, what benefit does it have besides the state of charge transmission?

Regards

Yes, the eBike sends live data only when it's turned on. However, it seems to wake up about all two hours, connects to BLE and sends data.

The ESPHome is intended to be stationary, however, I'm just working on a small ESP32 C3 Module with a battery, that you can put in your bag and that saves all data. Should be able to save all data for about 8 to 10 hours. Not sure if anyone would use this, I'm just trying to find out, if that works because I'm interested :slight_smile:

Can this bridge be installed on a Esp32 which is already used for 2 sensors? If that's possible, I'll combine the settings with my current Esp :slight_smile:

Great work again!

Should work, yes. If you could give me feedback, that would help other users :slight_smile:

Nice! I will try to make some time this week. I'll let you know if I succeed!

I got it working :slight_smile:

It is running on my bedsensor board, which has 2 FSR bedsensors (FSR - the best bed occupancy sensor) and a DHT sensor as well.
Since our bedroom (1st floor) is located next to the driveway, it connected directly to the bike.

I just copied the parts from the example code on Github and sorted it between the original code of my sensor.

Thanks for the amazing work! Now I can turn my charger on and off more accurate on <25% and >80% without guessing! (using a Shelly plug S)

Thank you for your feedback!! Sounds great!

Great job @Xunil99 :grinning_face:
Did anyone try to flash a smart plug with this?
I was thinking of this one so I can charge to 80% by default and top it up to 100% on the same day doing a ride...

@ma_aue Funny timing - that is exactly what @henri98 just contributed. See PR #22, now merged: a standalone charge limiter on a Sonoff BASICR4 (which has an ESP32-C3 inside, so it runs the LDI bridge AND switches mains via its onboard relay). It reads the live battery SoC over BLE and cuts the relay at a configurable limit (default 80%), all on the device, so it works even without Home Assistant in the loop.

It is built for the BASICR4, but the logic (template number for the limit + a battery_soc.on_value that turns the switch off when SoC reaches it) ports to any ESPHome-controlled smart plug, like your Athom, as long as the bridge has the bike's SoC.

Your "80% by default, top up to 100% before a ride" idea goes one step further than the current example, which just stops at the limit. With an Athom plug in Home Assistant you could do the top-up part as an HA automation (raise the limit / re-enable charging on the morning of a ride). The charge limiter device gives you the reliable hard stop; HA handles the smarter scheduling on top.

Credit for the charge limiter goes to @henri98.

For anyone who wants to take the bridge along on a ride and still get the data into Home Assistant at home, there is now a documented mobile setup.

The idea: the ESP runs off a powerbank in your frame bag, joins your phone's hotspot, and pushes its data home through a WireGuard tunnel that the ESP builds itself. You do not need a VPN app on the phone, it just provides the hotspot. It uses MQTT instead of the native API, because with MQTT the ESP pushes the data, so the constantly changing IP on the road no longer matters.

Guide and example config:

One thing worth knowing if you run your own WireGuard server with a separate tunnel subnet: ESPHome's WireGuard does not do policy routing, so the ESP's tunnel IP has to be in the same subnet as your MQTT broker (a FritzBox does this automatically), or you use full tunnel 0.0.0.0/0. There is a short troubleshooting section in MOBILE.md covering exactly that.

All values (WiFi, hotspot, MQTT, WireGuard) live in secrets.yaml, so nothing personal ends up in the config.

Sounds great :grinning_face:
I was more thinking in the direction to let HA handle all the logic and just expose all entities from the smart plug running the Bosch ldi stuff.

Overall I'd like to have:

  • cool down timer when connecting the bike to the charger, like 60 min. Quite often I just plug it in after coming home from a ride.
  • default 80% charge limit
  • manual top up to 100% either toggled by HA or the button on the smart plug
  • LED indicating if top up mode is enabled on the smart plug
  • enabling top up mode should also cancel the #1 timer

There are probably different preferences for everyone so imho it is best to do most of it in HA and only add Bosch support on the smart plug.

I ordered these plugs yesterday, let's see when they arrive and how things work out then. Good to know that the platform is known as working. :slightly_smiling_face:

I've tried to use the web installer on a ESP32-S3 board which also supports a 2.8" LCD display and an SD card slot. It indicates that the board is not supported.
Its spec: Equipped with ESP32-S3 Dual-Core Processor, Clocked at 240MHz, 16MB SRAM, 8MB PSRAM, Supports 2.4GHz WiFi and Bluetooth 5.0.

I was interested in logging data to an SD card whilst on the move. Has anyone found a ESP-S3 board that supports an SD card an is compatible with this Bridge software?

Hi Kirby,

For exactly that use case I am using the CYD, the "Cheap Yellow Display", a 2.8" ESP32 board with a resistive touch screen and a microSD slot. The bridge firmware runs nicely on it, so logging GPS to the SD card while riding should in theory be possible on this board.

Two honest caveats though. ESPHome has no built in file writer, so writing to the SD card needs either a community SD component or a small custom code block. And the coordinates would have to come from a separate GPS module on the board, because the Bosch LDI itself does not broadcast lat/lon over Bluetooth.

About your board: the web installer only offers the few prebuilt targets, so it reports anything else as unsupported. That does not mean your ESP32 S3 board cannot work. The bridge is a normal ESPHome external component, so it should compile for an S3 too if you write your own YAML with the correct pins for that board.

For my own setup I actually went a different way. I use WireGuard so the bridge sends all the live data straight into my Home Assistant while roaming, which I find more useful than a local file.

I am currently building live data onto the display itself as well, including weather and a rain forecast that the device fetches on its own. I will attach a few photos of the screen below. This display firmware is still alpha, I am actively working on it, and I will publish it on GitHub once it runs properly.

Cheers,
Volker




[quote="Xunil99, post:14, topic:1010148"]
Volker, Many thanks for your helpful advice - I'll let you know how I get on. Kirby

Thanks for your efforts.

What doesn’t work for me: if I leave the ESP32 powered on all the time sensors are not updated when the bike (Purion 200) is turned on nearby. Sensors only get updated when the bike is turned on first and the ESP32 powered on afterwards.

Thanks for the report, that is a useful data point.

I went through the ESP32 side’s BLE advertising code and I don’t think the boot order should matter from that side: it advertises undirected, indefinitely, and always resumes advertising immediately after any disconnect or failed connection attempt. So my best guess is that this isn’t a bug in the ESP32 firmware being “asleep” when it shouldn’t be, but rather that the Purion 200’s own reconnection logic (which is closed source, so I can’t verify this directly) may only actively scan for known peripherals for a short window around its own power on, rather than continuously. If that is the case, a peripheral that has already been advertising for a while at the default, fairly slow interval (about 1.28 seconds) could simply have a lower chance of being caught inside that short window than one that starts advertising right as the bike happens to be listening, which matches what you are seeing.

I made two changes that should help regardless of the exact cause:

  1. The ESP32 now advertises at a much shorter interval (20 to 40 ms instead of about 1.28 s) for the first 30 seconds after boot, after any disconnect, and after any failed connection attempt, then falls back to the slower interval to save power. This gives a bike doing only a brief scan window a much better chance of catching the advertisement, no matter which device powered on first.
  2. Added a watchdog that checks every 10 seconds whether the ESP32 is actually still advertising while disconnected, and restarts it if not. This is defensive: even though the existing code already restarts advertising after every disconnect, this catches any case where advertising might have silently stopped for some other reason during a long idle period.

Could you flash the updated version and test the exact scenario that failed for you (ESP32 powered on and left running, bike turned on afterward)? If it still does not connect, a debug log from the ESP32 covering that attempt (enable debug logging for the bosch_ebike_ldi component) would help a lot, specifically whether a GAP CONNECT line ever shows up at all. That would tell us whether the bike’s connection attempt is reaching the ESP32 and failing somewhere in the handshake, or never arriving in the first place.

In the meantime, powering the bike on first and the ESP32 afterward should keep working as a reliable workaround.

Thank you.
I flashed current firmware version OTA and will see if there is a change.

However the problem seems to be different. ESP32 is connected when Purion 200 is turned on (see picture). However sensors aren’t updated.

Thanks, that screenshot actually tells me a lot, and it points somewhere different than I expected.

Looking at the timestamps: the connection came up at 17:06:19, and “eBike In Motion” toggled between Erkannt and Frei several times over the next 7 minutes before disconnecting at 17:13:10. That only happens if live data is genuinely flowing end to end: encryption, service discovery, and the notification subscription all have to succeed for that sensor to change at all. So this is not a connection or pairing problem, something further down the chain is more selective about which fields update than it should be.

Two things would help me narrow this down:

  1. Which specific entities stayed on “Unknown” or an old value during that same window? Speed, cadence, rider power, battery SoC, and odometer are the ones I would check.
  2. Was the bike actually moved/ridden during that test, or just powered on and left stationary near the bridge?

The second question matters because speed, cadence, and rider power genuinely have nothing to report while the bike is standing still, so those staying blank would be expected, not a bug. Battery SoC and odometer are different: the bridge does one explicit read right after connecting that is supposed to return a full snapshot of every available field, so those two should show a value within a few seconds of connecting regardless of whether the bike moves.

If battery SoC or odometer are the ones stuck, a debug log covering that same connection window would help a lot (enable debug logging for the bosch_ebike_ldi component, then capture from connect to first NOTIFY_RX or the INITIAL_READ line). That would show me exactly what came back on that first read.

I only observed the battery SoC - which didn’t get updated. According to the odometer history it also did update only after re-powering the ESP32
Before the screenshot the bike was just moved maybe 5 meters.

Current situation: it seems to work with the new firmware. Sensor values are updated just when turning on the bike (e.g. state of light is shown correctly when changing).
But this time I compiled myself. Now also the WebUI is working (which didn’t when I first flashed using USB and the web installer). Could possibly be something wrong with the web installer binary image?