BMW CarData directly via MQTT

You may be aware that BMW broke the ConnectedDrive HA integration. Instead, you’re supposed to use the BMW CarData API. This API is free to use with your own car. It doesn’t allow control commands, but I don’t need that anyway. In this topic, I want to discuss making use of this data in Home Assistant (or any MQTT consumer)!


:information_source: Don’t want to tinker? Check out the BimmerData Streamline custom HA integration. It may be suitable for your needs!


:warning: BMW CarData availability is limited to a few countries.
:warning: BMW CarData does not allow controlling EV charging.

BMW CarData has two APIs, one is a REST API and one is an MQTT server. I found the REST API to be fairly useless, it offers little data of interest. The MQTT server, on the other hand, basically throws all the information you could ever want at you.

Authentication with the MQTT server is handled using an OAuth2 REST API. And then, you already have an MQTT server inside HA already anyway. Luckily, someone went and created a forwarding agent for BMW CarData specifically:

I’ve been running it for quite some time now and it works flawlessly. Thank you, dj0abr!

When setting this up yourself, make sure to follow the guides:

  1. First, from the BimmerData Streamline custom integration, use the great instructions (quoted below) to simply enable all data subscriptions
  2. When using Docker, follow the instructions exactly, to facilitate the initial login

Connect the bridge to your MQTT broker (usually Mosquitto managed by Home Assistant). Further bridge configuration depends on whether custom processing is desired or not. Check the following posts!

Enabling all data, quoted from BimmerData Streamline README:

BMW Portal Setup (DON’T SKIP, DO THIS FIRST)

The CarData web portal isn’t available everywhere (e.g., it’s disabled in Finland). You can still enable streaming by logging in by using supported region. It doesn’t matter which language you select - all the generated Id and configuration is shared between all of them.

BMW

Mini

  1. Select the vehicle you want to stream.
  2. Choose BMW CarData or Mini CarData.
  3. Generate a client ID as described here: CarData Customer Portal
  4. Subscribe the client to both scopes: cardata:api:read (Request access to CarData API) and cardata:streaming:read (CarData Stream) and click authorize.
    Note, BMW portal seems to have some problems with scope selection. If you see an error on the top of the page, reload it, select one scope and wait for +30 seconds, then select the another one and wait agin.
  5. Scroll to the Data Selection section (Datenauswahl ändern) and load all descriptors (keep clicking “Load more”).
  6. Check every descriptor you want to stream. To automate this, open the browser console and run:
(() => {
  const labels = document.querySelectorAll('.css-k008qs label.chakra-checkbox');
  let changed = 0;

  labels.forEach(label => {
    const input = label.querySelector('input.chakra-checkbox__input[type="checkbox"]');
    if (!input || input.disabled || input.checked) return;

    label.click();
    if (!input.checked) {
      const ctrl = label.querySelector('.chakra-checkbox__control');
      if (ctrl) ctrl.click();
    }
    if (!input.checked) {
      input.checked = true;
      ['click', 'input', 'change'].forEach(type =>
        input.dispatchEvent(new Event(type, { bubbles: true }))
      );
    }
    if (input.checked) changed++;
  });

  console.log(`Checked ${changed} of ${labels.length} checkboxes.`);
})();

[…]

  1. Save the selection.
  2. Repeat for all the cars you want to support

[…]

Now you have the data you want on your MQTT broker. Continue reading to find out how to work with it!

It’s probably also possible to have the MQTT bridge run as a Home Assistant App (formerly Add-on). Sounds like a fun mini project!

With split topics

If you don’t want advanced data processing, just use the split topics feature!

In the bridge’s .env file, enable the following switches:

SPLIT_TOPICS=1
MQTT_RETAIN=1

That’s it, skip the rest of this post and move on to adding the entities to Home Assistant.

Process data with Node-RED

If you want to process the data yourself, Node-RED is a great solution. I process the data mostly just like the bridge’s split topics feature does, with one addition: I create a tracker entity for my car, so I can see its location in Home Assistant.

There’s also the as-of-yet untapped condition monitoring data. It looks quite interesting! More about that below.

Further configuration of the bridge is not required.

Raw messages from the CarData broker look like this:

