Skip to main content

specialized-turbo

Python library for talking to Specialized Turbo e-bikes (Vado, Levo, Creo) over Bluetooth Low Energy. Reads speed, power, cadence, battery, motor temp, odometer, assist level, range. Can also write settings like assist level and acceleration.

Async, built on bleak. Includes a CLI. Protocol docs in docs/protocol.md.

Installation

pip install specialized-turbo

For automatic key retrieval on newer encrypted bikes:

pip install "specialized-turbo[cloud]"

Quick start

Stream telemetry

import asyncio
from specialized_turbo import SpecializedConnection, TelemetryMonitor


async def main():
    async with SpecializedConnection("DC:DD:BB:4A:D6:55") as conn:
        monitor = TelemetryMonitor(conn)
        await monitor.start()

        async for msg in monitor.stream():
            print(f"{msg.field_name} = {msg.converted_value} {msg.unit}")


asyncio.run(main())

Newer bikes advertise an HMI serial/hardware pair and require a wrapped key from Specialized's keystore API. Supply either a provider or a wrapped key:

from specialized_turbo import SpecializedConnection
from specialized_turbo.cloud import SpecializedCloudClient

async with SpecializedCloudClient() as cloud:
    await cloud.login("rider@example.com", password)
    async with SpecializedConnection(
        "DC:DD:BB:4A:D6:55",
        key_provider=cloud,
    ) as conn:
        ...

# Or avoid account authentication by supplying the 64-character wrapped key.
async with SpecializedConnection(address, wrapped_key=wrapped_key) as conn:
    ...

The password is not persisted by the library. Provider users control token storage; manual-key users can operate without cloud access after obtaining the per-bike wrapped key.

Read the snapshot

async with SpecializedConnection("DC:DD:BB:4A:D6:55") as conn:
    monitor = TelemetryMonitor(conn)
    await monitor.start()
    await asyncio.sleep(5)

    snap = monitor.snapshot
    print(f"Speed: {snap.motor.speed_kmh} km/h")
    print(f"Battery: {snap.battery.charge_pct}%")
    print(
        f"Power: {snap.motor.rider_power_w} W (rider) + {snap.motor.motor_power_w} W (motor)"
    )
    print(f"Cadence: {snap.motor.cadence_rpm} RPM")
    print(f"Assist: {snap.motor.assist_level}")

Query a single value

from specialized_turbo import SpecializedConnection, Sender, BatteryChannel

async with SpecializedConnection("DC:DD:BB:4A:D6:55") as conn:
    msg = await conn.request_value(Sender.BATTERY, BatteryChannel.CHARGE_PERCENT)
    print(f"Battery: {msg.converted_value}%")

Write commands

async with SpecializedConnection("DC:DD:BB:4A:D6:55") as conn:
    await conn.set_assist_level(2)  # TRAIL
    await conn.set_acceleration(50.0)  # 50%
    await conn.set_shuttle(25)
    await conn.set_assist_percentage(0, 35)  # ECO = 35%

CLI

Scan for bikes:

specialized-turbo scan
specialized-turbo scan --timeout 15

Retrieve the wrapped key required by a newer encrypted bike:

specialized-turbo fetch-key DC:DD:BB:4A:D6:55 \
  --email rider@example.com

The password is prompted without echo. By default, stdout contains only the 64-character wrapped key, ready for --wrapped-key or the Home Assistant manual-key flow. Use explicit HMI identifiers when the bike is not nearby:

specialized-turbo fetch-key \
  --hmi-hardware 3.2.1 \
  --hmi-serial 123456789 \
  --email rider@example.com \
  --json

fetch-key requires specialized-turbo[cloud]. It does not print the final unwrapped AES key.

Stream telemetry:

specialized-turbo telemetry DC:DD:BB:4A:D6:55 --email rider@example.com
specialized-turbo telemetry DC:DD:BB:4A:D6:55 --wrapped-key "$WRAPPED_KEY"
specialized-turbo telemetry DC:DD:BB:4A:D6:55 --format json
specialized-turbo telemetry DC:DD:BB:4A:D6:55 --duration 30

Read a single value:

specialized-turbo read list                                             # show available fields
specialized-turbo read battery_charge_percent DC:DD:BB:4A:D6:55
specialized-turbo read speed DC:DD:BB:4A:D6:55 --format json

Write a value:

specialized-turbo write list                                            # show writable fields
specialized-turbo write assist_level 2 DC:DD:BB:4A:D6:55  # set to TRAIL
specialized-turbo write acceleration 50 DC:DD:BB:4A:D6:55 # 50% sensitivity

Dump GATT services (debugging):

specialized-turbo services DC:DD:BB:4A:D6:55

Capture complete TCX writes and notifications for protocol debugging:

