BMW CarData service through MQTT

Hi,

I had some problems getting the solution from BredmichMichael working (I tried his first version a couple of months ago). I also looked at the Python solution, which was running fine, but I wanted a JavaScript-based approach. I used code from BredmichMichael and ideas from BucofskiTVDV.

Everything is contained in a single JS file.

Before you start, you need to have the MQTT and Telegram bindings installed and working.

From BMW you will need a Client ID, GCID, and streaming data access. Please follow the instructions from BredmichMichael in the openHAB Community thread: [openHAB Community] Connect to new BMW CarData service through MQTT .

Below you can find the required items.

// BMWClientID must be set before you can start.
// Request your Client ID at: https://www.bmw.de/de-de/mybmw/vehicle-overview
String    BMWClientID                       "Client ID [%s]"
// All items below are initialized after triggering ManualTokenRefresh
String    BMWTokenAccess                    "Access Token"
String    BMWTokenRefresh                   "Refresh Token"
String    BMWTokenID                        "ID Token"
String    BMWCodeDevice                     "Device Code"
String    BMWCodeUser                       "User Code"
String    BMWCodeChallenge                  "Challenge Code"
String    BMWCodeVerifier                   "Verifier Code"
Number    BMWExpire                         "Expiry date"
Switch    ManualTokenRefresh

The BMWMqtt.things file. You don’t need to configure anything in the GUI. The Broker and Datastream are created automatically. Search in this file for “add gcid/BMW ID LOGIN(email address)” and for “add gcid/VIN” (appearing multiple times), and replace these placeholders with your own values.

Bridge mqtt:broker:bmw "MQTT Broker iX3" [
    lwtQos=0,
    publickeypin=true,
    keepAlive=60,
    hostnameValidated=true,
    secure=true,
    birthRetain=true,
    shutdownRetain=true,
    certificatepin=true,
    password="this field is automatiically filled",
    protocol="TCP",
    qos=1,
    reconnectTime=60000,
    port=9000,
    mqttVersion="V5",
    host="customer.streaming-cardata.bmwgroup.com",
    lwtRetain=true,
    username="add gcid/BMW ID LOGIN(email address)",
    enableDiscovery=false
] {

  Thing topic bmwCar "iX3 Datastream" {
    Channels:
        Type number : lastRemainingRange "Actieradius resterend" [
            stateTopic="add gcid/VIN",
            transformationPattern="JSONPATH:$.data[\"vehicle.drivetrain.lastRemainingRange\"].value"
        ]
        Type number : timeToFullyCharged "Tijd tot vol" [
            stateTopic="add gcid/VIN",
            transformationPattern="JSONPATH:$.data[\"vehicle.drivetrain.electricEngine.charging.timeToFullyCharged\"].value"
        ]
        Type number : batteryHeader "Accu status %" [
            stateTopic="add gcid/VIN",
            transformationPattern="JSONPATH:$.data[\"vehicle.drivetrain.batteryManagement.header\"].value"
        ]
        Type number : travelledDistance "Kilometerstand" [
            stateTopic="add gcid/VIN",
            transformationPattern="JSONPATH:$.data[\"vehicle.vehicle.travelledDistance\"].value"
        ]
    }
}

And the JS file. Search for BotXXXXX and replace it with your own Telegram bot account.
var { items, rules, triggers, actions, log } = require("openhab");

// -----------------------------------------------------------------------------
// CONFIGURATION
// -----------------------------------------------------------------------------
const config = {
    // === TELEGRAM ===
    telegramCallback: "telegram:telegramBot:BotXXXXX:callbackRawEvent",
    telegramActionUID: "telegram:telegramBot:BotXXXXX",
    telegramReplyId: "TelegramBotXXXXReplyId",
    bmwAuthenticatieId: "BMWAuthenticatie",

    // === TOKEN ITEMS ===
    tokenRefreshItem: "BMWTokenRefresh",
    clientIDItem: "BMWClientID",
    tokenAccessItem: "BMWTokenAccess",
    refreshTokenExpiryItem: "BMWExpire",
    codeVerifierItem: "BMWCodeVerifier",
    codeChallengeItem: "BMWCodeChallenge",
    deviceCodeItem: "BMWCodeDevice",
    userCodeItem: "BMWCodeUser",
    tokenIdItem: "BMWTokenID",
    manualTriggerItem: "ManualTokenRefresh", 

    // === THINGS FILE / MQTT ===
    manualUpdateItem: "ManualUpdateBMWThings",
    mqttThingsFile: "/openhab/conf/things/BMWMqtt.things",

    // === MISC ===
    tokenEndpointHost: "customer.bmwgroup.com"
};
const HTTP_TIMEOUT = 15000;

