Skip to main content

Async Python client for the A Better Routeplanner (ABRP) / Iternio telemetry API

Project description

aioabrp

Async Python client for the A Better Routeplanner (ABRP) / Iternio telemetry API. The library mirrors ABRP's API points 1:1 and has no Home Assistant dependency: a stateless request/response AbrpClient (garage, vehicle catalog, vehicle-model display metadata, one-shot telemetry snapshot) and a resilient TelemetryStream (server-sent events with reconnect/backoff/watchdog) that delivers extracted, typed metric values — never raw wire dicts — to consumer callbacks. Authentication is injected: the consumer owns the token lifecycle and hands the library a fresh access token via AbstractAuth (or the fixed-token StaticAuth convenience).

Installation

pip install aioabrp

Requires Python ≥ 3.14. The only runtime dependency is aiohttp.

Usage

A runnable standalone example (fill in a real partner API key and access token before running — the calls below hit the live API):

import asyncio

import aiohttp

from aioabrp import (
    AbrpClient,
    ConnectionEvent,
    StaticAuth,
    Telemetry,
    TelemetryStream,
)

API_KEY = "your-iternio-partner-api-key"
ACCESS_TOKEN = "your-abrp-access-token"


def on_update(vehicle_id: int, telemetry: Telemetry) -> None:
    for metric, mv in telemetry.items():
        print(f"vehicle {vehicle_id}: {metric} = {mv.value!r} (time={mv.time})")


def on_connection_change(event: ConnectionEvent) -> None:
    print(f"connection: {event.state.name} (reason={event.reason})")


async def main() -> None:
    async with aiohttp.ClientSession() as session:
        auth = StaticAuth(ACCESS_TOKEN)
        client = AbrpClient(session, API_KEY, auth)

        vehicles = await client.async_get_vehicles()
        for vehicle in vehicles:
            print(f"{vehicle.vehicle_id}: {vehicle.name or vehicle.vehicle_model}")

        stream = TelemetryStream(
            session,
            API_KEY,
            auth,
            vehicle_ids=[v.vehicle_id for v in vehicles],
            on_update=on_update,
            on_connection_change=on_connection_change,
        )
        await stream.start()
        try:
            await asyncio.sleep(600)  # stream telemetry for ten minutes
        finally:
            await stream.stop()


if __name__ == "__main__":
    asyncio.run(main())

Get your own API key

The api_key constructor argument is an Iternio partner API key, not a per-user credential. If you are building your own consumer, obtain your own key from Iternio — see the Iternio Telemetry API documentation or contact Iternio for a partner API key. The per-user access token comes from your own auth flow and is handed to the library through AbstractAuth.

Consumer contracts

These behaviors are pinned by the test suite; consumers may rely on them.

Metrics and units

The library surfaces every metric ABRP's v2 telemetry OutputPoint exposes — a 1:1 mirror of the 26 wire fields — as members of the Metric enum, each carrying a MetricValue[T]. Values keep the raw ABRP wire scale; the library performs no unit conversion (rendering is consumer policy):

Metric(s) value type Unit
soc, soh float percent (wire frac surfaced ×100; soh not clamped)
power, hvac_power float W
voltage float V
current float A
soe, battery_capacity, charging_energy_added float Wh
odometer, range, elevation float m
speed, calibrated_max_speed float m/s
heading float degrees
battery_temperature, cabin_set_point, cabin_temperature, external_temperature float °C
calibrated_ref_cons float Wh/km
speed_factor float dimensionless multiplier
charging_state ChargingState closed enum
driving_state DrivingState closed enum (gear)
location Location lat/lon
calibrated_confidence tuple[float, ...] 1- or 4-element confidence vector (opaque; not interpreted)
map_info MapInfo struct: region (enum), country_3, address, speed_limit_ms (m/s), is_free_speed_zone

Notes:

  • A frame is a sparse delta: Telemetry.items() yields only the metrics present in that frame, and may yield any of the 26. Consumers that map Metric onto something else (e.g. an entity table) must tolerate members they don't handle rather than assume a fixed set.
  • A categorical wire member the library doesn't recognize (an unknown charging_state / driving_state) omits that metric rather than leaking a raw string, and logs one warning per stream instance.
  • map_info subfields are each independently optional; an unrecognized region degrades to None while the rest of the struct survives. A map_info block with no usable subfield is omitted entirely.

Callbacks

  • on_update and on_connection_change are synchronous callbacks, delivered on the event loop that ran start(). They must be non-blocking — a slow callback stalls the stream (and every other stream on the same loop).
  • A raising callback is logged with its traceback and swallowed; the stream continues (frame loss beats stream death).

Connection events

  • CONNECTED fires on the first frame of a connection, not on the HTTP connect — a connection that opens but never produces a frame keeps reading as down until proven healthy.
  • DISCONNECTED is steady-state, not exceptional: the ABRP server unilaterally closes idle streams at roughly 200 s and the stream reconnects with backoff. Do not treat a DISCONNECTED event as an outage. Events are status reports, not strict state transitions — a DISCONNECTED MAY arrive before the first CONNECTED (for example when the very first connection attempt fails).
  • AUTH_FAILED is terminal: the stream stops itself and will not retry. The consumer decides whether/when to restart with fresh credentials.
  • A transient (non-AbrpAuthError) failure from the token getter emits no ConnectionEvent at all (debug log only) — no connection attempt was made, so consumers keep seeing the last known state during a token-endpoint outage.