specialized-turbo capture DC:DD:BB:4A:D6:55 --duration 60 > tcx-capture.tsv

Available fields

Field Unit Writable Description
battery_capacity_wh Wh Total battery capacity
battery_remaining_wh Wh Remaining energy
battery_health % Battery health
battery_temp °C Battery temperature
battery_charge_cycles cycles Number of charge cycles
battery_voltage V Battery voltage
battery_current A Battery current draw
battery_charge_percent % State of charge
rider_power W Rider pedal power
cadence RPM Pedaling cadence
speed km/h Current speed
odometer km Total distance
assist_level -- yes OFF / ECO / TRAIL / TURBO
motor_temp °C Motor temperature
motor_power W Electric motor power
peak_assist % ECO / TRAIL / TURBO percentages
shuttle -- yes Shuttle mode value (0-100)
wheel_circumference mm yes Wheel circumference setting
assist_lev1_pct % yes ECO assist percentage
assist_lev2_pct % yes TRAIL assist percentage
assist_lev3_pct % yes TURBO assist percentage
fake_channel -- Bit-coded internal channel
acceleration % yes Acceleration sensitivity

TCX2+ bikes have additional fields (range, altitude, gradient, calories, system temperature, and more). See docs/protocol.md for the full list.

Protocol support

Five protocol variants exist:

Protocol Message format Encryption
TCU1 [sender][channel][data] None
TCX1 Same as TCU1, using TURBOHMI GATT UUIDs None
TCX2 2-byte parameter ID + payload + clear CRC-16 Optional AES-128-CTR
TCX3 Same as TCX2 Optional AES-128-CTR
TCX4 Same as TCX2 Optional AES-128-CTR

TCX1 shares the TURBOHMI UUID family with TCX2/3/4 but uses legacy write-then-read queries. The connected service structure distinguishes it from TCX2+. The BLEProfile enum (TCU1 / TCX) controls which GATT UUID family to use.

For encrypted bikes, only packet bytes 2–17 are encrypted. The parameter ID and CRC remain clear. The wrapped per-bike key comes from Specialized's cloud, while SYSTEM_GET_NEW_VI supplies a fresh session IV at connection time.

See docs/protocol.md for the full spec.

Pairing

The connection requests authenticated pairing for bikes using the TURBOHMI UUID family. Passkey entry and numeric comparison are handled by the active Bluetooth backend or its pairing agent; Bleak cannot accept a passkey value directly.

If the backend cannot present or confirm the pairing prompt, pair through the operating system first and reconnect. The deprecated --pin and pin= arguments are accepted for compatibility but their values are ignored.

Development

uv sync --extra dev
uv run pytest

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

specialized_turbo-0.8.3.tar.gz (155.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

specialized_turbo-0.8.3-py3-none-any.whl (99.2 kB view details)

Uploaded Python 3

File details

Details for the file specialized_turbo-0.8.3.tar.gz.

File metadata

  • Download URL: specialized_turbo-0.8.3.tar.gz
  • Upload date:
  • Size: 155.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for specialized_turbo-0.8.3.tar.gz
Algorithm Hash digest
SHA256 d5aa7c56c863847eea2b0d0fbb0d3f8e89a20a897d2663b3bac84f9bde9496fb
MD5 de6e951923a97e53454f38558a130056
BLAKE2b-256 111890f18cd980bc3ddd32cac399890e574d8506353702ae558c0323c77e87f2

See more details on using hashes here.

Provenance

The following attestation bundles were made for specialized_turbo-0.8.3.tar.gz:

Publisher: publish.yml on JamieMagee/specialized-turbo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file specialized_turbo-0.8.3-py3-none-any.whl.

File metadata

File hashes

Hashes for specialized_turbo-0.8.3-py3-none-any.whl
Algorithm Hash digest
SHA256 d278e33a6ce091bbaf77d60e0589dc20901ae909eeb9c72443445fd081e4e4ed
MD5 2aec019f9efde286605b41442932d628
BLAKE2b-256 ed9d9b8e5bcd3d54173e7164ad10ba1334bcae534c3d40bcd76da8a1f5ca63e1

See more details on using hashes here.

Provenance

The following attestation bundles were made for specialized_turbo-0.8.3-py3-none-any.whl:

Publisher: publish.yml on JamieMagee/specialized-turbo

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.8.3 This release

2 files

0.8.2

2 files

0.8.1

2 files

0.8.0

2 files

0.7.9

2 files

0.7.8

2 files

0.7.7

2 files

0.7.6

2 files

0.7.5

2 files

0.7.4

2 files

0.7.3

2 files

0.7.2

2 files

0.7.1

2 files

0.7.0

2 files

0.6.1

2 files

0.6.0

2 files

0.5.0

2 files

0.4.2

2 files

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

0.2.5

2 files

0.2.4

2 files

0.2.3

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page