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.2.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.2-py3-none-any.whl (99.2 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: specialized_turbo-0.8.2.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.2.tar.gz
Algorithm Hash digest
SHA256 4a207eeeb6692e713bb21b89878999e6664279faba8d3a6f05641ddc5d924254
MD5 beedfe1a4d064a695bcb468094b05a3d
BLAKE2b-256 2598ff6921cd831685595ce8b88549ad40e4319a30efe93b64fe9d89319dcb35

See more details on using hashes here.

Provenance

The following attestation bundles were made for specialized_turbo-0.8.2.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.2-py3-none-any.whl.

File metadata

File hashes

Hashes for specialized_turbo-0.8.2-py3-none-any.whl
Algorithm Hash digest
SHA256 0fb67da343e5a9998b2ec9b31ece7ba1cbee6cbee2fc4445d1b3a7bec5478749
MD5 50cf9aab64668e8732fb04af9bada167
BLAKE2b-256 af24116cb87814c470f581736ea0b836b1e0e085b5aa18f1b4a2aa1c7f5e19f4

See more details on using hashes here.

Provenance

The following attestation bundles were made for specialized_turbo-0.8.2-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

0.8.3

2 files

This release

0.8.2 This release

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