{
  "vin": "WBAxxxxxxxxxxxxxx",
  "entityId": "9b8af880-f7f1-45ca-a9aa-2f834e4400f3",
  "topic": "WBAxxxxxxxxxxxxxx",
  "timestamp": "2026-03-27T17:55:52.58Z",
  "data": {
    "vehicle.cabin.window.row1.passenger.status": {
      "timestamp": "2026-03-27T17:55:48Z",
      "value": "CLOSED"
    }
  }
}

The Node-RED flow is like this:

  1. MQTT subscription node, bmw/WBAxxxxxxxxxxxxxx topic
  2. Function node (contents follow)
  3. MQTT publish node ( :exclamation: Enable “Retain”!)

The function node’s code:

if (!msg.payload.data) {
    //node.warn("No data property on payload");
    return null;
}

if (!msg.payload.vin) {
    //node.warn("No VIN on data");
    return null;
}

const key = Object.keys(msg.payload.data)[0];
if (!key) {
    //node.warn("No key on data object");
    return null;
}

/** @type {NodeMessage[]} */
const resultMsgs = [];

// Special handling: current location
if (key.startsWith("vehicle.cabin.infotainment.navigation.currentLocation")) {
    // Must be within 30 seconds of each other
    const deviationLimitMs = 30 * 1000;

    /** @type {{ timestamp: string, value: unknown, unit?: string }} */
    const value = msg.payload.data[key];
    const ts = value.timestamp && new Date(value.timestamp).getTime();

    if (ts && !isNaN(ts)) {
        switch (key) {
            case "vehicle.cabin.infotainment.navigation.currentLocation.heading":
                flow.set("heading", { ts, value });
                break;
            case "vehicle.cabin.infotainment.navigation.currentLocation.latitude":
                flow.set("latitude", { ts, value });
                break;
            case "vehicle.cabin.infotainment.navigation.currentLocation.longitude":
                flow.set("longitude", { ts, value });
                break;
            // case "vehicle.cabin.infotainment.navigation.currentLocation.altitude":
            //     flow.set("altitude", { ts, value });
            //     break;
        }

        /** @type {{ ts: number, value: { timestamp: string, value: unknown, unit?: string } | undefined}[]} */
        // const [heading, latitude, longitude, altitude] =
        //     flow.get(["heading", "latitude", "longitude", "altitude"]);
        const [heading, latitude, longitude] =
            flow.get(["heading", "latitude", "longitude"]);
        if (heading && latitude && longitude) {
            // Differences in milliseconds, absolute
            const timeDifferencesMs = [
                heading.ts - latitude.ts,
                heading.ts - longitude.ts,
                // heading.ts - altitude.ts,
            ].map(x => Math.abs(x));

            // All differences less than acceptable temporal deviation
            const withinLimits = timeDifferencesMs.every(x => x < deviationLimitMs);

            if (withinLimits) {
                // Push in HA-compatible way, key-value
                resultMsgs.push({
                    topic: `bimmer/${msg.payload.vin}/vehicle.cabin.infotainment.navigation.currentLocation`,
                    payload: {
                        heading: heading.value.value,
                        latitude: latitude.value.value,
                        longitude: longitude.value.value,
                        // altitude: altitude.value.value,
                    }
                });
            }
        }
    }
}

// Always publish transformed message
resultMsgs.push({
    topic: `bimmer/${msg.payload.vin}/${key}`,
    payload: msg.payload.data[key]
})

return resultMsgs;

I had problems with the altitude not being updated, so I disabled it. Maybe it’s more reliable on your car.

There’s more data to decode: The “teleservices” (condition monitoring stuff):

  • vehicle.channel.teleservice.lastAutomaticServiceCallTime
  • vehicle.channel.teleservice.lastTeleserviceReportTime
  • vehicle.channel.teleservice.status
  • vehicle.drivetrain.internalCombustionEngine.engine.ect
  • vehicle.electricalSystem.battery.voltage
  • vehicle.electronicControlUnit.diagnosticTroubleCodes.raw
  • vehicle.status.conditionBasedServicesCount
  • vehicle.status.serviceDistance.next
  • vehicle.status.serviceDistance.yellow
  • vehicle.status.serviceTime.hUandAuServiceYellow
  • vehicle.status.serviceTime.inspectionDateLegal
  • vehicle.status.serviceTime.yellow

Unfortunately, I haven’t figured out yet what triggers an update to this data, it appears somewhat random. Maybe I drove too little.

vehicle.electronicControlUnit.diagnosticTroubleCodes.raw looks particularly interesting, but I have no idea how to decode it. It looks like this on my car:

