Co-Authored-By: Claude Opus 5 <[email protected]>
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/getGpsInfois 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, decryptsencryDataand 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.