v4: multi-brand device ID
SDK v4 drops `bydDeviceId` for a brand-prefixed `deviceId` plus a separate `brand` field. The wire form is `<brand>:<native_id>`, e.g. `byd:BYDMCKLE0PARD8801`.
v4: multi-brand deviceId
SDK v4 is a hard cutover from the BYD-only identifier to a brand-prefixed canonical that scales across BYD, Geely, NIO, Tesla, and future adapters. No back-compat shims exist — this rename shipped pre-release; there is no v3 → v4 alias layer.
The new shape
{
"device_id": "byd:BYDMCKLE0PARD8801",
"brand": "byd"
// … rest of payload
}device_id(wire) /deviceId(TypeScript) — the full prefixed canonical, including the brand prefix.brand—'byd' | 'geely' | 'nio' | 'tesla'. Redundant with the prefix indeviceId, but carried separately so consumers don't have to parse the prefix on every read. Both must agree; the host rejects mismatches with HTTP 422.
The prefixed canonical is stored end-to-end:
- Database columns hold
byd:BYDMCKLE.... - JSON wire payloads transmit
byd:BYDMCKLE.... - Redis keys (e.g.
mqtt:auth:{deviceId}) carry the full prefixed form. - MQTT topics split brand from device_id at the segment level (see below); they are the only place the prefix is stripped.
What changed across the SDK
| v3.x (removed) | v4 |
|---|---|
bydDeviceId: string | deviceId: string |
byd_device_id (wire) | device_id (wire) |
| — | brand: 'byd' | 'geely' | 'nio' | 'tesla' (new, required) |
redactVin() / redactDeviceId() | redactDeviceId() |
Install.bydDeviceId, CarStatus.bydDeviceId | Install.deviceId, CarStatus.deviceId (+ .brand) |
HTTP routes
The /byd/ route prefix is gone. Every car-scoped route is
/api/v1/cars/{device_id}/…. The {device_id} segment is
URL-encoded since the prefix contains a colon:
GET /api/v1/cars/byd%3ABYDMCKLE0PARD8801/install
POST /api/v1/cars/byd%3ABYDMCKLE0PARD8801/pair
POST /api/v1/cars/byd%3ABYDMCKLE0PARD8801/unpair
GET /api/v1/cars/byd%3ABYDMCKLE0PARD8801/status
GET /api/v1/cars/byd%3ABYDMCKLE0PARD8801/location
POST /api/v1/cars/byd%3ABYDMCKLE0PARD8801/transferClients must encodeURIComponent(deviceId) before substituting into
a path. FastAPI on the host side decodes the segment automatically.
Admin query params changed too: ?byd_device_id=… → ?device_id=…
(same URL-encoding rule).
MQTT topics
Brand lives in the topic, not the payload — Mosquitto's ACL can't parse JSON, so the brand has to be a segment for per-brand authorization to work:
cars/{brand}/{device_id}/cmd
cars/{brand}/{device_id}/cmd/ack
cars/{brand}/{device_id}/status
cars/{brand}/{device_id}/telemetry
cars/{brand}/{device_id}/presence{brand} is the lowercase brand wire string (byd, geely, nio,
tesla). {device_id} on the topic is the un-prefixed native
ID (e.g. BYDMCKLE0PARD8801), because the brand is already encoded
in the preceding segment. Anywhere else (payloads, DB rows, Redis
keys, HTTP paths), the full prefixed form is mandatory.
Code update — TypeScript / React
// ❌ v3.x — removed
import type { Install, CarStatus } from 'i99dash';
function carHandle(install: Install) {
return install.bydDeviceId;
}
const tail = `****${status.bydDeviceId.slice(-4)}`;
const id = c.req.query('byd_device_id');
// ✓ v4
import type { Install, CarStatus } from 'i99dash';
function carHandle(install: Install) {
return install.deviceId; // 'byd:BYDMCKLE0PARD8801'
}
const tail = `****${status.deviceId.slice(-4)}`;
const id = c.req.query('device_id'); // URL-decoded by the frameworkCode update — calling your own backend
// ❌ v3.x — note both the removed callApi proxy and the old param name
client.callApi({
path: '/api/v1/fuel-stations',
query: { byd_device_id: ctx.activeCarId },
});
// ✓ current — plain fetch() to a declared manifest.network origin,
// with the renamed device_id param
await fetch(
`https://api.fuel.example.com/v1/stations?device_id=${encodeURIComponent(ctx.activeCarId)}`,
);Two changes rolled together here: callApi was removed in favour of
plain fetch() (declare the origin in manifest.network — see
Calling an external API), and
the device parameter was renamed byd_device_id → device_id.
ctx.activeCarId already returns the prefixed canonical
(byd:BYDMCKLE…) in v4 — no client-side rewriting needed.
JWT claims
If you forward identity to your own backend via the host-signed
JWT, the claim is now device_id (full prefixed form) and the new
brand claim is added alongside:
// payload.sub = userId
// payload.device_id = 'byd:BYDMCKLE0PARD8801' (was: byd_device_id)
// payload.brand = 'byd' (new in v4)
// payload.locale = 'en' | 'ar' | ...Validation
A v4 SDK validator splits the prefix and dispatches per-brand:
validate_device_id(device_id) -> (brand, native_id)— splits on the first:, validates the brand is a known enum value, and dispatches to the per-brand native-id validator.- BYD's native-ID regex:
^[A-HJ-NPR-Z0-9]{17}$(VIN charset). - Per-brand validators live under
app/domain/car/brands/<brand>/identity.pyon the host.
A device_id value with no prefix, an unknown brand prefix, or a
native ID that fails the per-brand regex returns HTTP 422.
Privacy posture is unchanged
The device ID is still device-identifying data. Continue masking it
in user-facing UI — see
Best practices › privacy. The
prefix doesn't change the masking guidance: render only the last 4
characters of the full prefixed form, e.g. byd:****8801.
Why no migration shim?
The platform is pre-release. Carrying a v3 → v4 alias layer would have meant: dual-name Pydantic fields, fallback URL routes, MQTT topic aliases, audit-log key translation, SharedPreferences migration on Dart. Each one is a future bug surface. Cutting cleanly costs less than maintaining the bridge.
Anything else?
If you find a v4 surface where deviceId is missing, brand is
absent, or the wire form is un-prefixed, please
open an issue.