![Logo](admin/byd-vehicle.png) # ioBroker.byd-vehicle [![Test and Release](https://github.com/piotr-siedlak/iobroker.byd/workflows/Test%20and%20Release/badge.svg)](https://github.com/piotr-siedlak/iobroker.byd/actions) Native ioBroker adapter for **BYD vehicles** via the BYD cloud - no Home Assistant, no Python, no Docker, no third-party bridge. > **Unofficial.** Not affiliated with, endorsed by or supported by BYD. It uses reverse-engineered cloud interfaces > (see [docs/UPSTREAM_ANALYSIS.md](docs/UPSTREAM_ANALYSIS.md)) that BYD can change or block at any time. Using it may log you out of the > BYD app. **This version has never been tested against a physical car** (see [Test status](#test-status)). Adapter id: `byd-vehicle` (npm `iobroker.byd-vehicle`). It is deliberately **not** called `byd` so it cannot clash with the existing [TA2k/ioBroker.byd](https://github.com/TA2k/ioBroker.byd). Polish version: [README_PL.md](README_PL.md). ## What it does * Login with your BYD account, region from the selected country (Poland and the other EU countries use the European server). * Discovers **all vehicles** of the account (several cars, several adapter instances/accounts are supported without state bleed). * Typed, role-correct telemetry: battery SoC and range, odometer, speed, interior/outside temperature, tire pressures (bar/kPa), doors, locks, windows, A/C and seat states, charging state, smart-charge schedule, energy statistics, GPS position, warning lights, fuel values (hybrids only), plus freshness/quality information for every vehicle. * **Remote commands** (off by default): lock/unlock, A/C start/stop, seat and steering-wheel heating/ventilation, battery heating, find car / flash lights, close/vent windows, trunk, start charging now, smart-charge schedule. * Optional use of BYD's own **push channel (MQTT)** for faster results; falls back to HTTP polling. * Conservative polling policies, request budget, back-off and a manual refresh that cannot wake the car in a loop. * Redacted logs and a one-click support export (no secrets, VIN or location). Not provided (nothing is faked): battery health (SoH), charging power/current/cost, route maps, V2H/V2G, stop-charging (the cloud call is a known no-op), one-click shutdown. See [docs/KNOWN_LIMITATIONS.md](docs/KNOWN_LIMITATIONS.md). ## Requirements * ioBroker with js-controller ≥ 6.0.11, Admin ≥ 7.0.23, Node.js ≥ 20. * A BYD account that owns the car or has it shared. **Recommended: create a dedicated second BYD account, share the car with it and use that account here.** Concurrent sessions of the same account (phone app and cloud) can invalidate each other. A dedicated account used for remote commands needs **its own control PIN** (set in the BYD app for that account). ## Installation The adapter is not (yet) in the official ioBroker repository. **From GitHub** (Admin → Adapters → "Install from custom URL" / "Aus eigener URL installieren"): ```text https://github.com/piotr-siedlak/iobroker.byd ``` **From a local build** (used for testing; replace the path): ```bash git clone https://github.com/piotr-siedlak/iobroker.byd.git cd iobroker.byd npm ci npm run build npm pack # creates iobroker.byd-vehicle-.tgz cd /opt/iobroker iobroker url /path/to/iobroker.byd-vehicle-.tgz iobroker add byd-vehicle ``` ## Configuration (Poland / Europe) 1. **Account tab:** account e-mail, password, **Country = Poland**. API region stays *Automatic*; language *Automatic*. Save, then press **Test saved connection** - you should see `Login OK. n vehicle(s) found` with masked VINs. 2. **Polling tab:** keep *Balanced* unless you have a reason not to (see [Polling](#polling-and-battery-protection)). 3. **Vehicles tab:** nothing to do. Add a row (VIN) only to disable a car, rename it or override intervals. 4. **Remote commands tab (optional):** switch on *Enable remote commands*, enter the **6-digit control PIN** of the BYD account you use here (this is not the account password), then enable only the categories you need. Unlock, open windows and trunk are separate, clearly marked opt-ins. 5. Check the instance log: you should see `Vehicle LC0***********1234 (…) added; realtime every 900 s`. ### Why a separate control PIN? The control PIN protects commands in the BYD cloud and is per account. The adapter verifies it **once per start**; if BYD rejects it, commands stay disabled until you fix the PIN and restart the instance (repeated wrong PINs can lock remote control for the day). ## Object tree `` is the vehicle's VIN (sanitised for use as an id). Masked in logs, and optionally published in `info.vin`. ```text byd-vehicle.0 info.connection · info.authenticated · info.apiRegion · info.lastSuccessfulLogin · info.lastSuccessfulUpdate info.lastError (redacted) · info.rateLimited · info.mqttConnected · info.upstreamVersion · info.requests.* account.vehicleCount · account.refreshVehicles (button, max once per 10 min) vehicles. info.* model, brand, energy type, alias, capabilities (JSON), present status.* lastPolledAt, lastCloudTimestamp, dataAgeSeconds, isStale, source (mqtt|http|trigger), quality, heldFields, lastError battery.* soc (%), range (km), power (W, experimental), heating fuel.* hybrids/ICE only driving.* odometer, speed, powerOn, consumption.* (kWh/100km, L/100km) charging.* state, timeToFullMinutes, schedule.* (cloud), energy.* climate.* interior/exterior temperature, hvacOn, circulation, seats.* body.doors.* · body.locks.* · body.windows.* tires..pressureBar|pressureKpa|pressureRaw|plausible|status location.* latitude, longitude, heading, lastFix (only if GPS is enabled) diagnostics.* warning lights, sentry mode, raw codes (opt-in) derived.* isCharging, isPluggedIn (+ confidence), allLocked, anyDoorOpen, anyWindowOpen, anyWarning commands.* only if enabled and supported (see below) ``` Examples: `byd-vehicle.0.vehicles..battery.soc` = `63` (`%`), `….tires.leftFront.pressureBar` = `2.5`, `….body.locks.leftFrontUnlocked` = `false` (ioBroker role `sensor.lock`: `true` means *unlocked*), `….derived.isPluggedIn` = `true` with `….derived.pluggedConfidence` = `low`. ### `ack` and trigger semantics * **Telemetry** is read-only and always written with `ack: true`. A value that becomes unavailable is set to `null`; the `status.*` states tell you how old the data is (`dataAgeSeconds`, `isStale`) and whether a value is being held (`heldFields`). * **Commands** are `write: true`, but only writes with `ack: false` are processed. When the command finishes (any outcome) the trigger is acknowledged with `ack: true`; that acknowledgement is not re-executed. Setpoints (temperature, duration, schedule fields) only store a value - **editing a parameter never sends a command**. * **Requested vs. confirmed.** `commands.*` hold what you asked for, `climate.*`/`charging.schedule.*`/`body.*` hold what the cloud reports. Nothing is set optimistically; after a successful command one read-back refresh runs. * Results are in `commands.status.*`: `lastCommand`, `lastResult` (`pending → sent → acknowledged → succeeded | failed | timed_out | unknown | rejected`), `lastError`, `pending`, `requestedAt`, `acceptedAt`, `completedAt`, `retryCount` (always `0`). ### Commands | State (under `vehicles..commands.`) | Purpose | |---|---| | `locks.lock`, `locks.unlock`* | lock / unlock | | `climate.targetTemperature`, `climate.durationMinutes`, `climate.start`, `climate.stop` | A/C | | `seats.driverHeat`, `driverVentilation`, `passengerHeat`, `passengerVentilation` (0 off / 1 low / 2 high), `seats.steeringWheelHeat` | seats | | `battery.heat` | battery heating | | `finder.findCar`, `finder.flashLights` | find car | | `windows.close`, `windows.openVent`* | windows (`openVent` only vents to ~10 %) | | `trunk.open`*, `trunk.close`* | trunk | | `charging.start`, `charging.schedule.startTime/endTime/repeat/enabled`, `charging.schedule.apply` | charging | | `refresh.realtime`, `refresh.gps`, `refresh.slow` | manual refresh (≥ 60 s apart per stream) | \* security-sensitive, separate opt-in. A command state exists only if (1) remote commands are enabled, (2) the PIN was verified, (3) the category is allowed in the config and (4) the vehicle's capability list contains it. A command is **sent once and never retried**. If the transport fails while sending, the result is `unknown` - check the car before you try again. Blockly/JavaScript example: ```js setState('byd-vehicle.0.vehicles..commands.climate.targetTemperature', 22); // only stores the setpoint setState('byd-vehicle.0.vehicles..commands.climate.start', true); // sends the command ``` ## Polling and battery protection **Every poll can wake the car and drain the 12 V battery.** Defaults are more conservative than the Home Assistant integration (300 s). | Policy | Telemetry | GPS | Charging + energy | HTTP requests per car and day* | |---|---|---|---|---| | Battery saver | 30 min | 60 min | 2 h | 108 / 180 / 828 | | **Balanced (default)** | 15 min | 30 min | 1 h | 216 / 360 / 1 656 | | Frequent | 5 min | 10 min | 30 min | 600 / 1 032 / 4 920 | | Custom | 1 min … 8 h | 1 min … 8 h | 1 min … 8 h | - | \* GPS and A/C polling included. *Min* assumes the push channel delivers every answer, *worst case* assumes HTTP polling (10 polls per read). Further protections: start-up staggering (no burst at start), ±10 % jitter, exponential back-off (60 s … 30 min, honouring server hints), a local request budget (30/min by default) and concurrency limit, no overlapping runs per stream, a manual refresh limit (60 s) and no refresh loop after commands (one bounded read-back). The state of the data is always visible in `status.*`. ## Privacy and security * Credentials and the PIN are stored encrypted by js-controller (`encryptedNative`/`protectedNative`), never logged, never put in URLs. * TLS certificate verification is always on; only `dilinkappoversea-*.byd.auto` over https is accepted (a developer override exists). * Location is sensitive: GPS can be switched off (nothing is requested or published). Logs and the support export mask VINs and remove coordinates. See [docs/SECURITY.md](docs/SECURITY.md) for the threat model. * ioBroker users who can write states can control the car when commands are enabled. Restrict Admin / object access accordingly. ## Test status | Layer | Status | |---|---| | Protocol port (crypto, signing, envelopes, parameters) | byte-identical to pyBYD on golden vectors generated by the Python code | | Client (login, sessions, back-off, trigger/poll, MQTT, commands) | tested against an in-process fake BYD cloud with real envelopes | | Mapping, polling, command lifecycle, object tree | unit tests with fixtures | | **Real BYD cloud / real car** | **not tested** - use the checklist in [docs/TEST_PLAN.md](docs/TEST_PLAN.md) | ## Development ```bash npm ci npm run build # compile TypeScript to build/ npm test # unit tests + package validation npm run lint npm run check # type check npm run docs:matrix # regenerate docs/FEATURE_MATRIX.md ``` Simulation mode (Admin → *Privacy and debug* → developer options) runs the whole adapter against two synthetic cars without any network access. Documentation: [ARCHITECTURE](docs/ARCHITECTURE.md) · [API mapping](docs/API_MAPPING.md) · [Feature matrix](docs/FEATURE_MATRIX.md) · [Security](docs/SECURITY.md) · [Troubleshooting](docs/TROUBLESHOOTING.md) · [Test plan](docs/TEST_PLAN.md) · [Known limitations](docs/KNOWN_LIMITATIONS.md). ## Credits and license Protocol knowledge and the Bangcle tables come from [jkaberg/pyBYD](https://github.com/jkaberg/pyBYD) and [jkaberg/hass-byd-vehicle](https://github.com/jkaberg/hass-byd-vehicle) (MIT); the `chargeState` observation from [TA2k/ioBroker.byd](https://github.com/TA2k/ioBroker.byd) (MIT). See [NOTICE](NOTICE). ## Changelog ### **WORK IN PROGRESS** - (Piotr Siedlak) initial version: read-only telemetry, polling policies, optional MQTT push, opt-in remote commands, smart-charge schedule - (Piotr Siedlak) review pass: command-acknowledgement, polling-lifecycle and session fixes; support export stored in `info.supportExport` ## License MIT License Copyright (c) 2026 Piotr Siedlak See [LICENSE](LICENSE).