Specialized Turbo e-bike integration

I ride a Specialized Turbo e-bike and wanted to track battery health and ride stats in Home Assistant. Specialized’s own app is fine, but I wanted the data in my dashboards alongside everything else.

What it does

It connects to Specialized Turbo e-bikes (2017+ models with a TCU) over Bluetooth Low Energy and exposes telemetry as sensor entities. Turn the bike on, HA discovers it, enter the pairing PIN from the TCU, and you get sensors:

  • Battery: charge %, capacity, remaining Wh, health, temperature, voltage, current, charge cycles
  • Motor/ride: speed, rider power (watts), motor power, cadence, odometer, motor temp

How it works

The bike broadcasts over Bluetooth. Home Assistant picks it up, connects, and starts receiving telemetry. No polling, no cloud, everything stays local. The BLE protocol was reverse-engineered by Sepp62/LevoEsp32Ble (MIT). I wrote a Python library to handle the parsing, and the HA integration sits on top of that.

Install

Available through HACS as a custom repository:

  1. HACS > Integrations > three-dot menu > Custom repositories
  2. Add https://github.com/JamieMagee/ha-specialized-turbo as type Integration
  3. Download, restart HA
  4. Turn on the bike

You’ll need a Bluetooth adapter that HA can reach (USB dongle or an ESPHome Bluetooth proxy with active: true).

Links

If you have a Specialized Turbo, give it a try and let me know how it goes. Bug reports and PRs welcome.

1 Like

No HUD display for your helmet?

…maybe v2.0?

Hi, nice, thanks for the integration.

I wanted to try it with my 1gen Turbo Levo (2018). But I immediately get no_devices_found error when trying to add integration (after install via HACS). But I can see the bike advertised as near bluetooth device via HA.
Do you require the name of the bluetooth device to be named “Specialized Turbo”? Not sure if I am reading the code well.

This is how my bike is identified to HA:

Address: C6:1A:10:12:5E:48
Name: SPECIALIZED
Source: A0:88:69:62:22:F7

Advertisement data

Manufacturer data

525 0x02 0x86 0x57 0x00 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF

Service data

Service UUIDs

00001816-0000-1000-8000-00805f9b34fb

If needed I have LightBlue in my mobile phone and I am not afraid to tinker a little.

Thanks for trying the integration! Unfortunately your 2018 Gen 1 Levo uses a different BLE protocol than what this integration supports.

The integration looks for bikes advertising with Nordic’s manufacturer ID (0x0059 / 89) and a TURBOHMI magic string in the payload. Your bike advertises with manufacturer ID 525 and the standard Bluetooth Cycling Speed and Cadence service (0x1816), which is a completely different setup.

So it’s not a name check, it’s the underlying BLE protocol that doesn’t match.

Since your bike uses the standard CSC service UUID, you might be able to get basic speed/cadence data through a generic Bluetooth cycling integration for Home Assistant.

Thanks for creating this integration. Not sure I am ready to use it and would need to expand my system to include BLE so want to evaluate at a high level before heading down the path.

My wife and I both have turbo vados and love them. Some of the riding is at home and some away from home when we camp.

Questions:

  • As stated we have 2 bikes. Are 2 bikes possible at all with the integration? Load the integration twice and configure one each?

  • My wife uses her Apple Watch to track her rides, I don’t connect to anything but the specialized app and then its to set the upper charge level when based on use. Will that matter?

  • At different times when I got the bike I tried using my iPad vs my android phone to connect with the specialized app… that confused the bike, the apps or both. It seemed to like to associate with only one. Maybe that’s not an issue here?

  • Since I’m using HA green and both my USB ports are already occupied and a little far from the garage, Assuming I can use a ESP32 Plugged in somewhere hidden, in the middle, central house-ish. Assuming that would work if all the radios reach?

  • Follow on… we go for a ride, come back and put the bikes in the garage and set them to charge… the integration(s) should find them both on, talk to them and get the info?

Thanks for the interest! Let me try and answer each of your questions:

  1. Yes, two bikes work. You add the integration separately for each bike. They’re identified by MAC address, so HA treats them as independent devices.
  2. The integration holds an active GATT connection to the bike, and BLE only allows one at a time. If the Apple Watch is connected, HA can’t be, and vice versa. Probably fine if the watch only connects during rides and HA picks them up when they’re home charging.
  3. The “confused bike” thing you hit with iPad vs Android is the same problem. BLE pairing bonds to one host.
  4. The integration uses HA’s standard Bluetooth framework, so proxies work out of the box. Put the ESP32 within radio range of the garage and you’re set. This is exactly the setup I use as well.
  5. Yes, when you come home and plug in, the bikes will start advertising over BLE. HA sees the advertisements through the ESP32 proxy, reconnects automatically, and starts pulling telemetry.