let refreshTriggerTime = null;
let refreshTimer = null;
let refreshBeforeExpire = 1; // minutes
let pollingTimer = null;
let pollingIntervalMs = 5000;
let pollingStartTime = 0;
const MAX_POLLING_TIME_MS = 4 * 60 * 1000;

// -----------------------------------------------------------------------------
// LOGGER
// -----------------------------------------------------------------------------
const logger = log("BMWTokenManager");
const DEBUG = true;
function dbg(msg) { if (DEBUG) logger.info("[DEBUG] " + msg); }

// -----------------------------------------------------------------------------
// TELEGRAM SUPPORT
// -----------------------------------------------------------------------------
function getTelegramAction() {
    const tg = actions.get("telegram", config.telegramActionUID);
    if (!tg) logger.error("telegramAction is NULL - check Thing UID");
    return tg;
}
function sendTelegramSafe(message) {
    const tg = getTelegramAction();
    if (tg) tg.sendTelegram(message);
}
function sendTelegramAnswerSafe(replyId, message) {
    const tg = getTelegramAction();
    if (tg) tg.sendTelegramAnswer(replyId, message);
}

// -----------------------------------------------------------------------------
// UTILITY FUNCTIONS
// -----------------------------------------------------------------------------
function encodeParam(key, value) {
    const URLEncoder = Java.type("java.net.URLEncoder");
    const StandardCharsets = Java.type("java.nio.charset.StandardCharsets");
    return URLEncoder.encode(key, StandardCharsets.UTF_8.name()) + "=" +
           URLEncoder.encode(value, StandardCharsets.UTF_8.name());
}
function formatDate(epochMillis) {
    const d = new Date(epochMillis);
    const p = (v) => String(v).padStart(2, "0");
    return `${p(d.getDate())}.${p(d.getMonth()+1)}.${d.getFullYear()} ${p(d.getHours())}:${p(d.getMinutes())}`;
}
function formatDateLong(epochMillis) { return new Date(epochMillis).toString(); }
function getItemState(name) {
    try {
        const it = items.getItem(name);
        if (!it || !it.state || it.state == "NULL" || it.state == "UNDEF") return null;
        return it.state.toString();
    } catch (e) {
        logger.error("getItemState error for " + name + ": " + e);
        return null;
    }
}
function buildTelegramText(url, exp) {
    return ["Open the link below (first-time login):\n", url, "\n",
            `This authorization is valid for ${exp} minutes.`].join("\n");
}

// -----------------------------------------------------------------------------
// SUBSYSTEM: DEVICE FLOW (Initial Authentication)
// -----------------------------------------------------------------------------

// PKCE generators
function generateCodeVerifier() {
    const SecureRandom = Java.type("java.security.SecureRandom");
    const Base64 = Java.type("java.util.Base64");
    const random = new SecureRandom();
    const bytes = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, 32);
    random.nextBytes(bytes);
    return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
function generateCodeChallenge(verifier) {
    const MessageDigest = Java.type("java.security.MessageDigest");
    const Base64 = Java.type("java.util.Base64");
    const StandardCharsets = Java.type("java.nio.charset.StandardCharsets");
    const digest = MessageDigest.getInstance("SHA-256");
    const hash = digest.digest(verifier.getBytes(StandardCharsets.UTF_8));
    return Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
}

// Device code flow
function deviceCodeFlowAndAuthorisation() {
    try {
        const clientId = getItemState(config.clientIDItem);
        if (!clientId) return logger.warn("Client ID missing – abort");

        const verifier = generateCodeVerifier();
        const challenge = generateCodeChallenge(verifier);
        items.getItem(config.codeVerifierItem).postUpdate(verifier);
        items.getItem(config.codeChallengeItem).postUpdate(challenge);

        const payload = [
            encodeParam("client_id", clientId),
            encodeParam("response_type", "device_code"),
            encodeParam("scope", "authenticate_user openid cardata:streaming:read cardata:api:read"),
            encodeParam("code_challenge", challenge),
            encodeParam("code_challenge_method", "S256")
        ].join("&");

        const headers = new java.util.HashMap();
        headers.put("Accept", "application/json");

        const url = "https://customer.bmwgroup.com/gcdm/oauth/device/code";
        const resp = actions.HTTP.sendHttpPostRequest(url, "application/x-www-form-urlencoded", payload, headers, HTTP_TIMEOUT);
        const json = JSON.parse(resp);

        items.getItem(config.deviceCodeItem).postUpdate(json.device_code);
        items.getItem(config.userCodeItem).postUpdate(json.user_code);
        items.getItem(config.manualTriggerItem).postUpdate("OFF");

        const urlWithCode = json.verification_uri_complete || `${json.verification_uri}?user_code=${json.user_code}`;
        sendTelegramSafe(buildTelegramText(urlWithCode, Math.floor((json.expires_in || 600)/60)));

        const tg = getTelegramAction();
        if (tg) {
            tg.sendTelegramQuery("BMW Auth: confirm login", config.bmwAuthenticatieId, "Good", "Failed");
            items.getItem(config.telegramReplyId).postUpdate(config.bmwAuthenticatieId);
        }

    } catch (e) {
        logger.error("deviceCodeFlow error: " + e);
    }
}

