thistineandClaude Opus 5 387128b517
Test / test (push) Failing after 14m14s
ci: re-run after the runner host restarted
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-05 22:19:48 +07:00
2026-09-05 22:03:16 +07:00
2026-09-05 22:03:16 +07:00
2026-09-05 22:03:16 +07:00
2026-09-05 22:03:16 +07:00
2026-09-05 22:03:16 +07:00

byd-exporter

Scrapes vehicle telemetry from the BYD cloud — the same API the BYD mobile app talks to — on a cron schedule, and serves the last reading as Prometheus metrics.

Go port of the protocol layer of pybyd 0.0.75, the client behind jkaberg/hass-byd-vehicle.

Why cron and not scrape-on-request

Every reading wakes the car's T-Box over the mobile network. The Home Assistant integration measures roughly 0.1 kWh/h of traction-battery drain at a 5-minute poll interval on a 60 kWh pack, and defaults to 5 minutes for that reason.

Letting Prometheus scrape the car directly would poll it every 15 seconds. So the schedule drives the BYD calls, /metrics just serves the last result, and Prometheus can scrape as often as it likes for free. Default schedule is hourly.

Run

export BYD_USERNAME='[email protected]' BYD_PASSWORD='...'
go run ./cmd/byd-exporter -once   # one scrape, print metrics, exit
go run ./cmd/byd-exporter         # serve :9838/metrics on the cron schedule

Configuration

All via environment variables.

Variable Default Notes
BYD_USERNAME — required. BYD app account (email or phone)
BYD_PASSWORD — required. BYD app password
BYD_SCHEDULE 0 * * * * 5-field cron expression, or @every 30m
BYD_LISTEN :9838 metrics listen address
BYD_REGION eu region code in dilinkappoversea-<region>.byd.auto — sg for APAC/Thailand, eu, au, br, jp, in, id, tr, kr, vn, mx, sa, om, kz, uz, no (Middle East/Africa)
BYD_BASE_URL derived from region full override
BYD_COUNTRY_CODE NL ISO country of the account
BYD_LANGUAGE en
BYD_TIME_ZONE UTC IANA zone sent to the API
BYD_POLL_ATTEMPTS 10 result polls before giving up on a sleeping car
BYD_POLL_INTERVAL 3s delay between those polls
BYD_SESSION_TTL 12h re-login interval
BYD_DEVICE_SEED the username see below

Device identity

BYD tracks the phone that logs in. The exporter derives a stable, realistic Android fingerprint (IMEI with a valid TAC prefix and Luhn digit, MAC, model) deterministically from BYD_DEVICE_SEED, so it looks like the same phone across restarts without persisting any state. Change the seed only if you want to present as a different device.

Running this alongside the Home Assistant integration on the same account means two "phones" polling one car. That works, but doubles the wake-ups.

Metrics

Every numeric field of the realtime payload is exported as byd_<snake_case_field>{vin,name}. There is no hand-maintained field list — BYD changes the payload between firmware versions, and new fields appear on their own.

byd_elec_percent{vin="LC0...",name="Sealion 7"} 82
byd_endurance_mileage{vin="LC0...",name="Sealion 7"} 391
byd_total_mileage{vin="LC0...",name="Sealion 7"} 14203
byd_left_front_tirepressure{vin="LC0...",name="Sealion 7"} 240
byd_vehicle_up{vin="LC0...",name="Sealion 7"} 1
byd_vehicle_info{vin="LC0...",name="Sealion 7",model="...",brand="BYD",plate="..."} 1
byd_scrape_success 1
byd_scrape_duration_seconds 21.4
byd_scrape_timestamp_seconds 1757000000

The trade-off: enum fields (doors, gear, charge state) arrive as their raw integers. Decode them in the dashboard — the meanings are in pybyd's realtime model and custom_components/byd_vehicle/sensor.py upstream.

byd_scrape_success 0 means the whole scrape failed (login or vehicle list); byd_vehicle_up 0 means one car didn't answer while the rest did.

Prometheus

Add to the agent's scrape_configs. The scrape interval is free — it reads a cached value, it does not touch the car.

  - job_name: 'byd'
    static_configs:
      - targets: ['byd-exporter:9838']

Deploy

Tag push builds and pushes the image via infras/workflow-templates:

git tag v0.1.0 && git push origin v0.1.0

Requires REGISTRY_USER and REGISTRY_TOKEN as repository secrets — the org-level secrets in infras do not reach a repo in the thistine namespace.

Then copy docker-compose.yml into infras/sg1-docker-composes as byd-exporter/docker-compose.yml, and add a deploy job to .gitea/workflows/release.yml calling bump-compose-version.yml to roll out each tag.

What this deliberately does not do

  • No MQTT. pybyd listens for a push and falls back to HTTP polling on timeout. This only polls — a scrape costs a handful of extra requests and takes a few seconds longer.
  • No GPS. /control/getGpsInfo is a second trigger-and-poll that wakes the car again. Add it if you want location badly enough to pay for it.
  • No remote control. Read-only by design; no lock, climate, or charge commands.
  • No typed model. The 1000-line pydantic model upstream is replaced by "export anything numeric".

Layout

cmd/byd-exporter/       flags, environment config, cron, HTTP server
internal/bangcle/       white-box AES envelope codec + the extracted tables
internal/byd/           API client: crypto, device fingerprint, endpoints
internal/byd/bydtest/   fake BYD cloud that speaks the real protocol
internal/metrics/       Prometheus text exposition
internal/exporter/      scrape orchestration and the /metrics handler
testdata/               fixture generator (Python, reads the reference client)

bangcle and jsonv are leaves; byd builds on them, exporter builds on byd and metrics, and cmd only wires things together.

Development

go test -race -cover ./...

Coverage sits around 90%. Three layers of test:

  • Wire-format fixtures. The bangcle ciphertext, the signing primitives and a complete login payload are compared byte-for-byte against pybyd's output for the same frozen clock and nonce. A renamed or reordered field fails here.
  • A fake cloud (internal/byd/bydtest) that unwraps the envelope, verifies the checkcode, decrypts encryData and encrypts its replies the same way the real backend does. Its server side is written independently of the client, so a shared bug cannot cancel itself out. Login, vehicle list, trigger-then-poll and the sleeping-car path all run through it end to end.
  • Unit tests for the payload-to-metric mapping and the JSON coercion, where BYD's habit of quoting numbers actually bites.

If BYD rotates the crypto tables, regenerate the fixtures with testdata/gen_fixtures.py and replace internal/bangcle/tables.bin from the new pybyd wheel.

Credit

Protocol reverse engineering is entirely @jkaberg's in hass-byd-vehicle / pybyd. This repo is a Go transcription of the read path.

S
Description
Prometheus exporter for BYD vehicle telemetry - Go port of the pybyd protocol, scraped on a cron schedule
Readme
621 KiB
0 Stars 1 Watchers 0 Forks
Languages
Go 99.6%
Dockerfile 0.4%