{
    "timestamp": "2026-01-21T10:54:45.719Z",
    "children": [
        {
            "vehicle.electronicControlUnit.diagnosticTroubleCodes.raw.address": {
                "timestamp": "2026-01-21T10:54:45.719Z",
                "value": "16"
            },
            "vehicle.electronicControlUnit.diagnosticTroubleCodes.raw.code": {
                "timestamp": "2026-01-21T10:54:45.719Z",
                "value": "CD0487"
            }
        },
        {
            "vehicle.electronicControlUnit.diagnosticTroubleCodes.raw.address": {
                "timestamp": "2026-01-21T10:54:45.719Z",
                "value": "114"
            },
            "vehicle.electronicControlUnit.diagnosticTroubleCodes.raw.code": {
                "timestamp": "2026-01-21T10:54:45.719Z",
                "value": "804833"
            }
        },
        {
            "vehicle.electronicControlUnit.diagnosticTroubleCodes.raw.address": {
                "timestamp": "2026-01-21T10:54:45.719Z",
                "value": "114"
            },
            "vehicle.electronicControlUnit.diagnosticTroubleCodes.raw.code": {
                "timestamp": "2026-01-21T10:54:45.719Z",
                "value": "80484C"
            }
        },
        "..."
    ]
}

Set up Home Assistant

To make use of the data in Home Assistant, you need to tell it what to look for and how to interpret it.

Please don’t take the following example verbatim, it contains placeholders:

  • Replace WBAxxxxxxxxxxxxxx with your vehicle’s actual VIN
  • Replace <prefix> with either bmw/vehicles (bridge split topics) or bimmer (Node-RED custom processing)
  • Update the device data on the first sensor to match your car

Only then add it to your configuration.yaml.

mqtt:
  sensor:
    - unique_id: "WBAxxxxxxxxxxxxxx-odometer"
      device:
        identifiers: ["WBAxxxxxxxxxxxxxx"]
        name: "BMW 318i"
        manufacturer: "BMW"
        model: "318i"
      name: "Odometer"
      icon: "mdi:counter"
      state_topic: "<prefix>/WBAxxxxxxxxxxxxxx/vehicle.vehicle.travelledDistance"
      unit_of_measurement: "km"
      device_class: distance
      state_class: measurement
      value_template: "{{ value_json.value if value_json else null }}"
      suggested_display_precision: 0

    - unique_id: "WBAxxxxxxxxxxxxxx-remainingFuel"
      device:
        identifiers: ["WBAxxxxxxxxxxxxxx"]
      name: "Remaining Fuel"
      icon: "mdi:gas-station"
      state_topic: "<prefix>/WBAxxxxxxxxxxxxxx/vehicle.drivetrain.fuelSystem.remainingFuel"
      unit_of_measurement: "L"
      device_class: volume_storage
      state_class: measurement
      value_template: "{{ value_json.value if value_json else null }}"
      suggested_display_precision: 0

    - unique_id: "WBAxxxxxxxxxxxxxx-lastRemainingRange"
      device:
        identifiers: ["WBAxxxxxxxxxxxxxx"]
      name: "Remaining Range"
      icon: "mdi:map-marker-distance"
      state_topic: "<prefix>/WBAxxxxxxxxxxxxxx/vehicle.drivetrain.lastRemainingRange"
      unit_of_measurement: "km"
      device_class: distance
      state_class: measurement
      value_template: "{{ value_json.value if value_json else null }}"
      suggested_display_precision: 0

  # This is only available with Node-RED processing
  device_tracker:
    - unique_id: "WBAxxxxxxxxxxxxxx-tracker"
      device:
        identifiers: ["WBAxxxxxxxxxxxxxx"]
      name: "Tracker"
      json_attributes_topic: "<prefix>/WBAxxxxxxxxxxxxxx/vehicle.cabin.infotainment.navigation.currentLocation"

There’s of course a lot more data. I suggest exploring the MQTT data using MQTT Explorer, MQTTX or a similar tool.

I didn’t bother with the following things because they aren’t reliable on my car:

  • Lock state
  • Door state (open/closed)
  • Windows (open/closed)
  • Hatch/trunk (open/closed)

But, as before: maybe it works fine with your BMW!

If you find something incorrect or not optimal with the configuration presented here, please comment. I am by no means a Home Assistant or MQTT expert.