1 Like

I’ve started upstreaming the integration:

Thanks. To my previous post I only copied what HA shows under Bluetooth advertisements - it showed only Bike-cadence service for some reason.

The manufacturer ID is indeed different. But I believe the connectivity should be almost the same as with newer Levo models. There are other advertised services which indeed output full data of the bike. My colleague even implemented this as sensor to our outdoor navigation Android app Locus Map.


When I will have some time and if I will be able to - I will try to play with your code a little to see what must be changed to connect even to my older Levo.

OK, I don’t understand python it seems. :disappointed_relieved: :smiley: Found mentioned manufacturer data in manifest.json, but don’t understand where is definition to which UUID to subscribe…

Can you please see from screenshot bellow if the data is same as on newer Levos? If I not mistaken that third payload from 18:38:50.146 contains data about assist mode I selected in that particular moment on stationary bike.

EDIT: Ah, found it. :smiley: I checked your protocol definition and at least battery_charge_percent and assist_level are defined on the “same place” as in my 1Gen Levo. (Don’t know the rest yet.) I also managed to alter _is_specialized_service_info within the code to be able to connect to my bike from HA. Unfortunately it wasn’t enough for integration to run, although Bluetooth stack in HA connected to the bike.

Hey, turns out you were right. The data format is the same between your 2018 Levo and the newer bikes. The only differences are the BLE UUIDs and the manufacturer ID in the advertisement data. Your screenshots confirmed it: the notification payloads on service 00000003-0000-4b49-4e4f-525441474947 use the exact same [sender][channel][data] message format, and fields like battery_charge_percent and assist_level sit at the same sender/channel positions.

I’m adding Gen 1 support to both the underlying library and the HA integration.

The Gen 1 UUID base is 000000xx-0000-4b49-4e4f-525441474947 (encodes “GIGATRONIK” reversed, presumably the TCU manufacturer for that generation). Same short IDs as Gen 2, just a different base.

I haven’t been able to test this on actual Gen 1 hardware since I don’t have one, so I’d appreciate it if you could try it out once I publish the update. Based on your screenshots, battery charge, assist level, speed, and cadence should all work. I’m less sure about whether your bike sends all the same channels. If some sensors show up as unavailable, that’s expected and we can figure out which fields your bike actually reports.

One thing I noticed: your bike’s pairing flow might differ from Gen 2. From what I’ve found, Gen 1 uses open GATT (no passkey), so you can probably leave the PIN field empty. Let me know how it goes.

EDIT: Release v0.2.1 · JamieMagee/ha-specialized-turbo · GitHub

Cool, thanks! Yep, my bike doesn’t need PIN and it connects properly with the version 0.2.1. But it seems like there are some differences in data retrieval after all.

Bellow are all entities which I saw that got populated by some data. Others like battery capacity never have anything other than unknown in my case. For example when connected for the first time (until reconnect) I saw only values for both temperatures entities, nothing else.

And of course as it is visible on screen, even sensors which are showing values are wrong. Those temperatures and voltage entities are valid though. You can see that assist level payload on screen from LightBlue above, so let me please know what can I check on my side.

BTW, is there some way to easily disconnect or reconnect bike via HA? (for example when I want to free bike BT connection for other app) I only managed to do it by disabling whole integration and then setup it again.

Thanks for testing this out! The bogus values you’re seeing (cadence, speed, rider power) are almost certainly 0xFF padding bytes being read as real data. Your Gen 1 bike pads notifications to 20 bytes with 0xFF, and the parser doesn’t know to ignore those yet. I’ve got a fix ready but want to confirm a couple things first.

Could you grab two things for me?

  1. Diagnostics download: Settings → Devices → your bike → three-dot menu → Download diagnostics. That’ll give me a JSON snapshot of exactly which fields populated and which stayed null.

  2. Debug logs: Add this to your configuration.yaml, restart, let the bike connect for a minute, then share the relevant log lines:

logger:
  logs:
    custom_components.specialized_turbo: debug

On your disconnect question: right now there’s no built-in way to disconnect without disabling the integration. I’ll look into adding that.

2 Likes