// Poller
function startTokenPolling() {
    if (pollingTimer) clearTimeout(pollingTimer);
    pollingStartTime = Date.now();
    pollingIntervalMs = 5000;
    pollingTimer = setTimeout(pollForToken, pollingIntervalMs);
}
function stopTokenPolling() {
    if (pollingTimer) clearTimeout(pollingTimer);
    pollingTimer = null;
}
function pollForToken() {
    if (Date.now() - pollingStartTime > MAX_POLLING_TIME_MS) {
        sendTelegramSafe("⏳ BMW Auth: authorization timed out");
        stopTokenPolling();
        return;
    }
    try {
        dbg("[Poller] Attempting to retrieve token…");

        const clientId = getItemState(config.clientIDItem);
        const deviceCode = getItemState(config.deviceCodeItem);
        const verifier = getItemState(config.codeVerifierItem);

        if (!clientId || !deviceCode || !verifier) {
            logger.error("[Poller] Missing required data; stopping poller.");
            stopTokenPolling();
            return;
        }

        const headers = new java.util.HashMap();
        headers.put("Accept", "application/json");
        headers.put("User-Agent", "openHAB BMW Token Poller");

        const payload = [
            encodeParam("client_id", clientId),
            encodeParam("device_code", deviceCode),
            encodeParam("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
            encodeParam("code_verifier", verifier),
        ].join("&");

        const url = "https://customer.bmwgroup.com/gcdm/oauth/token";
        const result = actions.HTTP.sendHttpPostRequest(url, "application/x-www-form-urlencoded", payload, headers, HTTP_TIMEOUT);

        if (!result) {
            logger.error("[Poller] Empty response from BMW during polling; stopping.");
            stopTokenPolling();
            return;
        }

        const json = JSON.parse(result);

        if (json.error) {
            if (json.error === "authorization_pending") {
                dbg("[Poller] authorization_pending → retry in " + pollingIntervalMs/1000 + "s");
                pollingTimer = setTimeout(pollForToken, pollingIntervalMs);
                return;
            }
            if (json.error === "slow_down") {
                pollingIntervalMs = Math.min(pollingIntervalMs + 5000, 15000);
                dbg("[Poller] slow_down → new interval " + pollingIntervalMs/1000 + "s");
                pollingTimer = setTimeout(pollForToken, pollingIntervalMs);
                return;
            }
            if (json.error === "expired_token") {
                logger.error("[Poller] Device code expired — user must authorize again.");
                sendTelegramSafe("❌ Authorization expired at BMW. Please restart the login process.");
                stopTokenPolling();
                return;
            }
            logger.error("[Poller] BMW Error during poll: " + json.error + " - " + (json.error_description || ""));
            stopTokenPolling();
            return;
        }

        if (json.access_token && json.refresh_token) {
            dbg("[Poller] >>> TOKEN RECEIVED");
            items.getItem(config.tokenAccessItem).postUpdate(json.access_token);
            items.getItem(config.tokenRefreshItem).postUpdate(json.refresh_token);

            if (json.id_token) {
                items.getItem(config.tokenIdItem).postUpdate(json.id_token);
                const ok = updateMqttPassword(json.id_token);
                const tg = getTelegramAction();
                if (ok) {
                    logger.info("New tokenIdItem written to things file (poller).");
                    sendTelegramSafe("âś… Tokens retrieved successfully and things file updated.");
                } else {
                    logger.warn("Could not write BMWTokenID to things file (poller).");
                    sendTelegramSafe("⚠️ Tokens retrieved, but updating the things file failed.");
                }
            }

            const expires = Date.now() + (json.expires_in || 3600) * 1000 - 5*60*1000;
            items.getItem(config.refreshTokenExpiryItem).postUpdate(String(expires));
            dbg("[Poller] Token received, expires at: " + formatDateLong(expires));

            stopTokenPolling();
            dbg("[Poller] Poller finished successfully.");
            return;
        }

        logger.error("[Poller] Unexpected poll response (no tokens). Stopping.");
        stopTokenPolling();

    } catch (e) {
        logger.error("[Poller] Exception while polling: " + e + " - " + (e.stack || "no stack"));
        stopTokenPolling();
    }
}

Good luck

2 Likes

Hi,

Thank you for your work - I’m trying to make it work, but I kept on getting the error CONNECT failed as CONNACK contained an Error Code: BAD_USER_NAME_OR_PASSWORD.

  1. I noticed that the only way my given clientid is used if I change the line in the .things file to clientID= - that way it also shows up in PaperUI, otherwise a new clientid was generated every time
  2. username=“add gcid/username”, - it wasn’t clear as the gcid was referred to as the “username” under step 9
    https://community.openhab.org/t/connect-to-new-bmw-cardata-service-through-mqtt/166693
    I think it could be useful for people to know that you do put in both: gcid/[connecteddrive username]
  3. I had to fill in the password= field with the content of id_token to make the broker go online.

Now it is online, I’m continuing the work with it.

Thanks!!

Use the switch ManualTokenRefresh for this.

1 Like

This line is wrong and not used. Remove the line

1 Like

Thanks, clientid row removed.
When I set it up yesterday it worked for like 5-10 minutes, it gave me some (wrong) values on the channels - then it went OFFLINE with the error above… Since then whatever I do I get the username/passw error and it stays offline.

I can ping customer.bmwgroup.com successfully, but not customer.streaming-cardata.bmwgroup.com btw.

I still need to set up my telegram stuff though…

Telegram part:
Also not clear:

TelegramBotXXXXReplyId→ is it TelegramBot”replyID”ReplyId or replace the whole TelegramBotXXXXReplyId with the replyID? Is replyID same as the BotXXXXX inside the previous lines or is it the chatID or the other id from the bot response when querying https://api.telegram.org/bot<token>/sendMessage?chat_id=<chatId>&text=testing?

→ {“ok”:true,“result”:{“message_id”:4,“from”:{“id”:1234567890,“is_bot”:true,“first_name”:“MyBMW”,“username”:“MyBMW_bot”},“chat”:{“id”:2345678901,“first_name”:“First”,“last_name”:“Last”,“type”:“private”},“date”:1766311018,“text”:“testing”}}

This is what I’ve done:

telegramCallback: “telegram:telegramBot:bot12356…mybotID:callbackRawEvent”,
telegramActionUID: “telegram:telegramBot:bot12356…mybotID”,
telegramReplyId: “TelegramBotbot12356…mybotIDReplyId”,

Sorry for the lame questions, this is my first try via Telegram. I might be missing something obvious. Should I turn TRACE on for the Telegram binding to see if it works well?

Is a requirement to start proces.

I assume you only have to replace XXXXX with your own name. Use the doc from the binding Telegram - Bindings | openHAB and test it.

Indeed, thanks.

Okay, I re-generated the items on the BMW site and with the new codes all turned green and manualrefresh also works.
I don’t see however the Strings in the item file filled in at all - is that normal?
I also added my id_token to the password row - it might not be needed but before it didn’t work and also thee guide step 12 (Connect to new BMW CarData service through MQTT) says so.

Do we need a .rules file to trigger the refreshes and the data flow? I can’t see any updates yet on the channels.

I tried by locking the car via the MyBMW app but that didn’t seem to trigger any updates.

Is telegram installed tested and used? Find below the items I use

Number    BMWTravelledDistance              "Kilometerstand"            {channel="mqtt:topic:bmw:bmwCar:travelledDistance"}
Number    BMWLastRemainingRange             "Actieradius resterend"     {channel="mqtt:topic:bmw:bmwCar:lastRemainingRange"}
Number    BMWBatteryHeader                  "Accu status"               {channel="mqtt:topic:bmw:bmwCar:batteryHeader"}
Number    BMWTimeToFullyCharged             "Tijd tot vol"              {channel="mqtt:topic:bmw:bmwCar:timeToFullyCharged"}

Installed and tested with the test message as described in the binding description. I don’t know how else I could test it under Openhab tbh.
The items part is ok, km stand and remaining range are bound to two items, but they don’t seem to get updates - both things (broker, data) are green though I just don’t know what should trigger them…

  1. Did you use the switch ManualTokenRefresh and received a message on telegram?
  2. Did you login (if asked) after activating the link and got a good result?
  3. Send the telegram reply with good?
  4. Received the 2 messages on telegram?
  5. Look in the logfile with debug enabled
const logger = log("BMWTokenManager");
const DEBUG = true;
function dbg(msg) { if (DEBUG) logger.info("[DEBUG] " + msg); }

Unfortunately I couldn’t make it work for me so I returned to Bredmich’s solution and after some tinkering that one does work now. I’m sure yours does too and it must be me not getting the full picture.
Telegram was not really working well neither for your solution nor for his - so in the end I went for logviewer and worked it out via that.
Thanks anyhow!

This topic was automatically closed 41 days after the last reply. New replies are no longer allowed.