Lifecycle

  • stop() is idempotent and cancel-based (never a graceful join). No callbacks fire after stop() returns.
  • stop() propagates the caller's own cancellation: if you wrap it in asyncio.wait_for(stream.stop(), timeout) and that times out, the resulting TimeoutError means the stream may still be running — the stop was interrupted, not completed.
  • start() after stop() restarts the stream.

Monotonicity gate

Each stream keeps one piece of state: a per-(vehicle_id, Metric) map of the last adopted block timestamp.

  • A block whose time is strictly older than the last adopted time for that (vehicle, metric) is dropped.
  • An equal-time block re-emits by design: every reconnect re-delivers a full-state snapshot with unchanged block times, and consumers rely on that backfill.
  • A time-less block (missing/malformed/naive time) is adopted and clears the gate for that metric — it carries no ordering claim, so it also stops gating subsequent values.
  • A block whose time is in the future is rewritten to "now" before delivery (and before gating), so a clock-skewed upstream stamp can neither be delivered to the consumer nor become an unreachable high-water mark that silently stalls the metric. AbrpClient.async_get_current_telemetry applies the same clamp (it does not gate).
  • Known limitation: a legitimately backdated server correction (an older timestamp that really is a newer truth) is suppressed for the stream's lifetime.

The gate can be pre-warmed across process restarts: pass TelemetryStream(..., seed=Mapping[int, Mapping[Metric, datetime]]) — a per-vehicle map of metric to its last wire-block time (e.g. derived from the consumer's last persisted snapshot) — and the stream seeds its high-water marks from those times, each clamped not-future. Only times are needed; no typed values. The clock is the aioabrp._clock._now seam (the monkeypatch target in tests).

Logging

Enable the aioabrp logger at DEBUG for triage. Connect/disconnect and reasons log at INFO, watchdog stalls at WARNING, per-frame activity at DEBUG. Frames are logged as keys and sizes only — frame bodies, header values, and tokens (including GPS coordinates, the map_info street address, and other PII) are never logged. The unknown-enum warnings carry only the unrecognized member token, never any payload content. Pass name= to TelemetryStream to prefix its log lines when running multiple streams.

Development

This project uses uv:

uv sync
uv run ruff format . && uv run ruff check . && uv run mypy && uv run pytest

Note: the locked dev environment constrains aiohttp<3.14 because the latest aioresponses release is incompatible with aiohttp ≥ 3.14 (aioresponses#289); the published runtime dependency stays unpinned.

Releases

Commits follow Conventional Commits; there is no CHANGELOG file and no version literal in the source — the version is derived from the git tag at build time (hatch-vcs), so feature PRs never carry a version bump.

To cut a release, a maintainer runs the Release workflow manually (workflow_dispatch, from main). It runs the quality gate, then derives the next version from the Conventional Commits since the last tag with git-cliff (feat → minor, fix → patch, breaking → major) — or uses the optional version input (X.Y.Z) to force a specific version; leave it blank to auto-derive. The workflow tags vX.Y.Z, builds the sdist + wheel (hatch-vcs reads the version from the tag), publishes to PyPI via Trusted Publishing (OIDC), then pushes the tag and creates the GitHub Release with git-cliff notes. The tag is pushed only after a successful publish, so a failed run leaves no dangling tag.

License

MIT — see LICENSE.

Project details


Download files

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

Source Distribution

aioabrp-0.3.0.tar.gz (107.0 kB view details)

Uploaded Source

Built Distribution

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

aioabrp-0.3.0-py3-none-any.whl (41.1 kB view details)

Uploaded Python 3

File details

Details for the file aioabrp-0.3.0.tar.gz.

File metadata

  • Download URL: aioabrp-0.3.0.tar.gz
  • Upload date:
  • Size: 107.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for aioabrp-0.3.0.tar.gz
Algorithm Hash digest
SHA256 d702c68b4960cfd860161deeefb9c7bc80e9bd0b1c6960f4006f4fbcc53a9923
MD5 0b1f5c43cf8bd0d565897ff6c04c52f7
BLAKE2b-256 a62cdd702223ddcbe87391a33f2b54d13b5a5b22883c05aff55d7c70d9e7cf42

See more details on using hashes here.

Provenance

The following attestation bundles were made for aioabrp-0.3.0.tar.gz:

Publisher: release.yml on mtandersson/aioabrp

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

File details

Details for the file aioabrp-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: aioabrp-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 41.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for aioabrp-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 45f9e235d6f4f766962f2805cf6bac9ff41d9859f7b1a25224d4921704c0f9c6
MD5 21c8342503c837da94736a1e45c2f956
BLAKE2b-256 53bbc5ab68ba0f3682be268a4e0656c26e3f4b872707a0d67d935911207fc6f9

See more details on using hashes here.

Provenance

The following attestation bundles were made for aioabrp-0.3.0-py3-none-any.whl:

Publisher: release.yml on mtandersson/aioabrp

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

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page