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.1.tar.gz (155.4 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.1-py3-none-any.whl (99.1 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: specialized_turbo-0.8.1.tar.gz
  • Upload date:
  • Size: 155.4 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.1.tar.gz
Algorithm Hash digest
SHA256 4945f82b694985685cd97a6f2f0c9eff07c43877ed1cc13c36f2514228d63c71
MD5 ef9406b27dd303c60ab84f36ce46de76
BLAKE2b-256 30458404d072061e41cd308561eceb6eb25d78d8166147f196009c86619cd248

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for specialized_turbo-0.8.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b69fbf95ea443917a799bf418ea6bdd6a55e5d803b285533bd74880476ef5296
MD5 4656f1cd21a2dde7c02657a7227a29ec
BLAKE2b-256 e7a2d1a7a79f967ba4acf23069f03899448c8e8d417cdf3fad044218f09004f0

See more details on using hashes here.

Provenance

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

0.8.2

2 files

This release

0.8.1 This release

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